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:
uncle6me-web
2026-07-24 15:38:39 +08:00
parent ab8e07201f
commit c662af7e2a
4 changed files with 189 additions and 16 deletions
+40 -8
View File
@@ -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 {}; }
}
function entrySource(e) { var m = entryMeta(e); return typeof m.source === 'string' ? m.source : ''; }
// 來源回溯超連結:PORTAL_SOURCE_WEB_BASE 有設且 source 是 gitea:// 才給 href
// 其餘回空字串=維持純文字(行為與未設時一字不變)。chunk 錨點(#n)捨棄。
// 來源回溯超連結:PORTAL_SOURCE_WEB_BASE 有設才給 href。認兩種 gitea schemet21):
// gitea://<path> → {base}/<path>base 已含 repo 瀏覽前綴;舊 rag_ingest v2,行為不變)
// gitea:<org/repo>@<path> → {base}/<org/repo>/src/branch/main/<path>km-wiki-ingestbaseGitea 站根)
// kb://<path>=用戶自己同步資料夾內的相對路徑,雲端沒有對應網頁 → 回空字串不造死鏈;
// 顯示交給 srcLocalPath()(誠實標「在你的同步資料夾內」)。其餘 scheme 回空=維持純文字。
// chunk/segment 錨點(#n / #segNN)捨棄——Gitea 網頁沒有對應錨。
var SOURCE_WEB_BASE = "";
function encSrcPath(p) { return p.split('/').map(encodeURIComponent).join('/'); }
function srcHref(src) {
if (!SOURCE_WEB_BASE || String(src).indexOf('gitea://') !== 0) return '';
var p = String(src).slice(8).replace(/#\d+$/, '');
return SOURCE_WEB_BASE.replace(/\/+$/, '') + '/' + p.split('/').map(encodeURIComponent).join('/');
src = String(src);
if (!SOURCE_WEB_BASE) return '';
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 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 mode = (s && s.mode) || '';
var href = s && s.source ? srcHref(s.source) : '';
var localPath = s && s.source ? srcLocalPath(s.source) : '';
return '<div class="nbrow" data-aisrc="' + esc(page) + '">' +
(mode ? '<span class="tag" style="flex:none">' + esc(mode) + '</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 && 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>';
}).join('') + '</div>';
}
@@ -822,9 +846,17 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
(src
? (function () {
var href = srcHref(src);
var body = href
? '<a class="mono" style="font-size:14px;color:var(--amber);word-break:break-all" href="' + esc(href) + '" target="_blank" rel="noopener">' + esc(src) + ' ↗</a>'
: '<span class="mono" style="font-size:14px;color:var(--amber);word-break:break-all">' + esc(src) + '</span>';
var localPath = srcLocalPath(src);
var body;
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>';
})()
: '');