portal: 來源回溯 gitea:// 顯示為可點超連結(PORTAL_SOURCE_WEB_BASE 可選 var)

- 新增可選 var PORTAL_SOURCE_WEB_BASE:設了之後 Portal 卡片詳頁「來源回溯」與
  AI 問答出處列把 gitea://<path>#n 渲染成 {base}/<path> 超連結(新分頁開啟);
  未設時行為一字不變(Mira 零影響)。
- 安全:href 過 esc()、路徑逐段 encodeURIComponent、rel=noopener;
  只認 gitea:// 前綴,其他 scheme 一律純文字。
- 驗證:tsc 0 錯誤;portal-auth/portal-data 測試 48/48 綠;
  渲染後 client JS 語法檢查通過。
- 來源:leo RAG demo 客戶測試回饋(2026-07-18)「來源回溯沒辦法點擊」。
- 部署(leo 閘):uncle6 重部 cypher 時帶
  --var PORTAL_SOURCE_WEB_BASE:"https://git.uncle6.me/Leo/arcrun-rag-demo-knowledge/src/branch/main"
This commit is contained in:
Claude (總管雲端)
2026-07-18 06:01:26 +00:00
parent 7bd7b4b26a
commit eaaf894b23
3 changed files with 34 additions and 3 deletions
+20 -3
View File
@@ -32,7 +32,7 @@ import { TAIPEI_CLIENT_JS } from '../lib/taipei-time';
export const portalUiRouter = new Hono<{ Bindings: Bindings }>();
function renderPortalHtml(brand: string): string {
function renderPortalHtml(brand: string, sourceWebBase = ''): string {
return `<!doctype html>
<html lang="zh-Hant">
<head>
@@ -480,6 +480,14 @@ function renderPortalHtml(brand: string): string {
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)捨棄。
var SOURCE_WEB_BASE = ${JSON.stringify(sourceWebBase)};
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('/');
}
function entryLib(e) { var m = entryMeta(e); return (typeof m.library === 'string' && m.library) ? m.library : 'general'; }
function entryTitle(e) {
if (e.page_name) return e.page_name;
@@ -704,9 +712,11 @@ function renderPortalHtml(brand: string): string {
'<div style="margin-top:7px;display:flex;flex-direction:column;gap:6px">' + srcs.map(function (s) {
var page = (s && (s.page || s.page_name)) || '';
var mode = (s && s.mode) || '';
var href = s && s.source ? srcHref(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>' : '') +
'<span style="margin-left:auto;color:rgba(var(--amber-rgb),.6);flex:none"></span></div>';
}).join('') + '</div>';
}
@@ -716,6 +726,7 @@ function renderPortalHtml(brand: string): string {
.catch(function (e) { $('ai-go').disabled = false; $('ai-out').innerHTML = '<div class="err" style="margin-top:12px">請求失敗:' + esc(friendlyErr(e)) + '</div>'; });
}
$('ai-out').addEventListener('click', function (ev) {
if (ev.target.closest('a')) return; // 「來源 ↗」超連結交給瀏覽器,不觸發帶入搜尋框
var t = ev.target.closest('[data-aisrc]');
if (!t) return;
var page = t.getAttribute('data-aisrc');
@@ -797,7 +808,13 @@ function renderPortalHtml(brand: string): string {
'<span class="muted mono">' + esc(fmtDateTime(e.created_at)) + '</span></div>' +
'<div class="md">' + mdRender(e.content || '(此條目沒有內文)') + '</div>' +
(src
? '<div class="srcline"><span style="font-size:13px;letter-spacing:.14em;color:rgba(var(--ink-rgb),.5);flex:none">來源回溯</span><span class="mono" style="font-size:14px;color:var(--amber);word-break:break-all">' + esc(src) + '</span></div>'
? (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>';
return '<div class="srcline"><span style="font-size:13px;letter-spacing:.14em;color:rgba(var(--ink-rgb),.5);flex:none">來源回溯</span>' + body + '</div>';
})()
: '');
})
.catch(function (er) { $('cd-main').innerHTML = '<div class="err">請求失敗:' + esc(friendlyErr(er)) + '</div>'; });
@@ -1213,5 +1230,5 @@ function renderPortalHtml(brand: string): string {
// 未 bootstrap 任何 portal_user 時登入必失敗(「尚未啟用」的誠實形態),不影響 console。
portalUiRouter.get('/portal', (c) => {
const brand = c.env.CONSOLE_BRAND || 'Arcrun';
return c.html(renderPortalHtml(brand));
return c.html(renderPortalHtml(brand, c.env.PORTAL_SOURCE_WEB_BASE || ''));
});
+6
View File
@@ -119,6 +119,12 @@ export type Bindings = {
PORTAL_UPLOAD_GITEA?: string;
// 能寫該 repo contents API 的 tokenwrangler secret)。**只在 server 側,永不下發前端**。
PORTAL_UPLOAD_TOKEN?: string;
// Portal 來源回溯連結 web base(可選,非機密)。設了之後 gitea:// 開頭的 source 在 Portal
// 顯示成可點超連結:gitea://<path>#<chunk> → {base}/<path>chunk 錨點捨棄——Gitea 網頁
// 無 chunk 概念)。例:"https://git.uncle6.me/Leo/arcrun-rag-demo-knowledge/src/branch/main"。
// 未設 → 純文字顯示,行為與現狀一字不變。知識庫 repo 是 private 時點了會要登入——要不要
// 設由實例自己決定(demo 知識庫是 public,適用)。
PORTAL_SOURCE_WEB_BASE?: string;
// kbdb-graph-plugin worker base URL(可選)。未設 → 用 WORKER_SUBDOMAIN 現算
// https://kbdb-graph-plugin.<subdomain>.workers.dev(該 repo wrangler.toml name 固定)。
// console 卡片詳頁「關聯視圖」經 cypher proxy 打它(kbdb-proxy.ts /kbdb/graph/neighbors/:name)。