feat(console): 收件夾改裝=全部待辦分流台(Arcrun#9,kb-ingest SDD R7 消費端)
console 頁 7 從 Telegram 訊息清單改裝成三欄分流台: - 資料源二合一:entry_type=todo(Logseq 萃取,Arcrun#8 ingest 線)+ entry_type=inbox (Telegram),/console/inbox-data → /console/triage-data(唯一消費者是本頁,一起改 裝不留死端點;仍鎖 console session——待辦原文屬機敏) - 三欄 80/15/5:ai=AI 會幫我搞定/collab=協作/leo=非我不可;owner_tier 缺席或看 不懂(triage engine 還沒判)→ 歸 collab 欄+「未分流」徽章,不冒領 ai、不誤判 leo - 各項顯示 text/project 徽章/source/marker;status:done 預設隱藏可切換 - per-project chips 過濾(全部/各 project/未分項),前端同包資料過濾零額外請求 - 手機直排沿 console 單檔薄殼;讀 KBDB 走既有 kbdbBase HTTP 慣例,不新增 binding 契約解析/分欄/計數=純函式 lib/console-triage-model.ts(比照 console-dashboard-model 模式,route 只做 IO),tests/console-triage-model.test.ts 11 測全綠(紅→綠驗證:抽掉 實作模組同測試即紅)。全套 vitest 98 passed,唯一失敗為 pre-existing executor 「不存在的零件」訊息斷言(main 同紅,與本 PR 無關)。 [總管-subagent] Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,7 +7,8 @@
|
||||
* 手機優先 390px + ≥1024px 側欄、頁面本體不出水平捲軸。
|
||||
*
|
||||
* 資訊架構:brief 8 頁(docs/1-vision/mira-console-design-brief.md)=
|
||||
* 1 登入/首次設定 2 駕駛艙 3 總庫搜尋 4 卡片詳頁+關聯 5 工作流 6 憑證管理 7 收件匣 8 設定。
|
||||
* 1 登入/首次設定 2 駕駛艙 3 總庫搜尋 4 卡片詳頁+關聯 5 工作流 6 憑證管理
|
||||
* 7 分流台(原收件匣,Arcrun#9 改裝) 8 設定。
|
||||
* 單檔 HTML + 原生 JS hash routing(#/cockpit …),零外部資源、零 build step。
|
||||
*
|
||||
* 薄殼(rule 07):零業務邏輯,只 fetch 既有 API 渲染。各頁資料源(誠實原則,無源的顯示
|
||||
@@ -23,7 +24,10 @@
|
||||
* + GET /workflows/:name/executions(最近執行);釘選存 localStorage
|
||||
* - 憑證:GET /credentials/catalog(D1 目錄唯讀,絕不含密文)+ POST /credentials(新增)
|
||||
* + PUT /credentials/:name(整筆替換);刪除後端未接 D1(T9 範圍)→ 標「即將開通」
|
||||
* - 收件匣:GET /console/inbox-data(需 session——訊息原文屬機敏,計數才免登入)
|
||||
* - 分流台:GET /console/triage-data(需 session——待辦/訊息原文屬機敏,計數才免登入)。
|
||||
* 資料源二合一(kb-ingest SDD R7 / Arcrun#9):entry_type=todo(Logseq 萃取)+
|
||||
* entry_type=inbox(Telegram);三欄 80/15/5(ai/collab/leo)+ per-project chips
|
||||
* +已完成預設隱藏可切換。分欄判定在 lib/console-triage-model.ts(純函式),頁面只渲染。
|
||||
* - 設定:vectorize 狀態探測(讀 /kbdb/search 回應的 mode/capability_hint,無新端點;
|
||||
* 開關本體需部署端 config kbdb_embed + acr update → 誠實文案講清楚、不假裝能遠端開)
|
||||
* + MCP token TTL 誠實佔位(/console/settings-data;可調功能=Arcrun#19)
|
||||
@@ -231,7 +235,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
.gcenter span { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 13.5px; font-weight: 600; line-height: 1.35; color: #241804; word-break: break-word; }
|
||||
.nbrow { display: flex; align-items: center; gap: 11px; padding: 12px 14px; border-radius: 10px; cursor: pointer; background: rgba(var(--ink-rgb),.045); border: 1px solid rgba(var(--ink-rgb),.1); font-size: 15.5px; }
|
||||
|
||||
/* ── 工作流 / 憑證 / 收件匣 清單 ── */
|
||||
/* ── 工作流 / 憑證 / 分流台 清單 ── */
|
||||
.listcol { display: flex; flex-direction: column; gap: 10px; padding: 16px 0 8px; }
|
||||
.rowhead { display: flex; align-items: center; gap: 12px; padding: 16px 18px; cursor: pointer; }
|
||||
.rowbody { padding: 0 18px 18px; border-top: 1px solid rgba(var(--ink-rgb),.1); }
|
||||
@@ -241,10 +245,23 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
.urlbox .u { font-family: ui-monospace, Menlo, monospace; font-size: 13.5px; color: var(--amber); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0; }
|
||||
.avatar { width: 38px; height: 38px; border-radius: 10px; background: rgba(var(--amber-rgb),.1); border: 1px solid rgba(var(--amber-rgb),.25); display: grid; place-items: center; font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 17px; color: var(--amber); flex: none; }
|
||||
.maskline { display: flex; align-items: center; gap: 10px; padding: 14px; border-radius: 10px; background: var(--well); border: 1px solid rgba(var(--ink-rgb),.1); }
|
||||
.inbox-item { padding: 16px 18px; border-radius: 12px; }
|
||||
.inbox-item.new { background: rgba(var(--amber-rgb),.07); border: 1px solid rgba(var(--amber-rgb),.28); }
|
||||
.inbox-item.done { background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.1); }
|
||||
.pulse-dot { width: 7px; height: 7px; border-radius: 50%; background: var(--amber); flex: none; animation: breatheAmber 2.6s ease-in-out infinite; }
|
||||
/* 分流台(Arcrun#9):三欄 80/15/5 手機直排;欄色點 ai=綠 collab=琥珀 leo=紅 */
|
||||
.tri-sec { margin-top: 24px; }
|
||||
.tri-sechead { display: flex; align-items: baseline; gap: 9px; }
|
||||
.tri-sechead .dot { flex: none; width: 9px; height: 9px; border-radius: 50%; align-self: center; }
|
||||
.tri-sec.ai .dot { background: var(--ok); } .tri-sec.collab .dot { background: var(--amber); } .tri-sec.leo .dot { background: var(--err); }
|
||||
.tri-sechead .nm { font-family: 'Songti TC','LiSong Pro',PMingLiU,serif; font-size: 17px; letter-spacing: .14em; }
|
||||
.tri-sechead .pc { font-size: 12.5px; color: rgba(var(--ink-rgb),.4); }
|
||||
.tri-sechead .ct { margin-left: auto; font-family: ui-monospace, Menlo, monospace; font-size: 14px; color: rgba(var(--ink-rgb),.55); }
|
||||
.tri-item { padding: 14px 16px; border-radius: 12px; margin-top: 9px; }
|
||||
.tri-item.new { background: rgba(var(--amber-rgb),.07); border: 1px solid rgba(var(--amber-rgb),.28); }
|
||||
.tri-sec.leo .tri-item.new { background: rgba(var(--err-rgb),.06); border-color: rgba(var(--err-rgb),.3); }
|
||||
.tri-item.done { background: rgba(var(--ink-rgb),.04); border: 1px solid rgba(var(--ink-rgb),.1); opacity: .7; }
|
||||
.tri-item.done .tx { text-decoration: line-through; color: rgba(var(--ink-rgb),.6); }
|
||||
.tri-item .tx { font-size: 16.5px; line-height: 1.6; word-break: break-word; }
|
||||
.tri-item .mt { margin-top: 9px; display: flex; gap: 7px; align-items: center; flex-wrap: wrap; font-size: 12.5px; color: rgba(var(--ink-rgb),.45); }
|
||||
.tag.red { background: rgba(var(--err-rgb),.12); color: var(--err); }
|
||||
.tag.dim { background: rgba(var(--ink-rgb),.08); color: rgba(var(--ink-rgb),.55); }
|
||||
|
||||
/* ── 設定 ── */
|
||||
.setcol { display: flex; flex-direction: column; gap: 14px; padding: 20px 0 40px; }
|
||||
@@ -303,7 +320,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
<div class="nav" data-nav="search">總庫搜尋</div>
|
||||
<div class="nav" data-nav="workflows">工作流</div>
|
||||
<div class="nav" data-nav="creds">憑證管理</div>
|
||||
<div class="nav" data-nav="inbox">收件匣</div>
|
||||
<div class="nav" data-nav="inbox">分流台</div>
|
||||
<div class="nav" data-nav="settings">設定</div>
|
||||
<div class="nav themelabel" data-themetoggle style="margin-top:8px;font-size:14.5px">☾ 切深色</div>
|
||||
<div class="foot" id="side-foot"></div>
|
||||
@@ -417,10 +434,11 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
<div class="listcol" id="cred-list"><div class="muted">載入中…</div></div>
|
||||
</div>
|
||||
|
||||
<!-- 收件匣 -->
|
||||
<!-- 分流台(原收件匣,Arcrun#9 改裝=全部待辦分流台) -->
|
||||
<div class="view page narrow" id="v-inbox">
|
||||
<div class="pagehead"><span class="t">收件匣</span><span class="m">Telegram 指令</span></div>
|
||||
<div id="ib-out" style="padding-top:6px"><div class="muted" style="padding:14px 0">載入中…</div></div>
|
||||
<div class="pagehead"><span class="t">分流台</span><span class="m">全部待辦・80/15/5</span></div>
|
||||
<div class="hscroll" id="tg-chips" style="margin-top:14px"></div>
|
||||
<div id="tg-out" style="padding-top:2px"><div class="muted" style="padding:14px 0">載入中…</div></div>
|
||||
</div>
|
||||
|
||||
<!-- 設定 -->
|
||||
@@ -489,7 +507,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
<div class="tab" data-nav="cockpit">駕駛艙</div>
|
||||
<div class="tab" data-nav="search">搜尋</div>
|
||||
<div class="tab" data-nav="workflows">工作流</div>
|
||||
<div class="tab" data-nav="inbox">收件匣</div>
|
||||
<div class="tab" data-nav="inbox">分流台</div>
|
||||
<div class="tab" data-nav="settings">設定</div>
|
||||
</div>
|
||||
|
||||
@@ -631,7 +649,7 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
});
|
||||
window.scrollTo(0, 0);
|
||||
}
|
||||
var LOADERS = { cockpit: loadCockpit, search: initSearchOnce, card: loadCard, workflows: loadWorkflows, creds: loadCreds, inbox: loadInbox, settings: loadSettings };
|
||||
var LOADERS = { cockpit: loadCockpit, search: initSearchOnce, card: loadCard, workflows: loadWorkflows, creds: loadCreds, inbox: loadTriage, settings: loadSettings };
|
||||
function route() {
|
||||
var r = currentRoute();
|
||||
if (r.view === 'card' && r.arg) S.cardId = decodeURIComponent(r.arg);
|
||||
@@ -1239,42 +1257,78 @@ function renderConsoleHtml(registryBase: string): string {
|
||||
}
|
||||
});
|
||||
|
||||
// ── 收件匣 ──
|
||||
function loadInbox() {
|
||||
var out = $('ib-out');
|
||||
// ── 分流台(原收件匣,Arcrun#9:todo+inbox 二源、三欄 80/15/5、per-project chips)──
|
||||
// 頁面只渲染 /console/triage-data 的結果(分欄/計數判定在後端純函式);chips 切換與
|
||||
// 「顯示已完成」是同一包資料的前端過濾,零額外請求(leo 手機看,省來回)。
|
||||
var TG = { data: null, project: '', showDone: false };
|
||||
var TG_TIERS = [
|
||||
{ key: 'ai', nm: 'AI 會幫我搞定', pc: '80%', empty: '這欄空的——還沒有分給 AI 的待辦' },
|
||||
{ key: 'collab', nm: '協作・AI 做完你接手', pc: '15%', empty: '這欄空的——沒有要你配合的事' },
|
||||
{ key: 'leo', nm: '非我不可', pc: '5%', empty: '這欄空的——沒有非你不可的事' }
|
||||
];
|
||||
function triItem(m) {
|
||||
var meta = '';
|
||||
if (m.marker) meta += '<span class="tag dim">' + esc(m.marker) + '</span>';
|
||||
meta += '<span class="tag">' + esc(m.project || '未分項') + '</span>';
|
||||
if (m.source) meta += '<span>' + esc(m.source) + '</span>';
|
||||
if (m.unclassified && m.origin === 'todo') meta += '<span class="tag red">未分流</span>';
|
||||
meta += '<span class="mono" style="margin-left:auto">' + esc(fmtDateTime(m.at)) + '</span>';
|
||||
return '<div class="tri-item ' + (m.status === 'done' ? 'done' : 'new') + '"><div class="tx">' + esc(m.text) + '</div><div class="mt">' + meta + '</div></div>';
|
||||
}
|
||||
function triChip(label, val, on) {
|
||||
return '<button class="chip' + (on ? ' on' : '') + '" data-triproj="' + esc(val) + '">' + esc(label) + '</button>';
|
||||
}
|
||||
function renderTriage() {
|
||||
var d = TG.data || {};
|
||||
var items = d.items || [];
|
||||
var out = $('tg-out');
|
||||
// per-project chips:全部+各 project+(有未分項才出)未分項
|
||||
var chips = triChip('全部', '', TG.project === '');
|
||||
(d.projects || []).forEach(function (p) { chips += triChip(p, p, TG.project === p); });
|
||||
if (items.some(function (i) { return !i.project; })) chips += triChip('未分項', '__none__', TG.project === '__none__');
|
||||
$('tg-chips').innerHTML = items.length ? chips : '';
|
||||
if (!items.length) {
|
||||
out.innerHTML = '<div class="honest" style="margin-top:14px"><div class="h">分流台是空的</div><div class="b">資料源=KBDB todo entries(Logseq 萃取,ingest 灌入中)+ inbox entries(Telegram)。<br>寫入方尚未灌資料時這裡誠實顯示空。</div></div>';
|
||||
return;
|
||||
}
|
||||
var vis = items.filter(function (i) {
|
||||
return TG.project === '' || (TG.project === '__none__' ? !i.project : i.project === TG.project);
|
||||
});
|
||||
var doneN = vis.filter(function (i) { return i.status === 'done'; }).length;
|
||||
var html = '';
|
||||
TG_TIERS.forEach(function (t) {
|
||||
var news = vis.filter(function (i) { return i.tier === t.key && i.status !== 'done'; });
|
||||
var dones = TG.showDone ? vis.filter(function (i) { return i.tier === t.key && i.status === 'done'; }) : [];
|
||||
html += '<div class="tri-sec ' + t.key + '"><div class="tri-sechead"><span class="dot"></span><span class="nm serif">' + t.nm + '</span><span class="pc">' + t.pc + '</span><span class="ct">' + news.length + ' 件</span></div>';
|
||||
if (!news.length && !dones.length) html += '<div class="muted" style="font-size:14px;padding:10px 2px 0">' + t.empty + '</div>';
|
||||
html += news.map(triItem).join('') + dones.map(triItem).join('');
|
||||
html += '</div>';
|
||||
});
|
||||
html += '<div style="margin:24px 0 20px;text-align:center"><button class="btn3" id="tg-donetoggle" style="padding:9px 18px;font-size:13.5px;border-radius:999px">' +
|
||||
(TG.showDone ? '隱藏已完成' : '顯示已完成(' + doneN + ')') + '</button></div>';
|
||||
out.innerHTML = html;
|
||||
$('tg-donetoggle').addEventListener('click', function () { TG.showDone = !TG.showDone; renderTriage(); });
|
||||
}
|
||||
$('tg-chips').addEventListener('click', function (ev) {
|
||||
var t = ev.target.closest('[data-triproj]');
|
||||
if (!t) return;
|
||||
TG.project = t.getAttribute('data-triproj');
|
||||
renderTriage();
|
||||
});
|
||||
function loadTriage() {
|
||||
var out = $('tg-out');
|
||||
out.innerHTML = '<div class="muted" style="padding:14px 0">載入中…</div>';
|
||||
fetch('/console/inbox-data', { headers: { Authorization: 'Bearer ' + S.token } })
|
||||
fetch('/console/triage-data', { headers: { Authorization: 'Bearer ' + S.token } })
|
||||
.then(function (r) { return r.json().then(function (d) { return { ok: r.ok, status: r.status, d: d }; }); })
|
||||
.then(function (x) {
|
||||
if (!x.ok) {
|
||||
out.innerHTML = '<div class="honest" style="margin-top:14px"><div class="h">讀不到收件匣</div><div class="b">' + esc(x.d.error || ('HTTP ' + x.status)) + '</div></div>';
|
||||
out.innerHTML = '<div class="honest" style="margin-top:14px"><div class="h">讀不到分流台</div><div class="b">' + esc(x.d.error || ('HTTP ' + x.status)) + '</div></div>';
|
||||
return;
|
||||
}
|
||||
var items = x.d.items || [];
|
||||
if (!items.length) {
|
||||
out.innerHTML = '<div class="honest" style="margin-top:14px"><div class="h">收件匣是空的</div><div class="b">從 Telegram 丟指令給機器人,這裡會列出「新的」與「已處理」。<br>(資料源:KBDB inbox entries——寫入方尚未灌資料時這裡誠實顯示空。)</div></div>';
|
||||
return;
|
||||
}
|
||||
var news = items.filter(function (i) { return i.status !== 'done'; });
|
||||
var done = items.filter(function (i) { return i.status === 'done'; });
|
||||
var html = '';
|
||||
html += '<div class="serif" style="margin:20px 0 10px;font-size:16px;letter-spacing:.18em;color:var(--amber)">新的・' + news.length + ' 條</div>';
|
||||
html += news.length
|
||||
? '<div style="display:flex;flex-direction:column;gap:9px">' + news.map(function (m) {
|
||||
return '<div class="inbox-item new"><div style="font-size:16.5px;line-height:1.6;word-break:break-word">' + esc(m.text) + '</div>' +
|
||||
'<div style="margin-top:8px;display:flex;gap:10px;align-items:center;font-size:13px;color:rgba(var(--ink-rgb),.45)"><span class="pulse-dot"></span><span>等待 AI 處理</span><span class="mono" style="margin-left:auto">' + esc(fmtDateTime(m.at)) + '</span></div></div>';
|
||||
}).join('') + '</div>'
|
||||
: '<div class="muted" style="font-size:14px">沒有待處理的指令</div>';
|
||||
html += '<div class="serif" style="margin:26px 0 10px;font-size:16px;letter-spacing:.18em;color:rgba(var(--ink-rgb),.55)">已處理・' + done.length + ' 條</div>';
|
||||
html += done.length
|
||||
? '<div style="display:flex;flex-direction:column;gap:9px;padding-bottom:20px">' + done.map(function (m) {
|
||||
return '<div class="inbox-item done"><div style="font-size:16px;line-height:1.6;color:rgba(var(--ink-rgb),.7);word-break:break-word">' + esc(m.text) + '</div>' +
|
||||
'<div style="margin-top:8px;display:flex;gap:9px;align-items:baseline;font-size:14px"><span class="ok" style="flex:none">✓</span>' +
|
||||
'<span style="color:rgba(127,224,168,.8);line-height:1.5;word-break:break-word">' + esc(m.result || '已處理') + '</span>' +
|
||||
'<span class="dim mono" style="margin-left:auto;flex:none;font-size:13px">' + esc(fmtDateTime(m.at)) + '</span></div></div>';
|
||||
}).join('') + '</div>'
|
||||
: '<div class="muted" style="font-size:14px;padding-bottom:20px">還沒有已處理的紀錄</div>';
|
||||
out.innerHTML = html;
|
||||
TG.data = x.d;
|
||||
// 記著的 project 篩選若已不存在(資料變了)→ 退回全部,別讓頁面空得不明不白
|
||||
if (TG.project && TG.project !== '__none__' && (x.d.projects || []).indexOf(TG.project) < 0) TG.project = '';
|
||||
renderTriage();
|
||||
})
|
||||
.catch(function (e) { out.innerHTML = '<div class="err" style="padding:14px 0">請求失敗:' + esc(friendlyErr(e)) + '</div>'; });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user