diff --git a/console-ui/public/portal/index.html b/console-ui/public/portal/index.html
index 438cdb7..bd7ba8f 100644
--- a/console-ui/public/portal/index.html
+++ b/console-ui/public/portal/index.html
@@ -605,10 +605,34 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
// 來源回溯超連結:PORTAL_SOURCE_WEB_BASE 有設且 source 是 gitea:// 才給 href;
// 其餘回空字串=維持純文字(行為與未設時一字不變)。chunk 錨點(#n)捨棄。
var SOURCE_WEB_BASE = "";
+ function encSrcPath(p) { return p.split('/').map(encodeURIComponent).join('/'); }
+ // 🔴 t21(移植自已刪分支 fix/portal-source-scheme-t21,2026-08-05 分支整理救回):
+ // 新 ingest 鏈(rag_ingest_card/rag_ingest_direct)寫的 metadata.source 是 **kb://**,
+ // 但這裡原本只認 gitea:// ⇒ 收卡上雲後溯源連結一律失效(B4 部好溯源後一經重灌即再斷鏈)。
+ // 三種 scheme 各自的正解:
+ // gitea:// (舊 rag_ingest v2)→ {base}/,行為一字不變
+ // gitea:@ (km-wiki-ingest 實形)→ {base}//src/branch/main/
+ // kb:// (新 ingest 鏈實形)→ **雲端沒有對應網頁,回空字串=維持純文字,不造死鏈**
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) {
+ return base + '/' + encSrcPath(src.slice(8).replace(/#\d+$/, ''));
+ }
+ 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) {