sync: collector/ 同步自 inkstone/arcrun-rag@d129f8b(桌面小幫手 0.18.48)

This commit is contained in:
2026-08-28 09:16:44 +08:00
parent e8800d18ef
commit 3cc8788e9d
14 changed files with 728 additions and 95 deletions
+112 -65
View File
@@ -166,25 +166,39 @@ function cardKbVersions(s) {
</div>`;
}
// kbVersionLine:單一知識庫的版本判定文案,首頁卡與各庫頁共用同一份翻譯,
// 不讓兩處各寫各的文字(那樣才真的會出現「兩處講不同的話」)。
// kbVersionLine:單一知識庫的版本,首頁卡與各庫頁共用同一份(不讓兩處各寫各的)。
//
// 三種情況都要照實講(cloud_version.go 記過的坑:把「查不到」呈現成「一切正常」):
// ① Known 且落後 → 紅字+按鈕,按下去帶 email 開 install 頁(同 portal 版本卡的預填做法)
// ② Known 且已最新 → 淡字「已是最新版」
// ③ 不 Known → 照原因分兩句:連不上這個知識庫/查得到目前版本但暫時查不到最新版
// 🔴 `inkstone/arcrun-rag#159`leo 2026-08-28):「**GUI 是國際通用,圖形指示,
// 不限語文**,所以任何出現文字都要謹慎,文字表示『我的設計不夠好所以要靠文字來補』」
// ⇒ 這一行以前是三句話(「有新版可更新(目前 x → 最新 y)」「已是最新版x
// 「目前連不上這個知識庫,查不到版本」)。現在只剩**版本號與圖示**:
//
// 1.4.60 ✅ 已是最新
// 1.4.46 → 1.4.58 ⬆️ 有新版(按鈕,按下去帶 email 開安裝頁)
// ○ 從來沒查到過這台的版本
//
// 說明都收進 title(滑過去才出現),版面上一個字都不佔。
//
// 🔴 「這一輪連不上」**不再抹掉版本號**(見 collector/direct.go 那段註解):
// 版本查到過就是事實,抽風的是網路。連不上時版本號調淡,滑過去講得出來。
function kbVersionLine(a) {
const dim = a.cloudVerFresh ? '' : ' dim';
const tip = a.cloudVerFresh ? '' : '上次查到的版本(現在暫時連不上)';
if (!a.cloudVerKnown) {
const detail = a.cloudVerMine
? `已知版本 ${esc(a.cloudVerMine)},暫時查不到最新版本(稍後會自動再查)`
: '目前連不上這個知識庫,查不到版本';
return `<span class="d">${detail}</span>`;
// 分得出「沒查到這台的版本」與「查到了但暫時不知道最新版是多少」——
// 前者連版本號都沒有,後者有版本號可以顯示。
if (!a.cloudVerMine) {
return `<span class="kbv unknown" role="img" title="還沒查到這個知識庫的版本">○</span>`;
}
return `<span class="kbv${dim}" title="${esc(tip || '暫時查不到最新版本,稍後自動再查')}">${esc(a.cloudVerMine)}</span>`;
}
if (a.cloudVerStale) {
return `<span class="warn">有新版可更新(目前 ${esc(a.cloudVerMine)} 最新 ${esc(a.cloudVerLatest)}</span>
<button class="ghost" data-updatekb="${esc(a.email || '')}">前往安裝頁更新</button>`;
return `<span class="kbv${dim}" title="${esc(tip)}">${esc(a.cloudVerMine)}${esc(a.cloudVerLatest)}</span>
<button class="ico" data-updatekb="${esc(a.email || '')}"
title="前往安裝頁更新這個知識庫" aria-label="前往安裝頁更新這個知識庫">⬆️</button>`;
}
return `<span class="d">已是最新版(${esc(a.cloudVerMine)}</span>`;
return `<span class="kbv${dim}" title="${esc(tip)}">${esc(a.cloudVerMine)}</span>
<span class="fstat ok" role="img" title="已是最新版" aria-label="已是最新版">✅</span>`;
}
// installURLFor:與 portal 版本卡同一個做法——落後才需要按,按下去帶 email 讓安裝頁
@@ -320,7 +334,18 @@ function cardSkipped(k) {
// datapath → 樹(undefined=還沒問、null=問過但小幫手還沒回報)
// open:path → 這個資料夾的樹展開了沒
// nodespath → { 節點路徑: 展開了沒 }(沒有紀錄=用預設,見 nodeIsOpen
const treeState = { data: {}, open: {}, nodes: {} };
// whypath → { 節點路徑: 這一列的「為什麼」被點開了沒 }(arcrun-rag#159
const treeState = { data: {}, open: {}, nodes: {}, why: {} };
// ── 資料夾那一列的狀態圖示(`inkstone/arcrun-rag#159`)────────────────────
//
// leo 2026-08-28:「補送中、資料夾結構、移除,**變成 3 個 emoticon 就好,
// 畫面上都是字壓力很大**」「他要知道的是『我的資料夾是否同步了』,**有同步打勾就好**」
//
// 🔴 這裡**只做代碼→圖示的對照**,一個判斷都不做。
// 「該不該打勾」住在 Go 那一側(cmd/arcrun-app/folder_badge.go,有測試守著)——
// 前端自己判斷就會變成第二套判準,遲早跟後端說的不一樣。
const SYNC_ICON = { ok: '✅', working: '🔄', trouble: '⚠️', unknown: '○' };
// 資料夾路徑當不了 DOM id(含空白、斜線、中文)⇒ 折成一個穩定的短碼。
function treeBoxId(path) {
@@ -390,13 +415,12 @@ function renderFolderTree(path) {
// (指定了空資料夾,它就該在畫面上存在),前者是「再等一下」。
// 兩者講同一句話,等於拿我們自己編的答案回答使用者。
if (tree === null) {
box.innerHTML = `<div class="ftmsg">同步小幫手還沒掃到這個資料夾的結構。
第一次同步跑完之後就會出現——檔案多的話要等久一點,按上面的「立刻同步」可以催它。</div>`;
box.innerHTML = `<div class="ftmsg">還沒掃到這個資料夾,第一次同步跑完就會出現。</div>`;
return;
}
const nodes = tree.nodes || [];
if (!nodes.length) {
box.innerHTML = `<div class="ftmsg">這個資料夾目前沒有任何內容(指定了就會在這裡,等你放東西進去)。</div>`;
box.innerHTML = `<div class="ftmsg">這個資料夾目前是空的。</div>`;
return;
}
const r = rollupTree(nodes);
@@ -421,25 +445,37 @@ function renderFolderTree(path) {
+ `<span class="tw">${kids.length ? '<i></i>' : ''}</span>`
+ `<span class="ic">${kids.length && open ? '📂' : '📁'}</span>`
+ `<span class="nm">${esc(n.name || '(未命名)')}</span>`;
if (n.skipped && n.total_files === 0) {
// 🔴 整棵沒走進去 ⇒ 不准顯示 0/0(那會是我們自己編的數字),改講理由。
const why = n.skip_reason || '(小幫手沒說明理由)';
row += `<span class="skip">整個資料夾未收</span>`
+ `<span class="why" title="${esc(why)}">${esc(why)}</span>`;
} else if (n.skipped) {
// #180:走進去了、數字是真的,只是這一層一個檔都沒收
// ⇒ **數字照顯示**(他要知道底下有幾個),理由也照講。
const why = n.skip_reason || '(小幫手沒說明理由)';
row += `<span class="num">${s.synced} / ${s.total}</span>`
+ `<span class="why" title="${esc(why)}">${esc(why)}</span>`;
} else {
const full = s.total > 0 && s.synced === s.total;
row += `<span class="num${full ? ' full' : ''}">${s.synced} / ${s.total}</span>`;
const why = gapWhy(s);
if (why) row += `<span class="why" title="${esc(why)}">${esc(why)}</span>`;
}
if (kids.length && !open) row += `<span class="why">…還有 ${kids.length} 個子資料夾</span>`;
// ── 一列只有:三角形 + 資料夾名 + `X / Y``inkstone/arcrun-rag#159`)──
//
// leo 2026-08-28:「**上下廢話刪除**,**產生 GUI 就是要讓人減少讀字降低負擔,
// 你在 GUI 寫這麼多字剛好違背它的原理**」——他的截圖上同一句
// 「這是安裝範本時鋪下來的空白樣板…」**在一棵樹裡出現了 5 次**。
//
// 🔴 **「收起來」不等於「刪掉」**(本票紅線):那些理由是使用者要救回被跳過的
// 資料夾時的線索(`inkstone/arcrun-rag#136`)。所以理由沒有被拿掉,
// 只是**不再預設佔畫面**——點那個數字就展開這一列自己的理由。
// 🔴 也**不塞進 tooltip**(同一條紅線:「不要把長句子搬進 tooltip 裡繼續長」),
// tooltip 只有「點一下看原因」這種操作提示。
let why = n.skipped
? (n.skip_reason || '(小幫手沒說明理由)')
: gapWhy(s);
// 收檔策略那句話(原本掛在樹的上方,leo 圈掉了)改掛在**根那一列**——
// 它講的就是這個監看根,點根的數字就看得到,資訊沒有消失。
if (n.parent === '-' && tree.reason) why = why ? `${tree.reason}${why}` : tree.reason;
// 整棵沒走進去(#180 之後:skipped 且 total_files 為 0)⇒ 不准顯示 0/0
// 那會是我們自己編的數字。用「—」表示「這個數字我沒有」。
const noCount = n.skipped && n.total_files === 0;
const full = !noCount && s.total > 0 && s.synced === s.total;
const shown = noCount ? '—' : `${s.synced} / ${s.total}`;
row += `<span class="num${full ? ' full' : ''}${why ? ' hasWhy' : ''}"`
+ (why
? ` data-twhy="${esc(n.path)}" data-twroot="${esc(path)}" role="button" tabindex="0" title="點一下看原因"`
: '')
+ `>${shown}</span>`;
html += row + `</div>`;
if (why && (treeState.why[path] || {})[n.path]) {
html += `<div class="ftwhy" style="padding-left:${indent + 21}px">${esc(why)}</div>`;
}
if (open) kids.forEach(emit);
}
(r.kids['-'] || []).forEach(emit);
@@ -448,17 +484,22 @@ function renderFolderTree(path) {
nodes.forEach((n) => { seen[n.path] = true; });
nodes.forEach((n) => { if (n.parent !== '-' && !seen[n.parent]) emit(n); });
// 🔴 樹的上方與下方**什麼都不留**`inkstone/arcrun-rag#159`leo 在截圖上圈了三處):
// ① 上方的「全部展開/全部收合 + 這是一般資料夾或筆記庫…」整條拿掉
// ② 下方的「數字是『已同步 / 這個資料夾底下總共』…」整段拿掉
// ③ 下方的補送說明拿掉(狀態改由資料夾那一列的圖示表達)
// leo 原話:「**上下廢話刪除,只留乾淨 Tree**」。
//
// 收檔策略那句話(tree.reason)沒有消失——它跟每一列的理由走同一條路:
// 點根那一列的數字就看得到(emit() 裡的 hasWhy)。
//
// **唯一的例外是截斷警告**:它只在樹真的不完整時才出現,而不講就是讓畫面
// 宣稱「這是全部」——那是說謊,不是廢話。
let foot = '';
if (tree.truncated) {
foot += `<div class="ftmsg">資料夾太多,只顯示前 ${nodes.length} 個(實際有 ${tree.total_nodes} 個)。</div>`;
foot = `<div class="ftmsg">只顯示前 ${nodes.length}資料夾(實際有 ${tree.total_nodes} 個)。</div>`;
}
foot += `<div class="ftmsg">數字是「已同步 / 這個資料夾底下總共」。兩者不相等是正常的——
差額是還讀不了的格式、不在收檔範圍的檔案(程式碼等),或還在處理中。</div>`;
box.innerHTML = `<div class="fthead">`
+ `<button class="ghost" data-texpand="${esc(path)}">全部展開</button>`
+ `<button class="ghost" data-tcollapse="${esc(path)}">全部收合</button>`
+ (tree.reason ? `<span class="why">${esc(tree.reason)}</span>` : '')
+ `</div><div class="ftbody">${html}</div>${foot}`;
box.innerHTML = `<div class="ftbody">${html}</div>${foot}`;
wireTree();
}
@@ -467,8 +508,13 @@ function renderFolderTree(path) {
async function toggleFolderTree(path) {
treeState.open[path] = !treeState.open[path];
const box = $(treeBoxId(path));
// #159:那顆按鈕現在只有一個 CSS 畫的三角形(沒有文字),轉向由 class 決定。
const btn = document.querySelector(`[data-tree="${cssq(path)}"]`);
if (btn) btn.textContent = (treeState.open[path] ? '▾ ' : '▸ ') + '資料夾結構';
if (btn) {
btn.setAttribute('aria-expanded', String(!!treeState.open[path]));
const row = btn.closest('.folder');
if (row) row.classList.toggle('open', !!treeState.open[path]);
}
if (!box) return;
box.style.display = treeState.open[path] ? '' : 'none';
if (!treeState.open[path]) return;
@@ -487,16 +533,6 @@ async function toggleFolderTree(path) {
// 屬性選擇器要跳脫引號——資料夾路徑什麼字元都可能有。
function cssq(s) { return String(s).replace(/["\\]/g, '\\$&'); }
function setAllTreeNodes(path, open) {
const tree = treeState.data[path];
if (!tree) return;
const st = {};
(tree.nodes || []).forEach((n) => { st[n.path] = open; });
if (!open) st[''] = true; // 全部收合時根自己留著,不然整棵消失、看起來像壞了
treeState.nodes[path] = st;
renderFolderTree(path);
}
function wireTree() {
document.querySelectorAll('[data-tnode]').forEach((el) => {
const toggle = () => {
@@ -510,11 +546,19 @@ function wireTree() {
el.onclick = toggle;
el.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(); } };
});
document.querySelectorAll('[data-texpand]').forEach((el) => {
el.onclick = () => setAllTreeNodes(el.dataset.texpand, true);
});
document.querySelectorAll('[data-tcollapse]').forEach((el) => {
el.onclick = () => setAllTreeNodes(el.dataset.tcollapse, false);
// #159:點數字=展開這一列自己的「為什麼」。理由沒有被刪掉,只是不再預設佔畫面。
// stopPropagation:這個數字常常長在「整列可點=展開子資料夾」的列上,
// 不擋住的話點原因會順便把子資料夾收掉。
document.querySelectorAll('[data-twhy]').forEach((el) => {
const toggle = (e) => {
e.stopPropagation();
const root = el.dataset.twroot, np = el.dataset.twhy;
const st = treeState.why[root] || (treeState.why[root] = {});
st[np] = !st[np];
renderFolderTree(root);
};
el.onclick = toggle;
el.onkeydown = (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); toggle(e); } };
});
}
@@ -540,14 +584,17 @@ function pageLib(s, idx) {
: `正在從雲端收回…${f.retireRemaining ? `還有 ${f.retireRemaining}` : ''}`}</span>
</div>
${f.retireError ? `<div class="d folder-note">${esc(f.retireError)}</div>` : ''}` : `
<div class="folder">
<div class="folder${treeState.open[f.path] ? ' open' : ''}">
<button class="tw" data-tree="${esc(f.path)}" aria-expanded="${!!treeState.open[f.path]}"
title="展開這個資料夾" aria-label="展開或收合這個資料夾的內容"><i></i></button>
<span class="path" title="${esc(f.path)}">${esc(f.path)}</span>
<span class="tag">${f.resyncNote ? '補送中' : '自動同步中'}</span>
<button class="ghost" data-tree="${esc(f.path)}">${treeState.open[f.path] ? '' : '▸'} 資料夾結構</button>
<button class="ghost" data-rm="${esc(f.path)}" data-acc="${f.accIdx}">移除</button>
<span class="fstat ${esc(f.sync || 'unknown')}" role="img"
title="${esc(f.syncTip || '')}" aria-label="${esc(f.syncTip || '')}"
>${SYNC_ICON[f.sync] || SYNC_ICON.unknown}</span>
<button class="ico" data-rm="${esc(f.path)}" data-acc="${f.accIdx}"
title="移除這個資料夾" aria-label="移除這個資料夾並從知識庫收回">🗑</button>
</div>
<div class="ftbox" id="${treeBoxId(f.path)}"${treeState.open[f.path] ? '' : ' style="display:none"'}></div>
${f.resyncNote ? `<div class="d folder-note">${esc(f.resyncNote)}</div>` : ''}`).join('')
<div class="ftbox" id="${treeBoxId(f.path)}"${treeState.open[f.path] ? '' : ' style="display:none"'}></div>`).join('')
|| `<div class="empty"><div class="t">這個知識庫還沒有資料夾</div>
<div class="d">按右上的「加入資料夾」,選一個要自動整理的資料夾。</div></div>`}`;
}
+60 -12
View File
@@ -144,6 +144,41 @@ button.ghost:hover { color: var(--err); background: transparent; }
.folder .tag.retiring { color: rgba(var(--ink-rgb), .5); }
.folder-note { margin: -3px 0 9px; padding: 0 15px; font-size: 12.5px; }
/* ══ arcrun-rag#159:那一列只剩「三角形 + 路徑 + 兩個圖示」══════════════
leo 2026-08-28:「補送中、資料夾結構、移除,**變成 3 個 emoticon 就好,
畫面上都是字壓力很大**」「**展開三角形按鈕放資料夾上**」
⇒ 三角形移到列首(跟樹裡面的展開鈕同一個造型,使用者只要學一次)。 */
.folder > .tw {
flex: none; width: 22px; height: 22px; padding: 0; border: none; background: none;
display: inline-flex; align-items: center; justify-content: center;
}
.folder > .tw:hover { background: none; }
/* 三角形用 CSS 畫,不用 ▸▾ 字元——理由同樹裡那顆(見 .ftrow .tw i):
那兩個字在 Windows 的中文字型裡是全形,大小與位置每台機器都不一樣。 */
.folder > .tw i {
display: block; width: 0; height: 0;
border-left: 6px solid rgba(var(--ink-rgb), .5);
border-top: 5px solid transparent; border-bottom: 5px solid transparent;
transition: transform .12s;
}
.folder.open > .tw i { transform: rotate(90deg); }
.folder > .tw:hover i { border-left-color: var(--amber); }
/* 狀態圖示。**一個字都沒有**——說明住在 title/aria-label 裡,滑過去才出現。 */
.folder .fstat { flex: none; font-size: 14px; line-height: 1; cursor: default; }
/* 🔴 視覺層級要對得上意思:**只有需要你出手的那一格才准大聲**。
「同步中」是常態、不需要任何人做什麼 ⇒ 壓到跟灰階一樣安靜;
⚠️ 維持原色,因為它是唯一一個「你可能要看一下」的狀態。
(不壓的話 🔄 的藍色會比 ⚠️ 還跳,畫面就在對使用者喊錯的東西。) */
.folder .fstat.working { filter: grayscale(.85); opacity: .5; }
/* 還沒回報過的那一格刻意做得很淡:它不是「沒問題」,是「還不知道」。 */
.folder .fstat.unknown { color: rgba(var(--ink-rgb), .3); font-size: 12px; }
/* 圖示按鈕(移除):跟三角形同一個量級,hover 才變紅——不預設嚷嚷。 */
.folder .ico {
flex: none; padding: 4px 6px; border: none; background: none;
font-size: 14px; line-height: 1; filter: grayscale(1); opacity: .7;
}
.folder .ico:hover { background: none; filter: none; opacity: 1; }
.big-num { font-size: 26px; font-weight: 600; letter-spacing: .01em; }
.kv { display: flex; gap: 26px; margin-top: 4px; }
.kv .k { font-size: 12.5px; color: rgba(var(--ink-rgb), .45); }
@@ -299,6 +334,14 @@ input[type=text], input[type=password] {
.kbrow .nm { font-size: 14px; font-weight: 600; flex: none; }
.kbrow .right { display: flex; align-items: center; gap: 10px; font-size: 13px; }
.kbver { margin-bottom: 14px; font-size: 13.5px; display: flex; align-items: center; gap: 10px; }
/* arcrun-rag#159:版本那一格只有**數字與圖示**,說明收進 title。
等寬字讓 `1.4.46 → 1.4.58` 這種對照掃視時對得齊。 */
.kbv { font-family: ui-monospace, Menlo, monospace; font-size: 12.5px; color: rgba(var(--ink-rgb), .7); }
/* 這一輪連不上 ⇒ 版本號調淡表示「這是上次查到的」。**不是把它抹掉**——
版本查到過就是事實,抽風的是網路(見 collector/direct.go #159 那段)。 */
.kbv.dim { opacity: .45; }
/* 從來沒查到過:一個很淡的圈,跟資料夾那一列的 unknown 同一個語彙。 */
.kbv.unknown { font-family: inherit; color: rgba(var(--ink-rgb), .3); }
.warn { color: var(--err); font-size: 13px; }
#side .nav .dot.warn {
width: 7px; height: 7px; border-radius: 50%; flex: none;
@@ -432,12 +475,10 @@ input[type=text], input[type=password] {
margin: -3px 0 9px; padding: 8px 10px 6px;
background: var(--well); border-radius: 10px;
}
.fthead {
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
padding-bottom: 7px; margin-bottom: 5px;
border-bottom: 1px solid rgba(var(--ink-rgb), .08);
}
.fthead button { padding: 4px 9px; font-size: 12px; }
/* 🔴 arcrun-rag#159`.fthead`(全部展開/全部收合+收檔策略那句話)**整條移除**。
leo 2026-08-28 在截圖上圈掉它:「上下廢話刪除,只留乾淨 Tree」
「產生 GUI 就是要讓人減少讀字降低負擔,你在 GUI 寫這麼多字剛好違背它的原理」。
收檔策略那句話沒有被刪掉——它改掛在根那一列的數字上,點一下就看得到。 */
/* 樹本體:檔案總管那種「緊湊但點得到」的密度。
overflow-x 讓很深的樹自己捲,不去撐破右側版面。 */
.ftbody { overflow-x: auto; padding: 1px 0; }
@@ -474,6 +515,19 @@ input[type=text], input[type=password] {
}
/* 全部同步完=這一格沒有待辦,用完整墨色讓它從灰階裡浮出來(不另發明「成功色」)。 */
.ftrow .num.full { color: var(--ink); }
/* #159:這一列有「為什麼」可以看時,數字自己就是那顆按鈕(畫面上不多一個字、
也不多一顆按鈕)。虛線底線是唯一的提示——**它不是裝飾,是可點的告示**。 */
.ftrow .num.hasWhy {
cursor: pointer;
border-bottom: 1px dotted rgba(var(--ink-rgb), .35);
}
.ftrow .num.hasWhy:hover { color: var(--ink); border-bottom-color: var(--amber); }
.ftrow .num.hasWhy:focus-visible { outline: 2px solid var(--amber); outline-offset: 1px; }
/* 點開之後那一行理由。預設不存在——這就是「收起來不等於刪掉」的落點。 */
.ftwhy {
font-size: 12px; line-height: 1.6; padding: 1px 4px 4px;
color: rgba(var(--ink-rgb), .55); white-space: normal; max-width: 46em;
}
/* 🔴 選擇器**不要**綁在 .ftrow 底下:頁首那句「為什麼只收這些」也用同一個 class,
綁死就只有樹裡面吃得到樣式、頁首那句會掉回 16px 的內文級——實撞過一次,
畫面上那句話比整棵樹都大聲,喧賓奪主。 */
@@ -484,12 +538,6 @@ input[type=text], input[type=password] {
font-size: 12px; color: rgba(var(--ink-rgb), .45);
flex: 0 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis;
}
.fthead .why { flex: 1; min-width: 0; line-height: 1.6; }
/* 整棵沒走進去:語氣是告知不是錯誤,所以不用 --err。 */
.ftrow .skip {
font-size: 11.5px; color: rgba(var(--ink-rgb), .5);
border: 1px solid rgba(var(--ink-rgb), .18); border-radius: 5px; padding: 0 5px;
}
.ftmsg {
padding: 6px 2px 2px; font-size: 12px; line-height: 1.65;
color: rgba(var(--ink-rgb), .45);