@@ -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');
@@ -738,11 +749,23 @@ function renderPortalHtml(brand: string): string {
if (!x.ok) { $ ('se-count').innerHTML = '<span class="err">' + esc(x.d.error || ('關聯查詢失敗(HTTP ' + x.status + ') ')) + '</span>'; 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 '<div class="nbrow" data-gnode="' + esc(other) + '">' +
'<span class="tag green" style="flex:none">' + esc(t.predicate || '關聯') + '</span>' +
'<span style="word-break:break-word">' + esc(other) + '</span>' +
@@ -797,7 +820,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 +1242,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 || '' ) ) ;
} ) ;