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:
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* console「分流台」純函式層(Arcrun#9 收件夾改裝=全部待辦分流台,2026-07-08 總管派工;
|
||||
* 頂層 SDD kb-ingest-architecture R7 消費端)
|
||||
*
|
||||
* 為什麼獨立成 lib(比照 console-dashboard-model.ts):route 只做 IO(fetch KBDB entries),
|
||||
* 「兩種 entry 契約 → 統一分流項」的解析/分欄/分項判定全在這裡,vitest 直接餵資料驗證。
|
||||
*
|
||||
* 資料來源二合一(R7):
|
||||
* - entry_type=todo:Logseq 萃取灌入(Arcrun#8 ingest 線),content JSON 契約(已定)=
|
||||
* {"text","marker","owner_tier":"ai|collab|leo","project","source","status":"new|done"}
|
||||
* - entry_type=inbox:Telegram 指令(inbox_receive),沿用 {"text","from","status"}
|
||||
*
|
||||
* 三欄分流(80/15/5,北極星「減 leo 負」):
|
||||
* ai = AI 會幫我搞定(80%)
|
||||
* collab = AI 要我做完它接手(15%)
|
||||
* leo = 非 leo 不可(5%)
|
||||
* 無 owner_tier / 看不懂的值(triage engine 還沒判到)→ 歸 collab 欄但標 unclassified,
|
||||
* UI 顯「未分流」徽章——不冒領「AI 會搞定」、也不誤判成「非 leo 不可」,且欄位維持三欄
|
||||
* 不另開第四欄(80/15/5 的故事不破)。issue 原文的 mira80/collab15/leo5 詞彙當同義吃進來
|
||||
* (ingest 端若照 issue 舊詞灌,UI 不至於整欄蒸發)。
|
||||
*/
|
||||
|
||||
import { type KbdbEntry, parseCreatedAtMs, parseJsonContent } from './console-dashboard-model';
|
||||
|
||||
export type TriageTier = 'ai' | 'collab' | 'leo';
|
||||
|
||||
/** 契約值 → 三欄。issue 原文舊詞(mira80/collab15/leo5)當同義收;其餘=未分流。 */
|
||||
const TIER_ALIASES: Record<string, TriageTier> = {
|
||||
ai: 'ai',
|
||||
collab: 'collab',
|
||||
leo: 'leo',
|
||||
mira80: 'ai',
|
||||
collab15: 'collab',
|
||||
leo5: 'leo',
|
||||
};
|
||||
|
||||
export interface TriageItem {
|
||||
id: string;
|
||||
text: string;
|
||||
tier: TriageTier;
|
||||
/** true=owner_tier 缺席/看不懂(triage engine 未判),暫歸 collab 欄+UI 標「未分流」 */
|
||||
unclassified: boolean;
|
||||
/** '' = 未分項(UI 過濾 chips 用「未分項」承接) */
|
||||
project: string;
|
||||
/** todo 來源=content.source(如 notes);inbox 一律 telegram(可帶 from) */
|
||||
source: string;
|
||||
/** Logseq marker(TODO/DOING/LATER/NOW…);inbox 無='' */
|
||||
marker: string;
|
||||
status: 'new' | 'done';
|
||||
origin: 'todo' | 'inbox';
|
||||
at: string | number;
|
||||
at_ms: number | null;
|
||||
}
|
||||
|
||||
export interface TriageModel {
|
||||
/** 全部項(created_at 新→舊);分欄/過濾在 UI 端做(資料一次給齊,chips 切換零請求) */
|
||||
items: TriageItem[];
|
||||
/** 出現過的 project(去重、字典序;不含 ''——「未分項」由 UI 依 items 判斷要不要出 chip) */
|
||||
projects: string[];
|
||||
/** 各欄未完成數(status=new)+整體計數,供欄位標頭/收件計數用 */
|
||||
counts: { ai: number; collab: number; leo: number; unclassified: number; done: number; total: number };
|
||||
}
|
||||
|
||||
function normalizeStatus(v: unknown): 'new' | 'done' {
|
||||
return v === 'done' ? 'done' : 'new';
|
||||
}
|
||||
|
||||
function normalizeTier(v: unknown): { tier: TriageTier; unclassified: boolean } {
|
||||
const mapped = typeof v === 'string' ? TIER_ALIASES[v.trim()] : undefined;
|
||||
return mapped ? { tier: mapped, unclassified: false } : { tier: 'collab', unclassified: true };
|
||||
}
|
||||
|
||||
function str(v: unknown): string {
|
||||
return typeof v === 'string' ? v.trim() : '';
|
||||
}
|
||||
|
||||
/** todo entry → 分流項。content 非 JSON(不合契約的殘資料)→ 原文當 text 誠實顯示,不丟棄。 */
|
||||
export function todoToTriageItem(e: KbdbEntry): TriageItem {
|
||||
const j = parseJsonContent(e);
|
||||
const { tier, unclassified } = normalizeTier(j?.owner_tier);
|
||||
return {
|
||||
id: e.id,
|
||||
text: str(j?.text) || (e.content ?? ''),
|
||||
tier,
|
||||
unclassified,
|
||||
project: str(j?.project),
|
||||
source: str(j?.source),
|
||||
marker: str(j?.marker),
|
||||
status: normalizeStatus(j?.status),
|
||||
origin: 'todo',
|
||||
at: e.created_at,
|
||||
at_ms: parseCreatedAtMs(e.created_at),
|
||||
};
|
||||
}
|
||||
|
||||
/** inbox entry → 分流項(沿用 {"text","from","status"} 契約)。無 tier =未分流歸 collab。 */
|
||||
export function inboxToTriageItem(e: KbdbEntry): TriageItem {
|
||||
const j = parseJsonContent(e);
|
||||
const from = str(j?.from);
|
||||
return {
|
||||
id: e.id,
|
||||
text: str(j?.text) || (e.content ?? ''),
|
||||
tier: 'collab',
|
||||
unclassified: true,
|
||||
project: '',
|
||||
source: from ? `telegram・${from}` : 'telegram',
|
||||
marker: '',
|
||||
status: normalizeStatus(j?.status),
|
||||
origin: 'inbox',
|
||||
at: e.created_at,
|
||||
at_ms: parseCreatedAtMs(e.created_at),
|
||||
};
|
||||
}
|
||||
|
||||
/** 二源合一 → 分流台模型。排序=created_at 新→舊(時間不明沉底);分欄計數只算未完成。 */
|
||||
export function buildTriageModel(todoEntries: KbdbEntry[], inboxEntries: KbdbEntry[]): TriageModel {
|
||||
const items = [
|
||||
...todoEntries.map(todoToTriageItem),
|
||||
...inboxEntries.map(inboxToTriageItem),
|
||||
].sort((a, b) => (b.at_ms ?? -1) - (a.at_ms ?? -1));
|
||||
const projects = [...new Set(items.map((i) => i.project).filter(Boolean))].sort();
|
||||
const counts = { ai: 0, collab: 0, leo: 0, unclassified: 0, done: 0, total: items.length };
|
||||
for (const it of items) {
|
||||
if (it.status === 'done') {
|
||||
counts.done++;
|
||||
continue;
|
||||
}
|
||||
counts[it.tier]++;
|
||||
if (it.unclassified) counts.unclassified++;
|
||||
}
|
||||
return { items, projects, counts };
|
||||
}
|
||||
Reference in New Issue
Block a user