fix(portal): 溯源連結兼容 kb:// 與兩種 gitea scheme(daemon-beta t21)
新 ingest 鏈(rag_ingest_card/rag_ingest_direct)寫的 metadata source 是
kb://<path>,但 portal 溯源只認 gitea://——B4 部好溯源後一經重灌即再斷鏈。
- gitea://<path>(舊 rag_ingest v2):{base}/<path>,行為一字不變(不回歸)
- gitea:<org/repo>@<path>(km-wiki-ingest 實形):
{base}/<org/repo>/src/branch/main/<path>,#segNN 錨點捨棄
- kb://<path>(新 ingest 鏈實形):雲端沒有對應網頁,不造死鏈——
卡片詳頁顯示本地路徑文字+「在你的同步資料夾內」標註(沿用既有
sourceWebBase 未設時的純文字降級樣式);AI 出處列同標註
- 測試:console-ui/tests/portal-source-href.test.mjs(node --test,
抽真身 srcHref/srcLocalPath 求值,6 case 全綠);package.json 加 test script
- public/portal/index.html 用 ARCRUN_API_BASE=https://cypher.arcrun.dev
重 build(與既有產物同參數,diff 僅本次 scheme 改動)
實形出處:kb:// = arcrun-rag workflows/rag-ingest.yaml:115('kb://'+path);
gitea:@ = registry/examples/km-wiki-ingest/workflow.yaml:276。
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
"description": "Arcrun Console / Portal 靜態前端(Cloudflare Pages)——從 cypher-executor 搬出的 UI 層",
|
"description": "Arcrun Console / Portal 靜態前端(Cloudflare Pages)——從 cypher-executor 搬出的 UI 層",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build.mjs",
|
"build": "node scripts/build.mjs",
|
||||||
|
"test": "node --test 'tests/*.test.mjs'",
|
||||||
"deploy": "node scripts/deploy.mjs",
|
"deploy": "node scripts/deploy.mjs",
|
||||||
"deploy:personal": "node scripts/deploy.mjs personal",
|
"deploy:personal": "node scripts/deploy.mjs personal",
|
||||||
"deploy:enterprise": "node scripts/deploy.mjs enterprise",
|
"deploy:enterprise": "node scripts/deploy.mjs enterprise",
|
||||||
|
|||||||
@@ -478,13 +478,35 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
|
|||||||
try { var m = JSON.parse(e.metadata_json || 'null'); return m && typeof m === 'object' ? m : {}; } catch (er) { return {}; }
|
try { var m = JSON.parse(e.metadata_json || 'null'); return m && typeof m === 'object' ? m : {}; } catch (er) { return {}; }
|
||||||
}
|
}
|
||||||
function entrySource(e) { var m = entryMeta(e); return typeof m.source === 'string' ? m.source : ''; }
|
function entrySource(e) { var m = entryMeta(e); return typeof m.source === 'string' ? m.source : ''; }
|
||||||
// 來源回溯超連結:PORTAL_SOURCE_WEB_BASE 有設且 source 是 gitea:// 才給 href;
|
// 來源回溯超連結:PORTAL_SOURCE_WEB_BASE 有設才給 href。認兩種 gitea scheme(t21):
|
||||||
// 其餘回空字串=維持純文字(行為與未設時一字不變)。chunk 錨點(#n)捨棄。
|
// gitea://<path> → {base}/<path>(base 已含 repo 瀏覽前綴;舊 rag_ingest v2,行為不變)
|
||||||
|
// gitea:<org/repo>@<path> → {base}/<org/repo>/src/branch/main/<path>(km-wiki-ingest;base=Gitea 站根)
|
||||||
|
// kb://<path>=用戶自己同步資料夾內的相對路徑,雲端沒有對應網頁 → 回空字串不造死鏈;
|
||||||
|
// 顯示交給 srcLocalPath()(誠實標「在你的同步資料夾內」)。其餘 scheme 回空=維持純文字。
|
||||||
|
// chunk/segment 錨點(#n / #segNN)捨棄——Gitea 網頁沒有對應錨。
|
||||||
var SOURCE_WEB_BASE = "";
|
var SOURCE_WEB_BASE = "";
|
||||||
|
function encSrcPath(p) { return p.split('/').map(encodeURIComponent).join('/'); }
|
||||||
function srcHref(src) {
|
function srcHref(src) {
|
||||||
if (!SOURCE_WEB_BASE || String(src).indexOf('gitea://') !== 0) return '';
|
src = String(src);
|
||||||
var p = String(src).slice(8).replace(/#\d+$/, '');
|
if (!SOURCE_WEB_BASE) return '';
|
||||||
return SOURCE_WEB_BASE.replace(/\/+$/, '') + '/' + p.split('/').map(encodeURIComponent).join('/');
|
var base = SOURCE_WEB_BASE.replace(/\/+$/, '');
|
||||||
|
if (src.indexOf('gitea://') === 0) {
|
||||||
|
var p = src.slice(8).replace(/#\d+$/, '');
|
||||||
|
return base + '/' + encSrcPath(p);
|
||||||
|
}
|
||||||
|
if (src.indexOf('gitea:') === 0) {
|
||||||
|
var rest = src.slice(6).replace(/#[^#]*$/, '');
|
||||||
|
var at = rest.indexOf('@');
|
||||||
|
if (at <= 0 || at >= rest.length - 1) return '';
|
||||||
|
return base + '/' + encSrcPath(rest.slice(0, at)) + '/src/branch/main/' + encSrcPath(rest.slice(at + 1));
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// kb:// 來源的本地相對路徑(去 scheme、去錨點);非 kb:// 回空字串。
|
||||||
|
function srcLocalPath(src) {
|
||||||
|
src = String(src);
|
||||||
|
if (src.indexOf('kb://') !== 0) return '';
|
||||||
|
return src.slice(5).replace(/#[^#]*$/, '');
|
||||||
}
|
}
|
||||||
function entryLib(e) { var m = entryMeta(e); return (typeof m.library === 'string' && m.library) ? m.library : 'general'; }
|
function entryLib(e) { var m = entryMeta(e); return (typeof m.library === 'string' && m.library) ? m.library : 'general'; }
|
||||||
function entryTitle(e) {
|
function entryTitle(e) {
|
||||||
@@ -713,10 +735,12 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
|
|||||||
var page = (s && (s.page || s.page_name)) || '';
|
var page = (s && (s.page || s.page_name)) || '';
|
||||||
var mode = (s && s.mode) || '';
|
var mode = (s && s.mode) || '';
|
||||||
var href = s && s.source ? srcHref(s.source) : '';
|
var href = s && s.source ? srcHref(s.source) : '';
|
||||||
|
var localPath = s && s.source ? srcLocalPath(s.source) : '';
|
||||||
return '<div class="nbrow" data-aisrc="' + esc(page) + '">' +
|
return '<div class="nbrow" data-aisrc="' + esc(page) + '">' +
|
||||||
(mode ? '<span class="tag" style="flex:none">' + esc(mode) + '</span>' : '') +
|
(mode ? '<span class="tag" style="flex:none">' + esc(mode) + '</span>' : '') +
|
||||||
'<span style="word-break:break-word">' + esc(page || '(無頁名)') + '</span>' +
|
'<span style="word-break:break-word">' + esc(page || '(無頁名)') + '</span>' +
|
||||||
(href ? '<a href="' + esc(href) + '" target="_blank" rel="noopener" style="flex:none;color:var(--amber);font-size:13px" title="開啟來源檔">來源 ↗</a>' : '') +
|
(href ? '<a href="' + esc(href) + '" target="_blank" rel="noopener" style="flex:none;color:var(--amber);font-size:13px" title="開啟來源檔">來源 ↗</a>' : '') +
|
||||||
|
(!href && localPath ? '<span class="dim mono" style="flex:none;font-size:12.5px" title="' + esc(localPath) + '">在你的同步資料夾內</span>' : '') +
|
||||||
'<span style="margin-left:auto;color:rgba(var(--amber-rgb),.6);flex:none">›</span></div>';
|
'<span style="margin-left:auto;color:rgba(var(--amber-rgb),.6);flex:none">›</span></div>';
|
||||||
}).join('') + '</div>';
|
}).join('') + '</div>';
|
||||||
}
|
}
|
||||||
@@ -822,9 +846,17 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
|
|||||||
(src
|
(src
|
||||||
? (function () {
|
? (function () {
|
||||||
var href = srcHref(src);
|
var href = srcHref(src);
|
||||||
var body = href
|
var localPath = srcLocalPath(src);
|
||||||
? '<a class="mono" style="font-size:14px;color:var(--amber);word-break:break-all" href="' + esc(href) + '" target="_blank" rel="noopener">' + esc(src) + ' ↗</a>'
|
var body;
|
||||||
: '<span class="mono" style="font-size:14px;color:var(--amber);word-break:break-all">' + esc(src) + '</span>';
|
if (href) {
|
||||||
|
body = '<a class="mono" style="font-size:14px;color:var(--amber);word-break:break-all" href="' + esc(href) + '" target="_blank" rel="noopener">' + esc(src) + ' ↗</a>';
|
||||||
|
} else if (localPath) {
|
||||||
|
// kb://=本地同步資料夾的相對路徑,雲端沒有對應網頁——誠實顯示路徑文字+標註,不造死鏈。
|
||||||
|
body = '<span class="mono" style="font-size:14px;color:var(--amber);word-break:break-all">' + esc(localPath) + '</span>' +
|
||||||
|
'<span class="dim" style="font-size:12.5px;flex:none">在你的同步資料夾內</span>';
|
||||||
|
} else {
|
||||||
|
body = '<span class="mono" style="font-size:14px;color:var(--amber);word-break:break-all">' + esc(src) + '</span>';
|
||||||
|
}
|
||||||
return '<div class="srcline"><span style="font-size:13px;letter-spacing:.14em;color:rgba(var(--ink-rgb),.5);flex:none">來源回溯</span>' + body + '</div>';
|
return '<div class="srcline"><span style="font-size:13px;letter-spacing:.14em;color:rgba(var(--ink-rgb),.5);flex:none">來源回溯</span>' + body + '</div>';
|
||||||
})()
|
})()
|
||||||
: '');
|
: '');
|
||||||
|
|||||||
@@ -504,13 +504,35 @@ function renderPortalHtml(brand: string, sourceWebBase = ''): string {
|
|||||||
try { var m = JSON.parse(e.metadata_json || 'null'); return m && typeof m === 'object' ? m : {}; } catch (er) { return {}; }
|
try { var m = JSON.parse(e.metadata_json || 'null'); return m && typeof m === 'object' ? m : {}; } catch (er) { return {}; }
|
||||||
}
|
}
|
||||||
function entrySource(e) { var m = entryMeta(e); return typeof m.source === 'string' ? m.source : ''; }
|
function entrySource(e) { var m = entryMeta(e); return typeof m.source === 'string' ? m.source : ''; }
|
||||||
// 來源回溯超連結:PORTAL_SOURCE_WEB_BASE 有設且 source 是 gitea:// 才給 href;
|
// 來源回溯超連結:PORTAL_SOURCE_WEB_BASE 有設才給 href。認兩種 gitea scheme(t21):
|
||||||
// 其餘回空字串=維持純文字(行為與未設時一字不變)。chunk 錨點(#n)捨棄。
|
// gitea://<path> → {base}/<path>(base 已含 repo 瀏覽前綴;舊 rag_ingest v2,行為不變)
|
||||||
|
// gitea:<org/repo>@<path> → {base}/<org/repo>/src/branch/main/<path>(km-wiki-ingest;base=Gitea 站根)
|
||||||
|
// kb://<path>=用戶自己同步資料夾內的相對路徑,雲端沒有對應網頁 → 回空字串不造死鏈;
|
||||||
|
// 顯示交給 srcLocalPath()(誠實標「在你的同步資料夾內」)。其餘 scheme 回空=維持純文字。
|
||||||
|
// chunk/segment 錨點(#n / #segNN)捨棄——Gitea 網頁沒有對應錨。
|
||||||
var SOURCE_WEB_BASE = ${JSON.stringify(sourceWebBase)};
|
var SOURCE_WEB_BASE = ${JSON.stringify(sourceWebBase)};
|
||||||
|
function encSrcPath(p) { return p.split('/').map(encodeURIComponent).join('/'); }
|
||||||
function srcHref(src) {
|
function srcHref(src) {
|
||||||
if (!SOURCE_WEB_BASE || String(src).indexOf('gitea://') !== 0) return '';
|
src = String(src);
|
||||||
var p = String(src).slice(8).replace(/#\\d+$/, '');
|
if (!SOURCE_WEB_BASE) return '';
|
||||||
return SOURCE_WEB_BASE.replace(/\\/+$/, '') + '/' + p.split('/').map(encodeURIComponent).join('/');
|
var base = SOURCE_WEB_BASE.replace(/\\/+$/, '');
|
||||||
|
if (src.indexOf('gitea://') === 0) {
|
||||||
|
var p = src.slice(8).replace(/#\\d+$/, '');
|
||||||
|
return base + '/' + encSrcPath(p);
|
||||||
|
}
|
||||||
|
if (src.indexOf('gitea:') === 0) {
|
||||||
|
var rest = src.slice(6).replace(/#[^#]*$/, '');
|
||||||
|
var at = rest.indexOf('@');
|
||||||
|
if (at <= 0 || at >= rest.length - 1) return '';
|
||||||
|
return base + '/' + encSrcPath(rest.slice(0, at)) + '/src/branch/main/' + encSrcPath(rest.slice(at + 1));
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
// kb:// 來源的本地相對路徑(去 scheme、去錨點);非 kb:// 回空字串。
|
||||||
|
function srcLocalPath(src) {
|
||||||
|
src = String(src);
|
||||||
|
if (src.indexOf('kb://') !== 0) return '';
|
||||||
|
return src.slice(5).replace(/#[^#]*$/, '');
|
||||||
}
|
}
|
||||||
function entryLib(e) { var m = entryMeta(e); return (typeof m.library === 'string' && m.library) ? m.library : 'general'; }
|
function entryLib(e) { var m = entryMeta(e); return (typeof m.library === 'string' && m.library) ? m.library : 'general'; }
|
||||||
function entryTitle(e) {
|
function entryTitle(e) {
|
||||||
@@ -739,10 +761,12 @@ function renderPortalHtml(brand: string, sourceWebBase = ''): string {
|
|||||||
var page = (s && (s.page || s.page_name)) || '';
|
var page = (s && (s.page || s.page_name)) || '';
|
||||||
var mode = (s && s.mode) || '';
|
var mode = (s && s.mode) || '';
|
||||||
var href = s && s.source ? srcHref(s.source) : '';
|
var href = s && s.source ? srcHref(s.source) : '';
|
||||||
|
var localPath = s && s.source ? srcLocalPath(s.source) : '';
|
||||||
return '<div class="nbrow" data-aisrc="' + esc(page) + '">' +
|
return '<div class="nbrow" data-aisrc="' + esc(page) + '">' +
|
||||||
(mode ? '<span class="tag" style="flex:none">' + esc(mode) + '</span>' : '') +
|
(mode ? '<span class="tag" style="flex:none">' + esc(mode) + '</span>' : '') +
|
||||||
'<span style="word-break:break-word">' + esc(page || '(無頁名)') + '</span>' +
|
'<span style="word-break:break-word">' + esc(page || '(無頁名)') + '</span>' +
|
||||||
(href ? '<a href="' + esc(href) + '" target="_blank" rel="noopener" style="flex:none;color:var(--amber);font-size:13px" title="開啟來源檔">來源 ↗</a>' : '') +
|
(href ? '<a href="' + esc(href) + '" target="_blank" rel="noopener" style="flex:none;color:var(--amber);font-size:13px" title="開啟來源檔">來源 ↗</a>' : '') +
|
||||||
|
(!href && localPath ? '<span class="dim mono" style="flex:none;font-size:12.5px" title="' + esc(localPath) + '">在你的同步資料夾內</span>' : '') +
|
||||||
'<span style="margin-left:auto;color:rgba(var(--amber-rgb),.6);flex:none">›</span></div>';
|
'<span style="margin-left:auto;color:rgba(var(--amber-rgb),.6);flex:none">›</span></div>';
|
||||||
}).join('') + '</div>';
|
}).join('') + '</div>';
|
||||||
}
|
}
|
||||||
@@ -848,9 +872,17 @@ function renderPortalHtml(brand: string, sourceWebBase = ''): string {
|
|||||||
(src
|
(src
|
||||||
? (function () {
|
? (function () {
|
||||||
var href = srcHref(src);
|
var href = srcHref(src);
|
||||||
var body = href
|
var localPath = srcLocalPath(src);
|
||||||
? '<a class="mono" style="font-size:14px;color:var(--amber);word-break:break-all" href="' + esc(href) + '" target="_blank" rel="noopener">' + esc(src) + ' ↗</a>'
|
var body;
|
||||||
: '<span class="mono" style="font-size:14px;color:var(--amber);word-break:break-all">' + esc(src) + '</span>';
|
if (href) {
|
||||||
|
body = '<a class="mono" style="font-size:14px;color:var(--amber);word-break:break-all" href="' + esc(href) + '" target="_blank" rel="noopener">' + esc(src) + ' ↗</a>';
|
||||||
|
} else if (localPath) {
|
||||||
|
// kb://=本地同步資料夾的相對路徑,雲端沒有對應網頁——誠實顯示路徑文字+標註,不造死鏈。
|
||||||
|
body = '<span class="mono" style="font-size:14px;color:var(--amber);word-break:break-all">' + esc(localPath) + '</span>' +
|
||||||
|
'<span class="dim" style="font-size:12.5px;flex:none">在你的同步資料夾內</span>';
|
||||||
|
} else {
|
||||||
|
body = '<span class="mono" style="font-size:14px;color:var(--amber);word-break:break-all">' + esc(src) + '</span>';
|
||||||
|
}
|
||||||
return '<div class="srcline"><span style="font-size:13px;letter-spacing:.14em;color:rgba(var(--ink-rgb),.5);flex:none">來源回溯</span>' + body + '</div>';
|
return '<div class="srcline"><span style="font-size:13px;letter-spacing:.14em;color:rgba(var(--ink-rgb),.5);flex:none">來源回溯</span>' + body + '</div>';
|
||||||
})()
|
})()
|
||||||
: '');
|
: '');
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
/**
|
||||||
|
* Portal 溯源 scheme 測試(daemon-beta t21 — kb:// vs gitea:// 斷鏈地雷)
|
||||||
|
*
|
||||||
|
* 驗的是 src/portal-ui.ts 內嵌客戶端 JS 的 srcHref / srcLocalPath:
|
||||||
|
* gitea://<path> → {base}/<path>(舊 rag_ingest v2 實形,行為不回歸)
|
||||||
|
* gitea:<org/repo>@<path> → {base}/<org/repo>/src/branch/main/<path>(km-wiki-ingest 實形,
|
||||||
|
* 見 registry/examples/km-wiki-ingest/workflow.yaml `gitea:${repo}@${relPath}`)
|
||||||
|
* kb://<path> → 不造連結(雲端無對應網頁),srcLocalPath 給本地路徑文字,
|
||||||
|
* UI 標「在你的同步資料夾內」(arcrun-rag rag-ingest.yaml `'kb://' + path`)
|
||||||
|
*
|
||||||
|
* 測法:從 portal-ui.ts 的 template literal 抽出三個函式原始碼(brace 平衡掃描),
|
||||||
|
* 還原 template 跳脫(`\\` → `\`)後用 new Function 求值——測的就是真正會被
|
||||||
|
* build 進 public/portal/index.html 的那份 JS,零外部依賴(console-ui 哲學)。
|
||||||
|
*/
|
||||||
|
import { test } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import { readFileSync } from 'node:fs';
|
||||||
|
import { dirname, join } from 'node:path';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
|
||||||
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
||||||
|
const SRC = readFileSync(join(HERE, '..', 'src', 'portal-ui.ts'), 'utf8');
|
||||||
|
|
||||||
|
/** 從原始碼抽出 `function <name>(...) { ... }` 全文(brace 平衡;函式體無字串內大括號)。 */
|
||||||
|
function extractFn(name) {
|
||||||
|
const start = SRC.indexOf(`function ${name}(`);
|
||||||
|
assert.ok(start >= 0, `portal-ui.ts 找不到 function ${name}`);
|
||||||
|
const braceStart = SRC.indexOf('{', SRC.indexOf(')', start));
|
||||||
|
let i = braceStart + 1;
|
||||||
|
let depth = 1;
|
||||||
|
while (i < SRC.length && depth > 0) {
|
||||||
|
if (SRC[i] === '{') depth++;
|
||||||
|
else if (SRC[i] === '}') depth--;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
// template literal 內的 `\\` 在產出 HTML 時是 `\`——還原成瀏覽器實際跑的 JS
|
||||||
|
return SRC.slice(start, i).replace(/\\\\/g, '\\');
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 用指定 SOURCE_WEB_BASE 建出真正的 srcHref / srcLocalPath。 */
|
||||||
|
function makeFns(base) {
|
||||||
|
const code = [extractFn('encSrcPath'), extractFn('srcHref'), extractFn('srcLocalPath')].join('\n');
|
||||||
|
return new Function('SOURCE_WEB_BASE', `${code}\nreturn { srcHref, srcLocalPath };`)(base);
|
||||||
|
}
|
||||||
|
|
||||||
|
const BASE = 'https://gitea.example.com';
|
||||||
|
|
||||||
|
test('gitea://<path>(舊 rag_ingest v2)— 現行為不回歸:base + path、#n 錨點捨棄', () => {
|
||||||
|
const { srcHref } = makeFns(BASE + '/'); // 尾斜線要被吃掉
|
||||||
|
assert.equal(srcHref('gitea://system-dev/wiki/foo.md'), `${BASE}/system-dev/wiki/foo.md`);
|
||||||
|
assert.equal(srcHref('gitea://system-dev/wiki/foo.md#3'), `${BASE}/system-dev/wiki/foo.md`);
|
||||||
|
// 中文路徑逐段 encode
|
||||||
|
assert.equal(srcHref('gitea://docs/員工手冊.md'), `${BASE}/docs/${encodeURIComponent('員工手冊.md')}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('gitea:<org/repo>@<path>(km-wiki-ingest)— base/<org/repo>/src/branch/main/<path>', () => {
|
||||||
|
const { srcHref } = makeFns(BASE);
|
||||||
|
assert.equal(
|
||||||
|
srcHref('gitea:Leo/notes@system-dev/wiki/cards/foo.md'),
|
||||||
|
`${BASE}/Leo/notes/src/branch/main/system-dev/wiki/cards/foo.md`
|
||||||
|
);
|
||||||
|
// 分段錨點(#segNN)捨棄
|
||||||
|
assert.equal(
|
||||||
|
srcHref('gitea:Leo/notes@wiki/foo.md#seg02'),
|
||||||
|
`${BASE}/Leo/notes/src/branch/main/wiki/foo.md`
|
||||||
|
);
|
||||||
|
// 中文路徑逐段 encode
|
||||||
|
assert.equal(
|
||||||
|
srcHref('gitea:Leo/notes@docs/員工手冊.md'),
|
||||||
|
`${BASE}/Leo/notes/src/branch/main/docs/${encodeURIComponent('員工手冊.md')}`
|
||||||
|
);
|
||||||
|
// 缺 @ 或畸形 → 不造連結
|
||||||
|
assert.equal(srcHref('gitea:Leo/notes'), '');
|
||||||
|
assert.equal(srcHref('gitea:@wiki/foo.md'), '');
|
||||||
|
assert.equal(srcHref('gitea:Leo/notes@'), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('kb://<path>(新 ingest 鏈)— 不造死鏈、srcLocalPath 給本地路徑', () => {
|
||||||
|
const { srcHref, srcLocalPath } = makeFns(BASE);
|
||||||
|
assert.equal(srcHref('kb://人事/員工特休假規定.md'), '');
|
||||||
|
assert.equal(srcLocalPath('kb://人事/員工特休假規定.md'), '人事/員工特休假規定.md');
|
||||||
|
assert.equal(srcLocalPath('kb://foo.md#seg01'), 'foo.md');
|
||||||
|
// 非 kb:// 不給本地路徑
|
||||||
|
assert.equal(srcLocalPath('gitea://foo.md'), '');
|
||||||
|
assert.equal(srcLocalPath('gitea:Leo/notes@foo.md'), '');
|
||||||
|
assert.equal(srcLocalPath('https://example.com/x'), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('SOURCE_WEB_BASE 未設 — 全部 scheme 都不給 href(降級純文字),kb:// 顯示不受影響', () => {
|
||||||
|
const { srcHref, srcLocalPath } = makeFns('');
|
||||||
|
assert.equal(srcHref('gitea://wiki/foo.md'), '');
|
||||||
|
assert.equal(srcHref('gitea:Leo/notes@wiki/foo.md'), '');
|
||||||
|
assert.equal(srcHref('kb://foo.md'), '');
|
||||||
|
assert.equal(srcLocalPath('kb://foo.md'), 'foo.md'); // 本地路徑與 base 無關
|
||||||
|
});
|
||||||
|
|
||||||
|
test('未知 scheme — 誠實維持純文字(不猜連結)', () => {
|
||||||
|
const { srcHref, srcLocalPath } = makeFns(BASE);
|
||||||
|
assert.equal(srcHref('https://example.com/foo'), '');
|
||||||
|
assert.equal(srcHref('random-string'), '');
|
||||||
|
assert.equal(srcLocalPath('random-string'), '');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('UI 有 kb:// 的誠實標註(兩個顯示點:卡片詳頁 srcline + AI 出處列)', () => {
|
||||||
|
const hits = SRC.split('在你的同步資料夾內').length - 1;
|
||||||
|
assert.ok(hits >= 2, `「在你的同步資料夾內」標註應出現在 ≥2 個顯示點,實得 ${hits}`);
|
||||||
|
assert.ok(SRC.includes('srcLocalPath(src)'), '卡片詳頁應走 srcLocalPath 降級');
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user