From eaaf894b238ddef437d478c66d4aa5dd3fed0882 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claude=20=28=E7=B8=BD=E7=AE=A1=E9=9B=B2=E7=AB=AF=29?= Date: Sat, 18 Jul 2026 06:01:26 +0000 Subject: [PATCH 1/2] =?UTF-8?q?portal:=20=E4=BE=86=E6=BA=90=E5=9B=9E?= =?UTF-8?q?=E6=BA=AF=20gitea://=20=E9=A1=AF=E7=A4=BA=E7=82=BA=E5=8F=AF?= =?UTF-8?q?=E9=BB=9E=E8=B6=85=E9=80=A3=E7=B5=90=EF=BC=88PORTAL=5FSOURCE=5F?= =?UTF-8?q?WEB=5FBASE=20=E5=8F=AF=E9=81=B8=20var=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增可選 var PORTAL_SOURCE_WEB_BASE:設了之後 Portal 卡片詳頁「來源回溯」與 AI 問答出處列把 gitea://#n 渲染成 {base}/ 超連結(新分頁開啟); 未設時行為一字不變(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" --- cypher-executor/src/routes/portal-ui.ts | 23 +++++++++++++++++--- cypher-executor/src/types.ts | 6 +++++ system-dev/docs/3-specs/portal-auth/tasks.md | 8 +++++++ 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/cypher-executor/src/routes/portal-ui.ts b/cypher-executor/src/routes/portal-ui.ts index c9e5395..99c79b9 100644 --- a/cypher-executor/src/routes/portal-ui.ts +++ b/cypher-executor/src/routes/portal-ui.ts @@ -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 ` @@ -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 { '
' + 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 '
' + (mode ? '' + esc(mode) + '' : '') + '' + esc(page || '(無頁名)') + '' + + (href ? '來源 ↗' : '') + '
'; }).join('') + '
'; } @@ -716,6 +726,7 @@ function renderPortalHtml(brand: string): string { .catch(function (e) { $('ai-go').disabled = false; $('ai-out').innerHTML = '
請求失敗:' + esc(friendlyErr(e)) + '
'; }); } $('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 { '' + esc(fmtDateTime(e.created_at)) + '' + '
' + mdRender(e.content || '(此條目沒有內文)') + '
' + (src - ? '
來源回溯' + esc(src) + '
' + ? (function () { + var href = srcHref(src); + var body = href + ? '' + esc(src) + ' ↗' + : '' + esc(src) + ''; + return '
來源回溯' + body + '
'; + })() : ''); }) .catch(function (er) { $('cd-main').innerHTML = '
請求失敗:' + esc(friendlyErr(er)) + '
'; }); @@ -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 || '')); }); diff --git a/cypher-executor/src/types.ts b/cypher-executor/src/types.ts index e38fadd..6c9ca60 100644 --- a/cypher-executor/src/types.ts +++ b/cypher-executor/src/types.ts @@ -119,6 +119,12 @@ export type Bindings = { PORTAL_UPLOAD_GITEA?: string; // 能寫該 repo contents API 的 token(wrangler secret)。**只在 server 側,永不下發前端**。 PORTAL_UPLOAD_TOKEN?: string; + // Portal 來源回溯連結 web base(可選,非機密)。設了之後 gitea:// 開頭的 source 在 Portal + // 顯示成可點超連結:gitea://# → {base}/(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..workers.dev(該 repo wrangler.toml name 固定)。 // console 卡片詳頁「關聯視圖」經 cypher proxy 打它(kbdb-proxy.ts /kbdb/graph/neighbors/:name)。 diff --git a/system-dev/docs/3-specs/portal-auth/tasks.md b/system-dev/docs/3-specs/portal-auth/tasks.md index 8167f5b..ef9b000 100644 --- a/system-dev/docs/3-specs/portal-auth/tasks.md +++ b/system-dev/docs/3-specs/portal-auth/tasks.md @@ -143,6 +143,14 @@ `global_fetch_strictly_public` flag(self-hosted 同帳號 fetch 1042 正解,Arcrun#33 同族, 官方行為不變原理同 cypher 前例 rules/03 1042 段)。 +- [x] **Portal 來源回溯超連結(2026-07-18,任務層小改,分支 `feat/portal-source-link`; + 來源=leo RAG demo 客戶測試回饋「來源回溯 gitea:// 沒辦法點」)**: + 新增可選 var `PORTAL_SOURCE_WEB_BASE`(types.ts 註解含語意);portal-ui 卡片詳頁「來源回溯」 + 與 AI 問答出處列在「var 有設+source 為 gitea://」時渲染 `` + (gitea://#n → {base}/,chunk 錨點捨棄、路徑逐段 encodeURIComponent、href 過 + esc());未設 var 行為一字不變(Mira 零影響)。驗證:tsc 0、portal-auth/portal-data 測試 + 48/48 綠、渲染後 client JS new Function 語法檢查過。部署:uncle6 重部+設 var(leo 閘)。 + ## 第二波(不在本 SDD 動工範圍,掛號) - MCP token 綁庫集合(design §9;PR#15 擴充,只動 `mcp/`) -- 2.52.0 From 6ff1355fe8ed59f380ff267f62127f8ba7fdc60a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Claude=20=28=E7=B8=BD=E7=AE=A1=E9=9B=B2=E7=AB=AF=29?= Date: Sat, 18 Jul 2026 06:13:45 +0000 Subject: [PATCH 2/2] =?UTF-8?q?portal:=20=E5=9C=96=E8=AD=9C=E9=84=B0?= =?UTF-8?q?=E5=B1=85=E6=B8=B2=E6=9F=93=E4=BF=AE=20[object=20Object]?= =?UTF-8?q?=EF=BC=8B=E9=97=9C=E8=81=AF=E5=88=97=E5=BE=9E=20workflow=20neig?= =?UTF-8?q?hbors=20=E5=B0=8E=E5=87=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - workflow 版 graph_neighbors 的 neighbors 是 {node,predicate,from,depth} 物件, renderGraph 期望字串 → 節點 chips 全變 [object Object];正規化成名字再渲染。 - workflow 版 edges 恆空 → 關聯永遠 0;neighbor 物件本身就是一條邊,導出成 {subject,predicate,object} 餵關聯列(plugin 版行為不變)。 - 邊列 subject 比對改大小寫不敏感(graph 查詢端已正規化,搜 rag 命中節點 RAG)。 - 來源:leo RAG demo 客戶測試回饋問題 3(鄰居/關聯顯示有誤)。 --- cypher-executor/src/routes/portal-ui.ts | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/cypher-executor/src/routes/portal-ui.ts b/cypher-executor/src/routes/portal-ui.ts index 99c79b9..99b2aa6 100644 --- a/cypher-executor/src/routes/portal-ui.ts +++ b/cypher-executor/src/routes/portal-ui.ts @@ -749,11 +749,23 @@ function renderPortalHtml(brand: string, sourceWebBase = ''): string { if (!x.ok) { $('se-count').innerHTML = '' + esc(x.d.error || ('關聯查詢失敗(HTTP ' + x.status + ')')) + ''; renderGraphEmpty(x.d.error || '關聯查詢失敗'); return; } var neighbors = x.d.neighbors || []; var edges = x.d.edges || []; - $('se-count').textContent = '節點「' + name + '」・鄰居 ' + neighbors.length + '・關聯 ' + edges.length; - if (!neighbors.length) { renderGraphEmpty('尚無關聯資料——這個名字還沒有連進知識圖譜。'); return; } - renderGraph(name, neighbors.slice(0, 10)); + // 形狀相容(Arcrun#57 家族):plugin 版 neighbors 是字串、workflow 版是 + // { node, predicate, from, depth } 物件——不正規化會渲染成 [object Object]。 + var names = neighbors.map(function (nb) { + if (typeof nb === 'string') return nb; + return (nb && (nb.node || nb.name)) || ''; + }).filter(Boolean); + // workflow 版 edges 恆空,但 neighbor 物件本身就是一條邊(from --predicate--> node)→ 導出。 + if (!edges.length) { + edges = neighbors.filter(function (nb) { return nb && typeof nb === 'object' && nb.predicate; }) + .map(function (nb) { return { subject: nb.from || name, predicate: nb.predicate, object: nb.node }; }); + } + $('se-count').textContent = '節點「' + name + '」・鄰居 ' + names.length + '・關聯 ' + edges.length; + if (!names.length) { renderGraphEmpty('尚無關聯資料——這個名字還沒有連進知識圖譜。'); return; } + renderGraph(name, names.slice(0, 10)); $('gr-edges').innerHTML = edges.slice(0, 12).map(function (t) { - var other = t.subject === name ? t.object : t.subject; + // 大小寫不敏感比對(graph 查詢已正規化:搜 rag 會命中節點 RAG) + var other = String(t.subject).toLowerCase() === String(name).toLowerCase() ? t.object : t.subject; return '
' + '' + esc(t.predicate || '關聯') + '' + '' + esc(other) + '' + -- 2.52.0