@@ -331,6 +331,31 @@
< div id = "st-pw-status" style = "font-size:14px;min-height:1.2em" > < / div >
< / div >
< / div >
<!-- 07 - 27 leo:金鑰與下載原本只在一次性的「還差 N 步」卡片裡,按了「稍後再說」
或裝完就再也找不到。換金鑰/換電腦重新下載都需要常駐入口,故搬進設定頁。 -->
< div class = "panel" >
<!-- t87 07 - 28 leo:乾淨網址(只有 origin,不含 /portal/# 後綴),供小幫手連線用 -->
< div style = "display:flex;align-items:center;gap:8px;margin-bottom:14px;font-size:13.5px" >
< span style = "color:rgba(var(--ink-rgb),.6);white-space:nowrap" > 你的知識庫網址(小幫手連線用)< / span >
< code id = "st-origin-url" style = "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;color:var(--ink)" > < / code >
< button class = "btn3" id = "st-copy-url" style = "padding:5px 12px;font-size:13px;white-space:nowrap;flex:none" > 複製< / button >
< / div >
< div style = "font-size:17px;font-weight:600" > 同步小幫手< / div >
< div style = "margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)" > 把你電腦上的資料夾變成知識庫。換電腦或重裝時可以再下載一次。< / div >
< div style = "margin-top:12px" >
< a class = "btn3" id = "st-daemon-dl" href = "#" style = "display:inline-block;padding:11px 18px;border-radius:10px;text-decoration:none" > 下載 Mac 版< / a >
< div style = "margin-top:8px;font-size:13px;line-height:1.7;color:rgba(var(--ink-rgb),.5)" > 封測版未簽章,第一次請右鍵→打開。裝好第一次開啟時,貼上這個網址+你的帳號密碼就連上了。< / div >
< / div >
< / div >
< div class = "panel" >
< div style = "font-size:17px;font-weight:600" > AI 問答金鑰< / div >
< div style = "margin-top:4px;font-size:14px;line-height:1.65;color:rgba(var(--ink-rgb),.55)" > 用來啟用「問 AI」。到 < a href = "https://aistudio.google.com" target = "_blank" rel = "noopener" > aistudio.google.com< / a > 免費申請。金鑰只存在你自己的知識庫裡。< / div >
< div style = "margin-top:12px;display:flex;flex-direction:column;gap:10px" >
< input type = "password" id = "st-key" class = "txt" placeholder = "貼上 Google AI 金鑰(留空=不變更)" autocomplete = "off" >
< button class = "btn" id = "st-key-save" > 儲存金鑰< / button >
< div id = "st-key-status" style = "font-size:14px;min-height:1.2em" > < / div >
< / div >
< / div >
< button class = "btn3" id = "st-logout" style = "padding:14px;font-size:16px;border-radius:11px" > 登出< / button >
< / div >
< / div >
@@ -355,18 +380,15 @@
< div class = "listcol" id = "ad-users" > < div class = "muted" > 載入中…< / div > < / div >
< div class = "sechead" > 庫目錄管理< / div >
< div style = "margin-bottom:12px;padding:12px 15px;border-radius:11px;background:rgba(var(--amber-rgb),.06);border :1px dashed rgba(var(--amber-rgb),.35);font-size:13.5px;line-height:1.7;color:rgba(var(--ink-rgb),.65) " >
這裡是「庫」的登記簿——條目歸哪個庫由資料導入(ingest)時蓋章決定;未蓋章的舊資料一律視同 < b style = "color:var(--ink)" > general < / b > 。標了「圖譜來源」的庫決定誰能用圖譜模式(全都沒標=預設 general)。
< div style = "display:flex;align-items:center;gap:8px;margin-bottom :10 px;font-size:13.5px " >
< span style = "color:rgba( var(--ink-rgb),.6);white-space:nowrap" > 你的知識庫網址(小幫手連線用) < / span >
< code id = "ad-origin-url" style = "flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;font-size:13px;color:var(--ink)" > < / code >
< button class = "btn3" id = "ad-copy-url" style = "padding:5px 12px;font-size:13px;white-space:nowrap;flex:none" > 複製< / button >
< / div >
< div class = "panel " >
< div style = "font-size:16px;font-weight:600;margin-bottom:12px" > 登記新庫< / div >
< div class = "formrow" >
< input type = "text" id = "ad-nl-name" class = "txt" placeholder = "庫名(英數 _ -,例:finance) " autocomplete = "off" >
< input type = "text" id = "ad-nl-display" class = "txt" placeholder = "顯示名(例:財務庫)" autocomplete = "off" >
< input type = "text" id = "ad-nl-desc" class = "txt" placeholder = "描述(可留空)" autocomplete = "off" >
< button class = "btn" id = "ad-nl-create" style = "flex:none;padding:0 20px" > 登記< / button >
< / div >
< div id = "ad-nl-status" class = "err" style = "font-size:14px;min-height:1.2em;margin-top:8px" > < / div >
< div style = "margin-bottom:12px;padding:12px 15px;border-radius:11px;background:rgba(var(--amber-rgb),.06);border:1px dashed rgba(var(--amber-rgb),.35);font-size:13.5px;line-height:1.7;color:rgba(var(--ink-rgb),.65) " >
<!-- 07 - 27 leo:拿掉「登記新庫」——庫由「同步小幫手」看守資料夾時自動蓋章產生,
人工登記只會製造對不上的空庫。這裡只做「已存在的庫」的管理。 -->
裝好 < b style = "color:var(--ink)" > 同步小幫手 < / b > 並選好要看守的資料夾之後,每個資料夾會自動成為一個「庫」出現在下面—— < b style = "color:var(--ink)" > 不需要人工新增 < / b > 。下面還是空的,代表小幫手還沒裝好或還沒選資料夾。
< / div >
< div class = "listcol" id = "ad-libs" > < div class = "muted" > 載入中…< / div > < / div >
< / div >
@@ -420,10 +442,32 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
clearTimeout ( toast . _t ) ;
toast . _t = setTimeout ( function ( ) { el . style . display = 'none' ; } , 2000 ) ;
}
// safeJson:把回應當 JSON 解析,**解析不了就回空物件而不是拋例外**(t75 ①,2026-07-28)。
//
// 為什麼需要:原本各處都直接 r.json(),但伺服器不一定回 JSON——404 頁、Cloudflare 錯誤頁、
// 反向代理的 HTML 都是純文字。JSON.parse 一爆,錯誤就沿著 .catch 走到 friendlyErr,
// **技術訊息原文被噴到畫面上**。leo 同事實測看到的就是:
// 「Unexpected non-whitespace character after JSON at position 4」
// 真正的原因其實是 `/portal/admin/chat-key` 回 404(實例的 cypher 是舊版沒這端點),
// 但使用者只看得到一句看不懂的英文 ⇒ 無從判斷是自己打錯還是系統壞了。
function safeJson ( r ) {
return r . text ( ) . then ( function ( t ) {
if ( ! t ) return { } ;
try { return JSON . parse ( t ) ; } catch ( e ) { return { } ; }
} ) ;
}
// friendlyErr:給使用者看的訊息,**不外洩技術細節**(t75 ①)。
// 原本最後一行是 `return m` =把任何例外訊息原樣顯示,包含 JSON parse 錯誤、
// stack 片段這種對使用者毫無意義、只會嚇到人的東西。
function friendlyErr ( e ) {
var m = e && e . message ? String ( e . message ) : String ( e ) ;
if ( /failed to fetch|load failed|networkerror|network request failed/i . test ( m ) ) return '連線中斷——請檢查網路後重試' ;
return m ;
// JSON 解析類=伺服器回了非預期內容(多半是 404/代理錯誤頁),對使用者說人話。
if ( /json|unexpected token|unexpected non-whitespace/i . test ( m ) ) return '伺服器回應異常,請稍後再試一次(若持續發生請回報)' ;
// 其餘:只在含中文(=我們自己寫的訊息)時原樣顯示;純英文技術訊息一律收斂。
if ( /[\u4e00-\u9fff]/ . test ( m ) ) return m ;
return '操作失敗,請稍後再試一次' ;
}
function authHeaders ( ) { return S . token ? { 'Authorization' : 'Bearer ' + S . token } : { } ; }
@@ -576,7 +620,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
// t49:全新實例(還沒有任何帳號)→ 換顯示「首次設定」,不讓用戶對著登入殼發呆。
// 探測走 console/auth-status( configured 布林,console 首設同一顆;失敗就保持登入殼=誠實降級)。
fetch ( API _BASE + '/console/auth-status' )
. then ( function ( r ) { return r . json ( ) ; } )
. then ( function ( r ) { return safeJson ( r ) ; } )
. then ( function ( d ) {
if ( d && d . configured === false ) {
$ ( 'v-login' ) . classList . remove ( 'on' ) ;
@@ -620,7 +664,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
function boot ( ) {
if ( ! S . token ) { showAuth ( ) ; return ; }
fetch ( API _BASE + '/portal/session' , { headers : authHeaders ( ) } )
. then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , d : d } ; } ) ; } )
. then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
if ( ! x . ok ) { dropSession ( ) ; return ; }
S . profile = x . d ;
@@ -645,7 +689,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
headers : { 'Content-Type' : 'application/json' } ,
body : JSON . stringify ( { email : email , password : password } )
} )
. then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , d : d } ; } ) ; } )
. then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
$ ( 'login-submit' ) . disabled = false ;
if ( ! x . ok ) { $ ( 'login-status' ) . textContent = x . d . error || '登入失敗' ; return ; }
@@ -656,11 +700,128 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
} )
. catch ( function ( e ) { $ ( 'login-submit' ) . disabled = false ; $ ( 'login-status' ) . textContent = friendlyErr ( e ) ; } ) ;
}
// t72 OS 分流(2026-07-27, leo:「客戶是用 windows 的」)——在此之前四處寫死「下載 Mac 版」,
// Windows 客戶按下去會拿到 Mac 的 .app=按了連到錯的東西(leo 判準:那不算友善)。
// 判不出 OS 時**兩個都給**,不替用戶猜;且無論判成哪個,頁面都留「不是這個系統?」的另一版連結
// ——UA 會判錯,判錯時用戶要有路可走(施工圖 §3 注意事項 1、3)。
var DAEMON _BASE _DEFAULT = 'https://raw.githubusercontent.com/youlinhsieh/arcrun-rag-bundles/main/daemon/' ;
var DAEMON _MAC = 'ArcrunRAG-mac-unsigned.zip' ;
var DAEMON _WIN = 'ArcrunRAG-win-unsigned.zip' ;
// Mac 那顆 21MB > jsDelivr 單檔 20MB 上限(實測回 "File size exceeded...")→ 一律走 raw;
// Windows 13MB 雖在限內,同走 raw 保持單一來源、少一個會壞的地方。
function daemonBase ( ) {
var cfg = ( window . ARCRUN _CONFIG || { } ) ;
if ( cfg . daemonBase ) return String ( cfg . daemonBase ) . replace ( /\/?$/ , '/' ) ;
// 舊 key 相容(施工圖 §3 注意事項 2):daemonDownload 是「單一檔案網址」,
// 有設就尊重它當 Mac 版網址,並由它推出目錄給 Windows 版用。
if ( cfg . daemonDownload ) {
var s = String ( cfg . daemonDownload ) ;
return s . slice ( 0 , s . lastIndexOf ( '/' ) + 1 ) || DAEMON _BASE _DEFAULT ;
}
return DAEMON _BASE _DEFAULT ;
}
function daemonPick ( ) {
var base = daemonBase ( ) ;
var ua = navigator . userAgent || '' ;
// 手機/平板先排除:iOS 的 UA 含 "Mac OS X",不排除會把 iPhone 判成 Mac,
// 讓手機用戶下載一個裝不起來的桌面 app(測到才發現,2026-07-27)。
// 手機=判不出桌面 OS → 落到「兩個都給」,用戶回桌機再選。
var isMobile = /iPhone|iPad|iPod|Android|Mobile/i . test ( ua ) ;
var isWin = ! isMobile && /Windows NT/i . test ( ua ) ;
var isMac = ! isMobile && /Macintosh|Mac OS X/i . test ( ua ) && ! /Windows/i . test ( ua ) ;
var mac = { os : 'mac' , label : '下載 Mac 版' , url : base + DAEMON _MAC } ;
var win = { os : 'win' , label : '下載 Windows 版' , url : base + DAEMON _WIN } ;
if ( isWin ) return { pick : win , other : mac , sure : true } ;
if ( isMac ) return { pick : mac , other : win , sure : true } ;
return { pick : null , other : null , sure : false , mac : mac , win : win } ;
}
// 封測期第一次開啟的擋關提示(未簽章)——Mac/Windows 攔法不同,話術也不同。
function daemonHint ( os ) {
if ( os === 'win' ) return '(封測版未簽章,Windows 第一次會跳藍色視窗擋下來——點「更多資訊」→「仍要執行」就好)' ;
return '(封測版未簽章,第一次請右鍵→打開)' ;
}
// 07-27:設定頁的常駐入口(下載小幫手/換 AI 金鑰)——與一次性卡片同一組 API
( function ( ) {
var dl = $ ( 'st-daemon-dl' ) ;
if ( dl ) {
var d = daemonPick ( ) ;
if ( d . sure ) {
dl . setAttribute ( 'href' , d . pick . url ) ;
dl . textContent = d . pick . label ;
// 判對了也要留另一版的路(UA 會判錯)
var alt = document . createElement ( 'span' ) ;
alt . className = 'muted' ;
alt . style . cssText = 'font-size:12.5px;margin-left:8px' ;
alt . innerHTML = '不是這個系統?<a href="' + d . other . url + '">' + d . other . label + '</a>' ;
if ( dl . parentNode ) dl . parentNode . insertBefore ( alt , dl . nextSibling ) ;
} else {
// 判不出來=兩個都給,不預設 Mac
dl . setAttribute ( 'href' , d . mac . url ) ;
dl . textContent = d . mac . label ;
var both = document . createElement ( 'span' ) ;
both . className = 'muted' ;
both . style . cssText = 'font-size:12.5px;margin-left:8px' ;
both . innerHTML = '或 <a href="' + d . win . url + '">' + d . win . label + '</a>' ;
if ( dl . parentNode ) dl . parentNode . insertBefore ( both , dl . nextSibling ) ;
}
}
var kb = $ ( 'st-key-save' ) ;
if ( kb ) kb . addEventListener ( 'click' , function ( ) {
var k = ( $ ( 'st-key' ) . value || '' ) . trim ( ) ;
var m = $ ( 'st-key-status' ) ;
if ( ! k ) { m . textContent = '請先貼上金鑰' ; m . style . color = '' ; return ; }
kb . disabled = true ; m . textContent = '儲存中…' ; m . style . color = '' ;
fetch ( API _BASE + '/portal/admin/chat-key' , {
method : 'POST' ,
headers : Object . assign ( { 'Content-Type' : 'application/json' } , authHeaders ( ) ) ,
body : JSON . stringify ( { key : k } )
} ) . then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
kb . disabled = false ;
if ( guard401 ( x . status ) ) return ;
// t75 ①:404=這個實例的伺服器版本還沒有這個功能(不是使用者做錯)。
// 講清楚「為什麼」與「怎麼辦」,否則他只看到「儲存失敗」會反覆重試同一件事。
if ( x . status === 404 ) {
m . textContent = '你的知識庫版本還沒有這個功能,請先更新知識庫再回來設定金鑰' ;
m . style . color = '#b4462f' ; return ;
}
if ( ! x . ok ) { m . textContent = ( x . d && x . d . error ) || '儲存失敗' ; m . style . color = '#b4462f' ; return ; }
$ ( 'st-key' ) . value = '' ;
m . textContent = '已儲存,AI 問答可以用了' ; m . style . color = '#3f7a4f' ;
} )
. catch ( function ( e ) { kb . disabled = false ; m . textContent = friendlyErr ( e ) ; m . style . color = '#b4462f' ; } ) ;
} ) ;
} ) ( ) ;
$ ( 'st-logout' ) . addEventListener ( 'click' , function ( ) {
fetch ( API _BASE + '/portal/logout' , { method : 'POST' , headers : authHeaders ( ) } ) . catch ( function ( ) { /* 盡力而為 */ } ) ;
dropSession ( ) ;
} ) ;
// t87 07-28 leo:知識庫網址 helper(只有 origin,不含 /portal/# 後綴),兩處 UI 共用
function copyOriginUrl ( btn ) {
var url = location . origin ;
var orig = btn . textContent ;
if ( ! navigator . clipboard || ! navigator . clipboard . writeText ) {
alert ( '請手動選取並複製:' + url ) ;
return ;
}
navigator . clipboard . writeText ( url ) . then ( function ( ) {
btn . textContent = '已複製' ;
setTimeout ( function ( ) { btn . textContent = orig ; } , 1500 ) ;
} ) . catch ( function ( ) {
alert ( '請手動選取並複製:' + url ) ;
} ) ;
}
( function ( ) {
[ 'st-origin-url' , 'ad-origin-url' ] . forEach ( function ( id ) {
var el = $ ( id ) ; if ( el ) el . textContent = location . origin ;
} ) ;
[ 'st-copy-url' , 'ad-copy-url' ] . forEach ( function ( id ) {
var el = $ ( id ) ; if ( el ) el . addEventListener ( 'click' , function ( ) { copyOriginUrl ( this ) ; } ) ;
} ) ;
} ) ( ) ;
// ── t53 完成安裝清單(進站必見,三件做完才消失)─────────────────────────────
function setupSteps ( ) {
try { return JSON . parse ( localStorage . getItem ( 'arcrun_setup_steps' ) || '{}' ) ; } catch ( e ) { return { } ; }
@@ -678,7 +839,8 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
var s = setupSteps ( ) ;
if ( s . daemon && s . key ) return ; // t54 起只剩兩件:設定改由小幫手輸入帳密自取,不再下載檔案
var cfg = window . ARCRUN _CONFIG || { } ;
var daemonUrl = cfg . daemonDownload || 'https://raw.githubusercontent.com/youlinhsieh/arcrun-rag-bundles/main/daemon/ArcrunRAG-mac-unsigned.zip' ;
var dpick = daemonPick ( ) ; // t72 OS 分流(同一組判定,見上面 daemonPick)
var daemonUrl = dpick . sure ? dpick . pick . url : dpick . mac . url ;
var row = function ( done , id , html ) {
return '<div style="display:flex;gap:10px;align-items:baseline;margin-top:10px">'
+ '<span>' + ( done ? '✅' : '⬜' ) + '</span><div style="flex:1">' + html + '</div></div>' ;
@@ -690,11 +852,20 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
+ row ( s . daemon , 'daemon' ,
'<b>下載同步小幫手</b>(把資料夾變成知識庫)<br>'
+ ( daemonUrl
? '<a href="' + esc ( daemonUrl ) + '" id="sc-daemon">下載 Mac 版</a>'
+ '<span class="muted" style="font-size:12.5px">(封測版未簽章,第一次請右鍵→打開)</span>'
// t72:按鈕字樣+擋關話術都跟著 OS 走;並且一律附「另一個系統」的連結
//(UA 判錯時用戶要有路走;判不出來時=兩個平等並列,不預設 Mac)
? '<a href="' + esc ( daemonUrl ) + '" id="sc-daemon">'
+ esc ( dpick . sure ? dpick . pick . label : dpick . mac . label ) + '</a>'
+ '<span class="muted" style="font-size:12.5px">'
+ esc ( daemonHint ( dpick . sure ? dpick . pick . os : 'mac' ) ) + '</span>'
+ '<div class="muted" style="font-size:12.5px">'
+ ( dpick . sure
? '不是這個系統?<a href="' + esc ( dpick . other . url ) + '">' + esc ( dpick . other . label ) + '</a>'
: '或 <a href="' + esc ( dpick . win . url ) + '">' + esc ( dpick . win . label ) + '</a>' )
+ '</div>'
// 誠實降級(leo 判準:按了連到錯的機制=不友善):沒有可用下載點時
// 不放死連結,說明現況並讓這步可手動打勾,不卡住清單。
: '<span class="muted" style="font-size:13px">封測期由我們直接寄給你安裝檔( Mac 版); '
: '<span class="muted" style="font-size:13px">封測期由我們直接寄給你安裝檔;'
+ '收到後回來按 <a href="#" id="sc-daemon">我已經裝好了</a>。</span>' )
// t54(leo:「最好的就是把它的帳密直接輸入」):設定不再是一個要下載的檔案——
// 小幫手第一次開啟會問網址+帳密,自己去換設定。
@@ -719,10 +890,12 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
method : 'POST' ,
headers : Object . assign ( { 'Content-Type' : 'application/json' } , authHeaders ( ) ) ,
body : JSON . stringify ( { key : k } )
} ) . then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , d : d } ; } ) ; } )
} ) . then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
kb . disabled = false ;
if ( ! x . ok || ! x . d . success ) { m . textContent = x . d . error || '啟用失敗,請再試一次' ; return ; }
// t75 ①:同設定頁——404=實例版本太舊,不是使用者的錯。
if ( x . status === 404 ) { m . textContent = '你的知識庫版本還沒有這個功能,請先更新知識庫' ; return ; }
if ( ! x . ok || ! x . d . success ) { m . textContent = ( x . d && x . d . error ) || '啟用失敗,請再試一次' ; return ; }
markStep ( 'key' ) ;
} )
. catch ( function ( ) { kb . disabled = false ; m . textContent = '網路好像有問題,請再試一次' ; } ) ;
@@ -825,7 +998,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
$ ( 'se-results' ) . innerHTML = '' ;
var url = API _BASE + '/portal/data/search?q=' + encodeURIComponent ( q ) + '&mode=' + ( S . mode === 'semantic' ? 'semantic' : 'keyword' ) ;
fetch ( url , { headers : authHeaders ( ) } )
. then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
if ( guard401 ( x . status ) ) return ;
if ( ! x . ok ) { $ ( 'se-count' ) . innerHTML = '<span class="err">' + esc ( x . d . error || ( '查詢失敗(HTTP ' + x . status + ') ' ) ) + '</span>' ; return ; }
@@ -869,7 +1042,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
$ ( 'ai-go' ) . disabled = true ;
$ ( 'ai-out' ) . innerHTML = '<div class="muted" style="margin-top:12px">AI 查閱知識庫中…</div>' ;
fetch ( API _BASE + '/portal/data/chat?question=' + encodeURIComponent ( q ) , { headers : authHeaders ( ) } )
. then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
$ ( 'ai-go' ) . disabled = false ;
if ( guard401 ( x . status ) ) return ;
@@ -918,7 +1091,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
$ ( 'se-count' ) . textContent = '查詢關聯中…' ;
renderGraphEmpty ( '查詢關聯中…' ) ;
fetch ( API _BASE + '/portal/data/graph/neighbors/' + encodeURIComponent ( name ) , { headers : authHeaders ( ) } )
. then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
if ( guard401 ( x . status ) ) return ;
if ( ! x . ok ) { $ ( 'se-count' ) . innerHTML = '<span class="err">' + esc ( x . d . error || ( '關聯查詢失敗(HTTP ' + x . status + ') ' ) ) + '</span>' ; renderGraphEmpty ( x . d . error || '關聯查詢失敗' ) ; return ; }
@@ -981,7 +1154,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
if ( ! S . cardId ) { $ ( 'cd-main' ) . innerHTML = '<div class="muted">沒有指定卡片——從「搜尋」點一張進來。</div>' ; return ; }
$ ( 'cd-main' ) . innerHTML = '<div class="muted">載入中…</div>' ;
fetch ( API _BASE + '/portal/data/entries/' + encodeURIComponent ( S . cardId ) , { headers : authHeaders ( ) } )
. then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
if ( guard401 ( x . status ) ) return ;
if ( ! x . ok || ! x . d . entry ) { $ ( 'cd-main' ) . innerHTML = '<div class="err">' + esc ( x . d . error || '讀不到這張卡片' ) + '</div>' ; return ; }
@@ -1023,7 +1196,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
? ': <a href="' + esc ( SOURCE _WEB _BASE + '/system-dev/wiki/00-MAP.md' ) + '" target="_blank" rel="noopener" style="color:var(--amber)">00-MAP.md ↗</a>'
: '存在知識庫的 00-MAP.md' ;
fetch ( API _BASE + '/portal/data/graph/overview' , { headers : authHeaders ( ) } )
. then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
if ( guard401 ( x . status ) ) return ;
if ( ! x . ok ) { $ ( 'map-box' ) . innerHTML = '<div class="err">' + esc ( x . d . error || ( '總圖載入失敗(HTTP ' + x . status + ') ' ) ) + '</div>' ; return ; }
@@ -1129,7 +1302,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
function loadWorkflows ( ) {
$ ( 'wf-list' ) . innerHTML = '<div class="muted">載入中…</div>' ;
fetch ( API _BASE + '/portal/data/workflows' , { headers : authHeaders ( ) } )
. then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
if ( guard401 ( x . status ) ) return ;
if ( ! x . ok ) { $ ( 'wf-list' ) . innerHTML = '<div class="err">' + esc ( x . d . error || ( '讀取失敗(HTTP ' + x . status + ') ' ) ) + '</div>' ; return ; }
@@ -1193,7 +1366,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
headers : Object . assign ( { 'Content-Type' : 'application/json' } , authHeaders ( ) ) ,
body : JSON . stringify ( { filename : file . name , content _b64 : b64FromDataUrl ( reader . result ) } )
} )
. then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
if ( guard401 ( x . status ) ) return ;
if ( ! x . ok ) { setRowStatus ( row , false , '失敗' , x . d . error || ( 'HTTP ' + x . status ) ) ; return ; }
@@ -1228,7 +1401,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
var opt = { method : method , headers : Object . assign ( { 'Content-Type' : 'application/json' } , authHeaders ( ) ) } ;
if ( body !== undefined ) opt . body = JSON . stringify ( body ) ;
return fetch ( path , opt ) . then ( function ( r ) {
return r . json ( ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ;
return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ;
} ) ;
}
@@ -1315,23 +1488,18 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
'<span class="dim mono" style="font-size:13px">' + esc ( l . name ) + '</span>' +
'<span class="tag green">啟用中</span><span class="tag">同步自動出現</span></div>' +
'<div style="margin-top:8px;font-size:14.5px;line-height:1.65;color:rgba(var(--ink-rgb),.65)">'
+ '這個庫來自同步小幫手的資料夾,已經可以搜尋與設權限。登記到目錄後可以改顯示名、設為圖譜來源 。</div>' +
+ '這個庫來自同步小幫手的資料夾,已經可以搜尋與設權限。登記到目錄後可以改顯示名。</div>' +
'<div class="btnrow"><button class="btn2" data-act="lib-adopt" data-name="' + esc ( l . name ) + '">登記到目錄</button></div>' +
'</div>' ;
}
return '<div class="card-item"' + ( disabled ? ' style="opacity:.6"' : '' ) + ' >' +
return '<div class="card-item">' +
'<div style="display:flex;align-items:baseline;gap:10px;flex-wrap:wrap">' +
'<span class="serif" style="font-size:17px;font-weight:600">' + esc ( l . display _name || l . name ) + '</span>' +
'<span class="dim mono" style="font-size:13px">' + esc ( l . name ) + '</span>' +
'<span class="tag ' + ( disabled ? 'dim' : 'green' ) + '">' + ( disabled ? '已停用' : '啟用中' ) + '</span>' +
( l . graph _source ? '<span class="tag">圖譜來源</span>' : '' ) + '</div>' +
'</div>' +
( l . description ? '<div style="margin-top:8px;font-size:14.5px;line-height:1.65;color:rgba(var(--ink-rgb),.65)">' + esc ( l . description ) + '</div>' : '' ) +
'<div class="btnrow">' +
'<button class="btn2" data-act="lib-graph" data-lid="' + esc ( l . record _id ) + '" data-next="' + ( l . graph _source ? 'false' : 'true' ) + '">' + ( l . graph _source ? '取消圖譜來源' : '標為圖譜來源' ) + '</button>' +
( disabled
? '<button class="btn2" data-act="lib-status" data-next="active" data-lid="' + esc ( l . record _id ) + '">啟用</button>'
: '<button class="btn2 warn" data-act="lib-status" data-next="disabled" data-lid="' + esc ( l . record _id ) + '" data-name="' + esc ( l . display _name || l . name ) + '">停用</button>' ) +
'</div></div>' ;
'</ div>' ;
} ) . join ( '' ) ;
}
@@ -1369,28 +1537,6 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
. catch ( function ( e ) { $ ( 'ad-nu-create' ) . disabled = false ; st . textContent = friendlyErr ( e ) ; } ) ;
} ) ;
// 登記新庫
$ ( 'ad-nl-create' ) . addEventListener ( 'click' , function ( ) {
var name = $ ( 'ad-nl-name' ) . value . trim ( ) ;
var st = $ ( 'ad-nl-status' ) ;
if ( ! name ) { st . textContent = '請填庫名(英數 _ -) ' ; return ; }
st . textContent = '' ;
$ ( 'ad-nl-create' ) . disabled = true ;
adminApi ( 'POST' , '/portal/admin/libraries' , {
name : name ,
display _name : $ ( 'ad-nl-display' ) . value . trim ( ) ,
description : $ ( 'ad-nl-desc' ) . value . trim ( )
} )
. then ( function ( x ) {
$ ( 'ad-nl-create' ) . disabled = false ;
if ( guard401 ( x . status ) ) return ;
if ( ! x . ok ) { st . textContent = x . d . error || ( '登記失敗(HTTP ' + x . status + ') ' ) ; return ; }
$ ( 'ad-nl-name' ) . value = '' ; $ ( 'ad-nl-display' ) . value = '' ; $ ( 'ad-nl-desc' ) . value = '' ;
toast ( '庫已登記' ) ;
loadAdmin ( ) ;
} )
. catch ( function ( e ) { $ ( 'ad-nl-create' ) . disabled = false ; st . textContent = friendlyErr ( e ) ; } ) ;
} ) ;
// 「全部知識庫」勾選 → 其餘庫勾選框連動停用(存的就是 ["*"],個別勾選無意義)
document . addEventListener ( 'change' , function ( ev ) {
@@ -1468,7 +1614,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
. catch ( function ( e ) { t . disabled = false ; toast ( friendlyErr ( e ) ) ; } ) ;
return ;
}
// t52:把「自動出現的庫」正式登記進目錄(之後就能改顯示名/設圖譜來源 )
// t52:把「自動出現的庫」正式登記進目錄(之後就能改顯示名)
if ( act === 'lib-adopt' ) {
var lname = t . getAttribute ( 'data-name' ) || '' ;
if ( ! lname ) return ;
@@ -1484,28 +1630,6 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
. catch ( function ( e ) { t . disabled = false ; toast ( friendlyErr ( e ) ) ; } ) ;
return ;
}
if ( act === 'lib-status' || act === 'lib-graph' ) {
var lid = t . getAttribute ( 'data-lid' ) ;
var body2 = { } ;
if ( act === 'lib-status' ) {
var nextS = t . getAttribute ( 'data-next' ) ;
if ( nextS === 'disabled' && ! confirm ( '確定停用庫「' + ( t . getAttribute ( 'data-name' ) || '' ) + '」?停用後不再出現在庫勾選清單(既有授權不會被自動改)。' ) ) return ;
body2 . status = nextS ;
} else {
body2 . graph _source = t . getAttribute ( 'data-next' ) === 'true' ;
}
t . disabled = true ;
adminApi ( 'PATCH' , '/portal/admin/libraries/' + encodeURIComponent ( lid ) , body2 )
. then ( function ( x ) {
t . disabled = false ;
if ( guard401 ( x . status ) ) return ;
if ( ! x . ok ) { toast ( x . d . error || ( '更新失敗(HTTP ' + x . status + ') ' ) ) ; return ; }
toast ( '庫已更新' ) ;
loadAdmin ( ) ;
} )
. catch ( function ( e ) { t . disabled = false ; toast ( friendlyErr ( e ) ) ; } ) ;
return ;
}
} ) ;
// 一次性密碼關閉鈕(otpbox 動態生成,掛在容器上)
$ ( 'ad-otp' ) . addEventListener ( 'click' , function ( ev ) {
@@ -1541,7 +1665,7 @@ function taipeiMonthDay(ms) { var d = new Date(ms + TAIPEI_OFFSET_MS); return {
headers : Object . assign ( { 'Content-Type' : 'application/json' } , authHeaders ( ) ) ,
body : JSON . stringify ( { current : oldPw , 'new' : newPw } )
} )
. then ( function ( r ) { return r . json ( ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( r ) { return safeJson ( r ) . then ( function ( d ) { return { ok : r . ok , status : r . status , d : d } ; } ) ; } )
. then ( function ( x ) {
$ ( 'st-pw-save' ) . disabled = false ;
if ( guard401 ( x . status ) ) return ;