- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次) - .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…), 目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪 - .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/) - .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*) - .gitignore 补:备份件(*.bak-*) - 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
1182 lines
83 KiB
HTML
1182 lines
83 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||
<title>多会话协作 · 实时架构看板</title>
|
||
<script>/* 🔴 在样式生效前就套用上次选的风格 —— ⛔ 别等 load,否则会先闪一下当前风格 */
|
||
(function(){try{var s=localStorage.getItem('dsh-board-style');
|
||
if(s&&s!=='base'){document.documentElement.setAttribute('data-style',s)}}catch(e){}})();
|
||
</script>
|
||
<style>
|
||
/* ══════════════════════════════════════════════════════════════════════════════
|
||
🔴 风格系统(用户 2026-09-30:「画架构图参考 archify 的样式,右上角加风格切换(保留当前风格)」)
|
||
· **切换只换 `<html data-style="…">`** —— 颜色/字体/网格全部走令牌,SVG 里没有硬编码颜色
|
||
⇒ 换风格**不需要重画图**。
|
||
· 三套:`base`(**保留的当前风格**)/`archify-dark`/`archify-light`
|
||
(后两套令牌逐字取自 archify 的 `assets/template.html`,MIT)。
|
||
· ⛔ 新写样式一律走令牌,别在组件里写死颜色 —— 否则换风格会花。
|
||
══════════════════════════════════════════════════════════════════════════════ */
|
||
|
||
/* ── 通用(与风格无关的尺寸/字体/网格令牌) ─────────────────────────────── */
|
||
:root{
|
||
--radius-8:8px; --radius-12:12px; --radius-16:16px; --radius-pill:999px;
|
||
--font-ui:system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;
|
||
--node-stroke-w:1;
|
||
--shadow-pop:0 10px 28px rgba(0,0,0,.18);
|
||
}
|
||
|
||
/* ── 风格 1 · base =**当前风格**(跟随系统深浅色) ─────────────────────── */
|
||
:root{ /* base · light */
|
||
--color-bg-page:#f6f8f5; --color-bg-surface:#ffffff; --color-bg-subtle:#eef2ee;
|
||
--color-text-primary:#12261a; --color-text-secondary:#4a5c50; --color-text-tertiary:#7a8a80;
|
||
--color-border-subtle:#e2e9e2; --color-border-default:#cdd8ce; --color-border-strong:#a9b8ab;
|
||
--color-accent:#256d37; --color-accent-subtle:#e6f1e8; --color-on-accent:#ffffff;
|
||
--color-success:#1f7a3d; --color-success-subtle:#e6f4ea;
|
||
--color-warning:#8a5a00; --color-warning-subtle:#fdf3e0;
|
||
--color-danger:#b3261e; --color-danger-subtle:#fdecea;
|
||
--color-focus-ring:#256d37;
|
||
--color-canvas-dot:transparent; --dot-size:22px;
|
||
}
|
||
@media (prefers-color-scheme: dark){
|
||
:root:not([data-style]), :root[data-style="base"]{ /* base · dark */
|
||
--color-bg-page:#0f1412; --color-bg-surface:#161d1a; --color-bg-subtle:#1d2622;
|
||
--color-text-primary:#e8f1ea; --color-text-secondary:#adbcb2; --color-text-tertiary:#7f8f85;
|
||
--color-border-subtle:#243029; --color-border-default:#2f3d36; --color-border-strong:#455349;
|
||
--color-accent:#7fd3a0; --color-accent-subtle:#14301f; --color-on-accent:#08200f;
|
||
--color-success:#6ede8a; --color-success-subtle:#12301c;
|
||
--color-warning:#ffcf7a; --color-warning-subtle:#33260f;
|
||
--color-danger:#ff8f85; --color-danger-subtle:#3a1613;
|
||
--color-focus-ring:#7fd3a0;
|
||
}}
|
||
|
||
/* ── 风格 2 · Archify 暗(令牌取自 archify 的 dark 主题) ────────────────── */
|
||
:root[data-style="archify-dark"]{
|
||
--color-bg-page:#020617; --color-bg-surface:#0f172a; --color-bg-subtle:#1e293b;
|
||
--color-text-primary:#ffffff; --color-text-secondary:#94a3b8; --color-text-tertiary:#64748b;
|
||
--color-border-subtle:#1e293b; --color-border-default:#334155; --color-border-strong:#475569;
|
||
--color-accent:#34d399; /* archify --arrow-emphasis */
|
||
--color-accent-subtle:rgba(52,211,153,0.14); --color-on-accent:#052e16;
|
||
--color-success:#34d399; --color-success-subtle:rgba(52,211,153,0.14); /* backend-stroke */
|
||
--color-warning:#fbbf24; --color-warning-subtle:rgba(251,191,36,0.14); /* cloud-stroke */
|
||
--color-danger:#fb7185; --color-danger-subtle:rgba(251,113,133,0.16); /* security-stroke*/
|
||
--color-focus-ring:#34d399;
|
||
--color-canvas-dot:rgba(148,163,184,0.16); --dot-size:22px; /* --canvas-dot */
|
||
--font-ui:ui-monospace,"Cascadia Mono","JetBrains Mono",Consolas,"Microsoft YaHei",monospace;
|
||
--node-stroke-w:1.4;
|
||
}
|
||
|
||
/* ── 风格 3 · Archify 亮(令牌取自 archify 的 light 主题) ───────────────── */
|
||
:root[data-style="archify-light"]{
|
||
--color-bg-page:#f4f5f7; --color-bg-surface:#ffffff; --color-bg-subtle:#f1f5f9;
|
||
--color-text-primary:#111827; --color-text-secondary:#5b6474; --color-text-tertiary:#9aa3b2;
|
||
--color-border-subtle:#e5e7eb; --color-border-default:#cbd5e1; --color-border-strong:#94a3b8;
|
||
--color-accent:#059669;
|
||
--color-accent-subtle:rgba(5,150,105,0.10); --color-on-accent:#ffffff;
|
||
--color-success:#059669; --color-success-subtle:rgba(52,211,153,0.20);
|
||
--color-warning:#d97706; --color-warning-subtle:rgba(251,191,36,0.20);
|
||
--color-danger:#e11d48; --color-danger-subtle:rgba(251,113,133,0.15);
|
||
--color-focus-ring:#059669;
|
||
--color-canvas-dot:#d9dee5; --dot-size:22px;
|
||
--font-ui:ui-monospace,"Cascadia Mono","JetBrains Mono",Consolas,"Microsoft YaHei",monospace;
|
||
--node-stroke-w:1.4;
|
||
}
|
||
*{box-sizing:border-box}
|
||
body{margin:0;background:var(--color-bg-page);color:var(--color-text-primary);
|
||
font-family:var(--font-ui);font-size:14px;line-height:1.6;font-weight:400}
|
||
a,button{font:inherit;color:inherit}
|
||
:focus-visible{outline:2px solid var(--color-focus-ring);outline-offset:2px}
|
||
|
||
header{padding:24px 32px 16px;border-bottom:1px solid var(--color-border-subtle);
|
||
display:flex;flex-wrap:wrap;gap:16px;align-items:baseline}
|
||
h1{margin:0;font-size:20px;font-weight:500;line-height:1.3}
|
||
.lede{color:var(--color-text-secondary);font-size:13px}
|
||
.spacer{flex:1}
|
||
.stamp{color:var(--color-text-tertiary);font-size:12px;font-variant-numeric:tabular-nums}
|
||
.hd-left{display:flex;flex-direction:column;gap:2px}
|
||
.hd-left .lede strong,.hd-left .lede b{font-weight:500;color:var(--color-text-secondary)}
|
||
.hd-right{display:flex;flex-direction:column;align-items:flex-end;gap:4px}
|
||
.lag{font-size:12px;font-variant-numeric:tabular-nums;color:var(--color-text-tertiary)}
|
||
.lag.is-old{color:var(--color-warning)}
|
||
.ro{font-size:11px;color:var(--color-text-tertiary)}
|
||
.warnbar{margin:0 0 24px;border:1px solid var(--color-warning);background:var(--color-warning-subtle);
|
||
color:var(--color-warning);border-radius:var(--radius-12);padding:12px 16px;font-size:13px}
|
||
|
||
/* 「本项目」身份卡 —— 🔴 看板顶部第一眼就要说清"这是哪个需求目标项目" */
|
||
.card.proj{border-color:var(--color-accent);background:var(--color-accent-subtle)}
|
||
.pill{display:inline-flex;align-items:center;padding:3px 12px;border-radius:var(--radius-pill);
|
||
font-size:12px;font-weight:500;color:var(--color-on-accent);background:var(--color-accent)}
|
||
.crit{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||
.critLead{color:var(--color-text-secondary);font-size:12px}
|
||
.crit .chip{background:var(--color-bg-surface)}
|
||
/* 🆕 2026-09-30 任务类别行:一个工作区里可能有多个类别 ⇒ 每类一枚 chip,chip 里带**该类的主会话**。
|
||
⚠️ 这里 ⛔ **不**覆盖 `.chip` 的底色 —— 要保留 `.is-warn`(无主会话那种)的警示色。 */
|
||
.topics{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||
.accRow{margin-top:10px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
||
.accRow .critLead{color:var(--color-text-secondary)}
|
||
.projGoal{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;font-size:17px;line-height:1.4}
|
||
.projGoal b{font-weight:600}
|
||
.accBar{height:6px;border-radius:3px;background:var(--color-border-tertiary);overflow:hidden;
|
||
width:220px;flex:0 0 auto}
|
||
.accBar i{display:block;height:100%;background:var(--color-success);transition:width .3s}
|
||
.accBar.is-badn{background:var(--color-danger-subtle)}
|
||
.accBar.is-badn i{background:var(--color-danger)}
|
||
.accNum{font-variant-numeric:tabular-nums;font-size:14px}
|
||
.accNum.is-ok{color:var(--color-success)}
|
||
.accNum.is-bad{color:var(--color-danger)}
|
||
/* ⛔ 2026-10-01 删:`.projAttr`("目标 id / 主会话 / 工作区 / 归属判据 压成一列"那一行)——
|
||
用户当天后续定性:「**看板要精简的展示重点有效的信息,不是写说明书**」⇒ 那一列整体**不进版面**,
|
||
全部追加到卡片右上角 `?`(见 `renderProject()` 末尾)。页面从此只剩三行:目标 / 完成情况 / 任务类别。 */
|
||
/* 🔴 2026-10-01 多目标 tab(用户:「把协作实时看板改为 **tab 支持多个目标**执行协作状态展示」)
|
||
⚠️ 一个目标时也画(只是只有一格)—— 让"这是多目标看板"这件事**自己说得出来**,
|
||
⛔ 不要等有第二个目标了才让人发现原来支持。 */
|
||
.tabs{display:flex;flex-wrap:wrap;gap:8px;align-items:stretch;margin:0 0 20px;
|
||
border-bottom:1px solid var(--color-border-subtle);padding-bottom:12px}
|
||
.tab{display:flex;flex-direction:column;gap:3px;min-width:180px;max-width:420px;text-align:left;
|
||
padding:9px 14px;border:1px solid var(--color-border-default);border-radius:var(--radius-12);
|
||
background:var(--color-bg-surface);color:var(--color-text-secondary);font:inherit;font-size:13px;
|
||
cursor:pointer;transition:border-color .15s,background .15s}
|
||
.tab:hover{border-color:var(--color-accent)}
|
||
.tab.is-on{border-color:var(--color-accent);background:var(--color-accent-subtle);
|
||
color:var(--color-text-primary)}
|
||
.tab .tt{display:flex;align-items:center;gap:6px;font-weight:500;color:var(--color-text-primary);
|
||
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
||
.tab .tsub{font-size:11px;color:var(--color-text-tertiary);white-space:nowrap;overflow:hidden;
|
||
text-overflow:ellipsis}
|
||
.tab .badge{flex:0 0 auto;padding:0 7px;border-radius:var(--radius-pill);font-size:11px;
|
||
border:1px solid var(--color-border-default);color:var(--color-text-tertiary)}
|
||
.tab .badge.is-ok{color:var(--color-success);border-color:var(--color-success);
|
||
background:var(--color-success-subtle)}
|
||
.tab .badge.is-bad{color:var(--color-danger);border-color:var(--color-danger);
|
||
background:var(--color-danger-subtle)}
|
||
.tab .badge.is-warn{color:var(--color-warning);border-color:var(--color-warning);
|
||
background:var(--color-warning-subtle)}
|
||
/* ⛔ 2026-10-01 删:`.tabHint`("只有一格/怎么加第二格"那段说明)—— 见 `renderTabs()` 里的说明。
|
||
tab 条**只放数据**;使用说明一律进各板块右上角 `?`。 */
|
||
td .role{display:inline-block;padding:1px 8px;border-radius:var(--radius-pill);font-size:11px;
|
||
border:1px solid var(--color-border-default);color:var(--color-text-tertiary);white-space:nowrap}
|
||
td .role.is-main{color:var(--color-accent);border-color:var(--color-accent);background:var(--color-accent-subtle)}
|
||
|
||
main{padding:24px 32px 48px;max-width:1440px;margin:0 auto}
|
||
section{margin:0 0 24px}
|
||
.card{background:var(--color-bg-surface);border:1px solid var(--color-border-subtle);
|
||
border-radius:var(--radius-16);padding:20px}
|
||
.card > h2{margin:0 0 16px;font-size:14px;font-weight:500;color:var(--color-text-secondary);padding-right:30px}
|
||
.card > h2 .hint,.hero h2 .hint{font-weight:400;color:var(--color-text-tertiary);font-size:12px;margin-left:8px}
|
||
|
||
.hero{background:var(--color-bg-surface);border:1px solid var(--color-border-subtle);
|
||
border-radius:var(--radius-16);padding:24px}
|
||
.hero h2{margin:0 0 16px;font-size:16px;font-weight:500;padding-right:30px}
|
||
.hero p{margin:0 0 16px;color:var(--color-text-tertiary);font-size:12px}
|
||
svg.arch{width:100%;height:auto;display:block}
|
||
/* 画布点阵(archify 的 --canvas-dot):base 风格为 transparent ⇒ 无点阵 */
|
||
#archWrap{background-image:radial-gradient(var(--color-canvas-dot) 1px, transparent 1px);
|
||
background-size:var(--dot-size) var(--dot-size);background-position:0 0;
|
||
border-radius:var(--radius-12)}
|
||
|
||
/* ── 右上角「风格切换」(用户 2026-09-30 要求;默认=当前风格) ───────────── */
|
||
.stylesw{display:inline-flex;border:1px solid var(--color-border-default);border-radius:var(--radius-pill);
|
||
overflow:hidden;background:var(--color-bg-surface)}
|
||
.stylesw button{border:0;background:transparent;color:var(--color-text-secondary);cursor:pointer;
|
||
padding:4px 12px;font-size:12px;line-height:1.6;font-family:var(--font-ui);white-space:nowrap}
|
||
.stylesw button + button{border-left:1px solid var(--color-border-subtle)}
|
||
.stylesw button:hover{background:var(--color-bg-subtle);color:var(--color-text-primary)}
|
||
.stylesw button.on{background:var(--color-accent);color:var(--color-on-accent);font-weight:500}
|
||
.stylesw button:focus-visible{outline:2px solid var(--color-focus-ring);outline-offset:-2px}
|
||
|
||
/* ── 🔴 各板块右上角「?」:小字描述收进这里,鼠标移上/键盘聚焦才显示 ─────────────
|
||
(用户 2026-09-30:「把各板块的小字描述放到各板块对应右上角 ?号图标中,鼠标移上去显示
|
||
(**只保留标题,主体,类别标签**这类信息)」)
|
||
⛔ 别再往版面里摊大段说明文字 —— 一律进 .qtip。 */
|
||
.card,.hero{position:relative}
|
||
.qh{position:absolute;top:12px;right:14px;z-index:6;width:20px;height:20px;padding:0;
|
||
border-radius:50%;border:1px solid var(--color-border-default);background:var(--color-bg-subtle);
|
||
color:var(--color-text-secondary);font-size:12px;font-weight:500;line-height:18px;text-align:center;
|
||
cursor:help;font-family:var(--font-ui);user-select:none}
|
||
.qh:hover,.qh:focus-visible{background:var(--color-accent);color:var(--color-on-accent);
|
||
border-color:var(--color-accent);outline:none}
|
||
.qh .qtip{position:absolute;top:26px;right:0;z-index:30;width:min(440px,78vw);padding:12px 14px;
|
||
border-radius:var(--radius-12);border:1px solid var(--color-border-default);
|
||
background:var(--color-bg-surface);box-shadow:var(--shadow-pop);
|
||
color:var(--color-text-secondary);font-size:12px;font-weight:400;line-height:1.7;text-align:left;
|
||
white-space:normal;cursor:default;opacity:0;visibility:hidden;transition:opacity .12s ease}
|
||
.qh:hover .qtip,.qh:focus-visible .qtip{opacity:1;visibility:visible}
|
||
@media (prefers-reduced-motion: reduce){.qh .qtip{transition:none}}
|
||
.qtip b{color:var(--color-text-primary);font-weight:500}
|
||
.qtip .kt{display:inline-block;padding:0 6px;border-radius:var(--radius-pill);
|
||
border:1px solid var(--color-border-default);color:var(--color-text-tertiary);font-size:11px}
|
||
/* 「协作架构」板块右上角:延迟算式 + 保留的快照时刻(固定可见,⛔ 不收进 ?) */
|
||
.herometa{position:absolute;top:12px;right:52px;text-align:right;line-height:1.5;
|
||
font-size:12px;font-variant-numeric:tabular-nums;color:var(--color-text-tertiary)}
|
||
.herometa > span{display:block;white-space:nowrap} /* ⚠️ 只作用于直接子元素:写 `.herometa span` 会把内层 #lag 也变成块 ⇒ "当前 3 秒"被拆两行 */
|
||
.herometa b{color:var(--color-text-primary);font-weight:500}
|
||
.herometa .sec{color:var(--color-text-secondary)}
|
||
.herometa.is-old .sec{color:var(--color-warning)}
|
||
@media(max-width:820px){.herometa{position:static;text-align:left;margin:0 0 12px}}
|
||
|
||
.cols-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px}
|
||
.cols-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px}
|
||
@media(max-width:1100px){.cols-3,.cols-2{grid-template-columns:1fr}}
|
||
|
||
.chip{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:var(--radius-pill);
|
||
font-size:12px;font-weight:400;border:1px solid var(--color-border-default);
|
||
color:var(--color-text-secondary);background:var(--color-bg-subtle)}
|
||
.chip.is-ok{color:var(--color-success);background:var(--color-success-subtle);border-color:var(--color-success)}
|
||
.chip.is-bad{color:var(--color-danger);background:var(--color-danger-subtle);border-color:var(--color-danger)}
|
||
.chip.is-warn{color:var(--color-warning);background:var(--color-warning-subtle);border-color:var(--color-warning)}
|
||
.rowchips{display:flex;flex-wrap:wrap;gap:8px}
|
||
|
||
table{width:100%;border-collapse:collapse;font-size:13px}
|
||
th,td{padding:8px 8px;border-bottom:1px solid var(--color-border-subtle);text-align:left;vertical-align:top}
|
||
th{font-weight:500;color:var(--color-text-tertiary);font-size:12px}
|
||
tr:last-child td{border-bottom:0}
|
||
td.num{font-variant-numeric:tabular-nums;color:var(--color-text-tertiary);font-size:12px;white-space:nowrap}
|
||
td .ellip{display:block;max-width:34ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||
/* 🔴 弱化说明(台账「任务类别」列里标旧值/项目卡里标类别来源):⛔ 只弱化,⛔ 不隐藏 */
|
||
.oldline{color:var(--color-text-tertiary);font-size:12px}
|
||
.warnTx{color:var(--color-warning);font-weight:600}
|
||
code{font-family:ui-monospace,Consolas,monospace;font-size:12px;color:var(--color-text-secondary)}
|
||
.empty{padding:16px 0;color:var(--color-text-tertiary);font-size:13px}
|
||
.empty strong{display:block;color:var(--color-text-secondary);font-weight:500;margin-bottom:4px}
|
||
.err{border-color:var(--color-danger);background:var(--color-danger-subtle);color:var(--color-danger);
|
||
border-radius:var(--radius-12);padding:16px;font-size:13px}
|
||
.skel{height:14px;border-radius:var(--radius-8);background:var(--color-bg-subtle);margin:8px 0}
|
||
|
||
.legend{display:flex;flex-wrap:wrap;gap:16px;color:var(--color-text-tertiary);font-size:12px;margin-top:8px}
|
||
.legend i{display:inline-block;width:12px;height:12px;border-radius:4px;margin-right:6px;
|
||
vertical-align:-1px;border:1px solid var(--color-border-default)}
|
||
/* 🔴 2026-10-01(用户:「**这段乱七八糟的 也精简 分段落描述**」)——
|
||
架构图下面的说明原先是**一整行** `… · … · …`(八九条串起来,读起来是一堵墙)。
|
||
⇒ 改成**分块 + 一行一条**:块 = 一个小标题(「当前」「<唤醒会话名>」),
|
||
块内每条独立成行、行首一个小点。⛔ 别再退回 `join(' · ')` 那种串行。 */
|
||
.archNotes{margin-top:12px;display:flex;flex-direction:column;gap:10px}
|
||
.archNotes .anBlock{display:flex;flex-direction:column;gap:3px}
|
||
.archNotes .anLead{color:var(--color-text-secondary);font-size:12px;font-weight:500}
|
||
.archNotes .anItem{color:var(--color-text-tertiary);font-size:12px;line-height:1.65;
|
||
padding-left:14px;position:relative}
|
||
.archNotes .anItem::before{content:'·';position:absolute;left:3px}
|
||
|
||
.n-title{fill:var(--color-text-primary);font-size:16px;font-weight:500}
|
||
.n-title-sm{fill:var(--color-text-primary);font-size:13px;font-weight:500}
|
||
.n-sub{fill:var(--color-text-tertiary);font-size:11px}
|
||
.n-stat{fill:var(--color-accent);font-size:13px;font-weight:500}
|
||
.n-ok{fill:var(--color-success);font-size:12px;font-weight:500}
|
||
.n-idle{fill:var(--color-text-tertiary);font-size:12px}
|
||
.n-warn{fill:var(--color-warning);font-size:12px;font-weight:500}
|
||
.n-bad{fill:var(--color-danger);font-size:12px;font-weight:500}
|
||
.e-label{fill:var(--color-text-tertiary);font-size:12px}
|
||
.node-bg{fill:var(--color-bg-surface);stroke:var(--color-border-default)}
|
||
.node-on{fill:var(--color-success-subtle);stroke:var(--color-success)}
|
||
.node-warn{fill:var(--color-warning-subtle);stroke:var(--color-warning)}
|
||
.node-off{fill:var(--color-danger-subtle);stroke:var(--color-danger);stroke-dasharray:4 3}
|
||
.node-base{fill:var(--color-bg-subtle);stroke:var(--color-border-strong);stroke-dasharray:5 4}
|
||
.node-bg,.node-on,.node-warn,.node-off,.node-base,.trig-node{stroke-width:var(--node-stroke-w)}
|
||
/* 🔴 唤醒脉冲会话(画在 ② 主会话**左边**)—— 用户 2026-10-01:先要「换个样式 和 协作会话区分开」,
|
||
随后**定性**「**唤醒脉冲会话,本质还是会话**」⇒ **形状改回会话同族的 `rect rx=14`**,
|
||
⛔ **不再用六边形** —— 形状已不是区分手段,**位置**才是(贴在主会话左边、⛔ 不进下方那一排)。
|
||
⇒ 只剩一条视觉标记:外圈一圈**虚线光晕** ⇒ 一眼看出「它靠**脉冲**被叫醒,不是一直在跑」。
|
||
⚠️ 状态色仍然照旧(`up` ⇒ 绿/否则黄)—— 位置管"它是谁",颜色管"它活着没"。 */
|
||
.trig-halo{fill:none;stroke-width:1;stroke-dasharray:5 4;opacity:.6}
|
||
.trig-halo.h-on{stroke:var(--color-success)}
|
||
.trig-halo.h-warn{stroke:var(--color-warning)}
|
||
/* 状态徽章的三组图形(⛔ 用图形不用字符 —— 字符要靠字体,Linux/无 CJK 字体时会变成方框) */
|
||
.trig-dot{fill:var(--color-success)}
|
||
.trig-pause{fill:var(--color-warning)}
|
||
.trig-idle-dot{fill:none;stroke:var(--color-text-tertiary);stroke-width:1.4}
|
||
/* 🔴 2026-09-30 用户:「有些线都看不到」——根因是灰线用了 `--color-border-strong`(**太浅**,
|
||
白底对比度仅约 1.9:1)+ 1.5px ⇒ 基本看不见;而唤醒→主会话/WorkBuddy→Hook/--once/--tick/
|
||
④收结果**全是灰线** ⇒ 整张图看着像"线没连全"。改用 `--color-text-tertiary`(约 3.5:1),
|
||
并与 `.e-label` 同色(标签本就在描述这条线)⇒ 灰色语义保留,可见性恢复。
|
||
⚠️ 注释里⛔ 不写具体色值 —— 有静态用例守着"组件里零硬编码色"。 */
|
||
.edge{stroke:var(--color-accent);stroke-width:1.5;fill:none}
|
||
.edge-grey{stroke:var(--color-text-tertiary);stroke-width:1.5;fill:none}
|
||
@media (prefers-reduced-motion: no-preference){
|
||
.live{stroke-dasharray:6 5;animation:flow 1.6s linear infinite}
|
||
@keyframes flow{to{stroke-dashoffset:-22}}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<div class="hd-left">
|
||
<h1>多会话协作 · 实时架构看板</h1>
|
||
</div>
|
||
<span class="spacer"></span>
|
||
<div class="hd-right">
|
||
<!-- 🔴 右上角风格切换(你现在的风格 = 第一项,⛔ 不删) -->
|
||
<div class="stylesw" id="stylesw" role="group" aria-label="看板风格">
|
||
<button type="button" data-style="base" title="保留的原风格(跟随系统深浅色)">当前风格</button>
|
||
<button type="button" data-style="archify-dark" title="参考 archify 的暗色令牌">Archify 暗</button>
|
||
<button type="button" data-style="archify-light" title="参考 archify 的亮色令牌">Archify 亮</button>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<main>
|
||
<div class="err" id="err" hidden></div>
|
||
<div class="warnbar" id="warnbar" hidden></div>
|
||
|
||
<!-- 🔴 多目标 tab(用户 2026-10-01:「把协作实时看板改为 tab 支持多个目标执行协作状态展示」)
|
||
⛔ 一格 = 一个目标;下面「本项目 / 协作架构 / 需求台账 / 会话明细」四块跟着**当前 tab** 变,
|
||
⛔ 「前置 / 队列通知 / 最近上报」是**工作区级**的,不随 tab 变(它们本来就不属于某一个目标)。 -->
|
||
<div class="tabs" id="tabs" hidden></div>
|
||
|
||
<section class="card proj" id="projCard">
|
||
<h2>本项目</h2>
|
||
<span class="qh" tabindex="0" role="button" aria-label="本项目板块说明">?<span class="qtip" id="projTip">
|
||
上面每一格 tab 是<b>一个需求目标</b>;这一块画的是<b>当前那格</b>的目标。<br>
|
||
跟着 tab 走的还有:<b>协作架构</b>、<b>需求台账</b>、<b>会话明细</b> —— 因为它们都按"任务类别"归属到某个目标。<br>
|
||
上面那一排「前置 / 队列通知 / 最近上报」是<b>整个工作区</b>的,⛔ 不随 tab 变。<br>
|
||
<b>怎么多一格</b>:在收件箱的 <code>goals/</code> 目录里放一份与 <code>goal.json</code> 同字段的
|
||
<code><目标id>.json</code>,刷新即多一格。只有一格时,就是本工作区只登记了一个目标。<br>
|
||
看板只读:后台隔一会儿生成一份快照,页面直接读缓存(间隔见「协作架构」那块的问题说明)。
|
||
</span></span>
|
||
<div id="proj"><div class="skel"></div></div>
|
||
</section>
|
||
|
||
<section class="hero">
|
||
<h2>协作架构<span class="hint" id="archHint">用户 · 会话 · 协作会话 · 程序 · WorkBuddy</span></h2>
|
||
<!-- 🔴 延迟与"保留的快照时刻"固定在板块右上角(用户 2026-09-30)—— ⛔ 别收进 ? 里 -->
|
||
<div class="herometa" id="herometa">
|
||
<span><b id="ts2">—</b></span>
|
||
<span class="sec">延迟 <span id="lag">—</span></span>
|
||
</div>
|
||
<span class="qh" tabindex="0" role="button" aria-label="协作架构板块说明">?<span class="qtip">
|
||
主会话<b>左边</b>那格是<b>唤醒会话</b>(名字由使用方给)—— 画成<b>圆角矩形 + 外圈虚线</b>,
|
||
它本身<b>就是一条会话</b>,而且是<b>独立会话</b>(自己一条,⛔ <b>不在主会话上处理</b>),
|
||
<b>随需求确定时才创建</b>;位置贴在主会话左边,⛔ 不进下方那一排。<br>
|
||
它框色/徽章=实时状态:<b>绿 ●</b> 在跑 · <b>黄 ‖</b> 停着 · <b>灰 ○</b> 还没建。<br>
|
||
图上每条线旁边都写了它是什么(① 派活 · ②③ 上报 · ④ 收结果 · 队列),<b>看线就行</b>。<br>
|
||
🔴 <b>① 派活</b>的特别之处:它是主会话建一条<b>自动化排期</b>、宿主到点开新会话 ——
|
||
这是<b>唯一能开新会话的通道</b>(钩子开不了会话)⇒ 所以"叫醒"只能喊、⛔ 不能自己开棒。<br>
|
||
主会话框里写着它<b>负责哪几个任务类别</b>;主会话<b>下方那一排</b>是<b>协作会话</b> —— 一格一条,横着排、有几条画几条,一条都没有时画个空框写明「暂无协作会话」。<br>
|
||
任务类别是<b>同一个工作区</b>里用<b>会话名称前缀</b>区分的(每个类别各有自己的主会话)。<br>
|
||
状态都是实读的,会话读 WorkBuddy 的库,程序读它自己留下的时间记录。哪一格停着不动,看框的颜色就知道。<br>
|
||
看板只读。后台每 <span id="iv">—</span> 秒生成一份快照,页面直接读缓存。
|
||
</span></span>
|
||
<div id="archWrap"><svg class="arch" id="arch" viewBox="0 20 1280 902" role="img"
|
||
aria-label="多会话协作架构:用户、主会话(框内标出它负责的任务类别;左侧挂唤醒会话(名字由使用方给)—— 它是一条**独立会话**,随需求确定时创建、不在主会话上处理,画成圆角矩形+外圈虚线,贴在主会话左边)、协作会话一排(有几条画几条,一条都没有时画一个空框写明暂无协作会话)、协作程序与上报、WorkBuddy 在特定时机起来的 Hook进程、WorkBuddy"></svg></div>
|
||
<div class="legend">
|
||
<span><i style="background:var(--color-success-subtle);border-color:var(--color-success)"></i>有会话在跑</span>
|
||
<span><i style="background:var(--color-warning-subtle);border-color:var(--color-warning)"></i>有件没人在跑</span>
|
||
<span><i style="background:var(--color-bg-surface);border-color:var(--color-border-default)"></i>空闲 / 件已全完</span>
|
||
<span><i style="background:var(--color-danger-subtle);border-color:var(--color-danger)"></i>已停</span>
|
||
<span><i style="background:var(--color-bg-subtle);border-color:var(--color-border-strong)"></i>WorkBuddy</span>
|
||
</div>
|
||
<!-- 🔴 2026-10-01:原来这里另有一行「① 派活…④ 收结果…队列…」的静态小字 ——
|
||
它和下面 `#archNote` 一起读就是一堵墙。⇒ **并进 `#archNote` 的「连线」块**(一行一条),
|
||
⛔ 不再单独占一行版面。 -->
|
||
<div class="archNotes" id="archNote"></div>
|
||
</section>
|
||
|
||
<section class="cols-3">
|
||
<!-- 🔴 本板块的名称/标签/内容**全部由使用方提供**(board_ext)—— 技能侧不写死任何项目的东西 -->
|
||
<div class="card" id="frontCard"><h2>前置</h2><span class="qh" tabindex="0" role="button" aria-label="前置板块说明">?<span class="qtip" id="frontTip">
|
||
看什么、叫什么名字,由**使用方**的看板扩展(`board_ext.py`)决定。<br>
|
||
组件名一律写成「系统 · 模块 · 功能名」,不用自造简称。<br>
|
||
状态只做探测:读组件自己留下的时间记录和日志,不发请求、不改任何东西。
|
||
</span></span><div id="front"><div class="skel"></div></div></div>
|
||
<div class="card"><h2>队列通知 / 握手<span class="hint">上报</span></h2><div id="notify"><div class="skel"></div></div></div>
|
||
<div class="card"><h2>最近上报<span class="hint">wakeups.jsonl</span></h2><div id="wake"><div class="skel"></div></div></div>
|
||
</section>
|
||
|
||
<section class="cols-2">
|
||
<div class="card"><h2>需求台账<span class="hint">tasks.json</span></h2><div id="tasks"><div class="skel"></div></div></div>
|
||
<div class="card"><h2>会话明细<span class="hint">本项目 · 含历史</span></h2> <span class="qh" tabindex="0" role="button" aria-label="会话明细板块说明">?<span class="qtip">
|
||
分工看架构图第三层。本表列本项目全部会话,含已完成的。
|
||
</span></span><div id="sess"><div class="skel"></div></div></div>
|
||
</section>
|
||
</main>
|
||
|
||
<script>
|
||
var D=null, LIVE=(location.protocol==='http:'||location.protocol==='https:');
|
||
/* 🔴 board.html 的签名(服务端随快照下发)。布局与样式都在本文件里,而页面只轮询 board.json
|
||
⇒ 改了看板**不刷新页面就看不到**(用户 2026-09-30 为此连问两次:"怎么没有变化呢")。
|
||
⇒ 发现签名变了就**自动重载一次**,⛔ 不再要求用户手动 F5。 */
|
||
var HTML_SIG=null;
|
||
/* 🔴 风格切换:只改 <html data-style>。颜色/字体/网格全走 CSS 令牌 ⇒ **不用重画图**。
|
||
⛔ `base` 是"保留的当前风格" —— 选它时把属性**摘掉**,好让它继续跟随系统深浅色。 */
|
||
var STYLE_KEY='dsh-board-style';
|
||
function currentStyle(){
|
||
var s=document.documentElement.getAttribute('data-style');
|
||
return (s==='archify-dark'||s==='archify-light')?s:'base';
|
||
}
|
||
function applyStyle(s){
|
||
if(s!=='archify-dark'&&s!=='archify-light') s='base';
|
||
if(s==='base') document.documentElement.removeAttribute('data-style');
|
||
else document.documentElement.setAttribute('data-style',s);
|
||
try{ localStorage.setItem(STYLE_KEY,s) }catch(e){}
|
||
var btns=document.querySelectorAll('#stylesw button');
|
||
for(var i=0;i<btns.length;i++){
|
||
var on=(btns[i].getAttribute('data-style')===s);
|
||
btns[i].classList.toggle('on',on);
|
||
btns[i].setAttribute('aria-pressed', on?'true':'false');
|
||
}
|
||
}
|
||
function initStyleSwitcher(){
|
||
var box=document.getElementById('stylesw');
|
||
if(!box) return;
|
||
box.addEventListener('click',function(ev){
|
||
var b=ev.target.closest?ev.target.closest('button[data-style]'):null;
|
||
if(b) applyStyle(b.getAttribute('data-style'));
|
||
});
|
||
/* 键盘:方向键在三个按钮间走 */
|
||
box.addEventListener('keydown',function(ev){
|
||
if(ev.key!=='ArrowLeft'&&ev.key!=='ArrowRight') return;
|
||
var order=['base','archify-dark','archify-light'], cur=order.indexOf(currentStyle());
|
||
cur=(cur+(ev.key==='ArrowRight'?1:order.length-1))%order.length;
|
||
applyStyle(order[cur]);
|
||
var b=box.querySelector('button[data-style="'+order[cur]+'"]'); if(b) b.focus();
|
||
ev.preventDefault();
|
||
});
|
||
applyStyle(currentStyle()); /* 把按钮高亮同步到实际生效的风格 */
|
||
}
|
||
function esc(s){return String(s==null?'':s).replace(/[&<>"]/g,function(c){
|
||
return {'&':'&','<':'<','>':'>','"':'"'}[c]})}
|
||
function ago(iso){
|
||
if(!iso)return '—';
|
||
var d=new Date(String(iso).replace(' ','T')); if(isNaN(d))return String(iso);
|
||
var s=Math.max(0,(Date.now()-d.getTime())/1000);
|
||
return s<90?Math.round(s)+' 秒前':(s<5400?Math.round(s/60)+' 分钟前':Math.round(s/3600)+' 小时前');
|
||
}
|
||
function chip(t,cls,title){return '<span class="chip '+(cls||'')+'"'
|
||
+(title?(' title="'+esc(title)+'"'):'')+'>'+esc(t)+'</span>'}
|
||
function fmtB(n){
|
||
if(n==null||isNaN(n))return '—';
|
||
n=Number(n);
|
||
return n<1024?(n+' B'):(n<1048576?((n/1024).toFixed(1)+' KB'):((n/1048576).toFixed(1)+' MB'));
|
||
}
|
||
function emptyBox(title,action){
|
||
return '<div class="empty"><strong>'+esc(title)+'</strong>'+esc(action||'')+'</div>'}
|
||
function tailOf(p){return String(p==null?'':p).replace(/\\/g,'/').replace(/\/+$/,'').split('/').pop()||'—'}
|
||
/* ⛔ 界面上不出现裸的 done/running/completed(用户看的是中文) */
|
||
function zhState(st){return {done:'已完成',running:'进行中',todo:'待办',pending:'待办',
|
||
blocked:'受阻',claimed:'已认领'}[String(st||'')]||(st||'—')}
|
||
function zhSess(st){return {working:'执行中',completed:'已完成',error:'出错',archived:'已归档',
|
||
idle:'空闲'}[String(st||'')]||(st||'—')}
|
||
|
||
/* 🔴 顶部「本项目」卡 —— 用户 2026-10-01 定调:
|
||
「为什么看板里不显示**项目目标**了,项目的**属性标签 规则 用一列描述**就好,
|
||
重点是看**目标需求 完成情况**」
|
||
⇒ 版面主角=**目标**(第一行)+**完成情况**(第二行,进度条 + 逐条点名未完项);
|
||
属性(目标 id / 默认主会话 / 工作区)与规则(归属判据)压成**最后那一列描述**,⛔ 不再各占一行抢戏。 */
|
||
function renderProject(d){
|
||
var p=d.project||{}, acc=(d.goal||{}).acceptance||{}, keys=Object.keys(acc).sort();
|
||
var src=p.topics_source||{}, mbt=p.main_by_topic||{}, tops=p.topics||[];
|
||
/* ── ① 目标(主角)── */
|
||
var goalHtml='<div class="projGoal"><span class="critLead">目标</span>'
|
||
+'<span class="pill">'+esc(p.short||'(未声明主题)')+'</span>'
|
||
+'<b>'+esc(p.title||'(未声明目标)')+'</b></div>';
|
||
/* ── ② 完成情况(第二主角)──
|
||
⚠️ 2026-09-30:**没有有效验收判据时 ⛔ 不许显示成"无验收记录"那种中性字样** ——
|
||
判据是"**有没有有效项**",不是"字典空不空"(只写 `_说明` 行时旧代码会当成"全过")。
|
||
⚠️ 非 pass 的**逐条点名** —— 只写"4/5 通过"看不出**还差什么**(用户要的是"看完成情况")。 */
|
||
var pass=keys.filter(function(k){return String(acc[k]).toLowerCase()==='pass'}).length;
|
||
var bad =keys.filter(function(k){return String(acc[k]).toLowerCase()!=='pass'});
|
||
var accHtml;
|
||
if(keys.length){
|
||
var pct=Math.round(pass/keys.length*100);
|
||
accHtml='<div class="accRow"><span class="critLead">完成情况</span>'
|
||
+'<span class="accBar'+(bad.length?' is-badn':'')+'" role="img" aria-label="验收通过 '+pass+' / '+keys.length+'">'
|
||
+'<i style="width:'+pct+'%"></i></span>'
|
||
+'<b class="accNum '+(bad.length?'is-bad':'is-ok')+'">'+pass+' / '+keys.length+' 通过</b>'
|
||
+(bad.length
|
||
? '<span class="oldline">未完:'+bad.map(function(k){
|
||
return esc(k)+'('+esc(acc[k])+')'}).join('、')+'</span>'
|
||
: '<span class="oldline">全部通过</span>')
|
||
+'</div>';
|
||
}else{
|
||
accHtml='<div class="accRow"><span class="critLead">完成情况</span>'
|
||
+chip('⚠️ 未声明验收判据(⛔ 不因此判完成)','is-warn')+'</div>';
|
||
}
|
||
/* ── ③ 任务类别:**保留成 chips**(⛔ 不压进描述列)——
|
||
因为它带"**该类的主会话是谁**",是**操作性信息**;而"属性/规则"只是背景。
|
||
🆕 任务类别(用户:「同一个工作区 多会话协作…**通过协作会话名称前缀**区分具体任务会话」)
|
||
⇒ 每个类别一枚 chip,且 chip 里带**该类的主会话** —— ⛔ 否则看不出"哪条管哪一类";
|
||
没有主会话的类别用警示色(那类的件投出去会被降级报告)。
|
||
🔴 2026-10-01 用户:「**看板要精简的展示重点有效的信息,不是写说明书**」
|
||
⇒ 括注("同一个工作区 · 按名称前缀区分")**删掉** —— 标签就叫「任务类别」,够了;
|
||
**来源细节**(什么时候、谁说明的)挪进 chip 的悬停提示,⛔ 不占版面。 */
|
||
var tp=tops.length
|
||
? tops.map(function(t){ var s=mbt[t]||'';
|
||
var _tip='来源:'+(src.kind==='declared'
|
||
? ('调用时在对话里说明'+(src.at?(' · '+src.at):'')+(src.by?(' · '+src.by):''))
|
||
: (src.kind==='fallback' ? '⛔ 还没在对话里说明过(暂按目标简称走单类别)' : '⛔ 目标与类别都还没声明'));
|
||
return '<span class="chip'+(s?'':' is-warn')+'" title="'+esc(_tip)+'">'+esc(t)
|
||
+(s?(' 主会话 <code>'+esc(s)+'</code>'):' ⚠️ 无主会话')+'</span>'}).join('')
|
||
: '<span class="chip is-warn">(未声明任务类别)</span>';
|
||
/* ⚠️ 「**没声明过**」必须留在**版面上**(同族红线:有却不说 / 没有却说有)——
|
||
但只有一句,⛔ 不再带"怎么说明"那种指导语(那是说明书,进 `?`)。 */
|
||
var tpLead = (src.kind==='fallback')
|
||
? '<span class="critLead">任务类别 <b class="warnTx">⚠ 未声明</b></span>'
|
||
: '<span class="critLead">任务类别</span>';
|
||
document.getElementById('proj').innerHTML = goalHtml + accHtml
|
||
+'<div class="topics">'+tpLead+tp+'</div>';
|
||
|
||
/* 🔴🔴 2026-10-01(用户两次:「**看板要精简的展示重点有效的信息,不是写说明书**」)
|
||
⇒ 版面上**只留三行**:目标 / 完成情况 / 任务类别。
|
||
其余全是**背景与规则**(目标 id、默认主会话、工作区、归属判据、类别来源的时间与人),
|
||
⛔ 不再占版面 —— 它们**一条都没丢**,全部追加进这一块右上角 `?` 里
|
||
(`?` 本来就是"小字说明"的正式落点;本文件 `frontTip` 早就是这个写法,此处同款)。
|
||
⚠️ 动态部分每轮**先还原 static 文案**再追加 ⇒ ⛔ 不会一轮轮叠加。 */
|
||
var tip=document.getElementById('projTip');
|
||
if(tip){
|
||
if(!tip.dataset.base) tip.dataset.base=tip.innerHTML; /* 首轮把静态文案存下来 */
|
||
tip.innerHTML=tip.dataset.base; /* 每轮先还原,防重复追加 */
|
||
var _crit=(p.criteria||[]).map(esc).join(' / ')||'—';
|
||
tip.innerHTML+='<br><br><b>这一格目标的依据</b>'
|
||
+'(目标 id <code>'+esc(p.id||'—')+'</code>'
|
||
+' · 默认主会话 <code>'+esc(p.main_sid8||'—')+'</code>'
|
||
+' · 工作区 <code>'+esc(tailOf(p.workspace))+'</code>)<br>'
|
||
+'<b>归属判据</b>(任一命中即算作本项目):'+esc(_crit)+'<br>'
|
||
+'<b>任务类别的来源</b>:'+(src.kind==='declared'
|
||
? ('调用时在对话里说明'+(src.at?(' · '+esc(src.at)):'')+(src.by?(' · '+esc(src.by)):''))
|
||
: (src.kind==='fallback'
|
||
? '⛔ 还没在对话里说明过 ⇒ 现按目标简称走单类别;说明方式:调用本技能时讲清「任务类别」'
|
||
: '⛔ 目标与类别都还没声明(调用本技能时在对话里说明)'));
|
||
}
|
||
}
|
||
|
||
/* 🔴🔴 架构图布局(**用户 2026-09-30 指定**)—— 纵向四层,从上到下:
|
||
① 用户 ② 主会话 ③ 协作会话1 … 协作会话n(**个数是活的**,按本项目在册会话渲染)
|
||
④ 协作程序 + 监督程序(并排) + 底部一条「宿主地基」带
|
||
每条边都走**空白带**(⛔ 不横穿节点框):
|
||
① 派活带 y=264 ② 上报带 y=404 ③ 上报走右侧竖井 x=1254 ④ 收结果走地基带上方
|
||
🔴 每格都带真状态:会话取宿主库 `status`,程序取状态戳 mtime(`board.json.runtime`)。 */
|
||
function fmtAge(m){
|
||
if(m==null)return '—';
|
||
m=Number(m);
|
||
return m<1?'刚刚':(m<90?Math.round(m)+' 分钟前':(m<2880?Math.round(m/60*10)/10+' 小时前':Math.round(m/1440*10)/10+' 天前'));
|
||
}
|
||
/* ⚠️ 中日韩字符 ≈ 1em(13px 字号 ⇒ 约 13px/字),ASCII 约 6.8px。
|
||
⛔ 别再用"每字符 8.4px"那种估法 —— 会把标题算长一倍、**文字溢出框外**(第一版就是这个毛病)。
|
||
下面按**逐字符实际宽度**累加,比"数够 N 个字符就切"稳得多。 */
|
||
function textW(s, cjk, ascii){
|
||
s=String(s==null?'':s); var w=0;
|
||
for(var i=0;i<s.length;i++){ w += (s.charCodeAt(i) > 0x2E80 ? cjk : ascii); }
|
||
return w;
|
||
}
|
||
function fitText(s, maxPx, cjk, ascii){
|
||
s=String(s==null?'':s);
|
||
if(textW(s,cjk,ascii)<=maxPx) return s;
|
||
var out='';
|
||
for(var i=0;i<s.length;i++){
|
||
var c=s.charAt(i), w=(s.charCodeAt(i) > 0x2E80 ? cjk : ascii);
|
||
if(textW(out,cjk,ascii)+w+ascii>maxPx) return out+'…';
|
||
out+=c;
|
||
}
|
||
return out;
|
||
}
|
||
/* 两行版:先按整宽切一行,剩下的切第二行(用于"标题较长、框较窄"的格子) */
|
||
function fitTwo(t, maxPx, cjk, ascii){
|
||
t=String(t==null?'':t);
|
||
if(textW(t,cjk,ascii)<=maxPx) return [t];
|
||
var cut=0, w=0;
|
||
for(var i=0;i<t.length;i++){
|
||
var cw=(t.charCodeAt(i)>0x2E80?cjk:ascii);
|
||
if(w+cw>maxPx) break;
|
||
w+=cw; cut=i+1;
|
||
}
|
||
return [t.slice(0,cut), fitText(t.slice(cut), maxPx, cjk, ascii)];
|
||
}
|
||
|
||
function drawArch(d){
|
||
var S=d.sessions||[], rt=d.runtime||{}, LB=d.labor||[];
|
||
/* 🔴 2026-10-01(用户报「**创建了协作会话 但是 看板中没有展示**」):
|
||
同工作区、**但按命名判据没归入本项目**的会话。⛔ 它们**不是**本项目会话
|
||
(不算进 `S`、不进第三层、不进分工板);**但必须说出口** ——
|
||
否则一条真在跑的棒会「凭空消失」,读者只会以为机制没派活(同族红线:读到了却不说)。
|
||
典型成因:棒名写成 `[协作]-<昵称>-…`(漏方括号、或用 `goal.short` 而非 `topics`)⇒
|
||
`parse_session_name()` / `in_project()` 双双认不出。 */
|
||
var UNREC=d.sessions_unrecognized||[];
|
||
/* 🆕 2026-09-30:**每个任务类别各自的主会话**(`{类别: sid8}`)—— 第三层按类别渲染要用它 */
|
||
var MBT=(d.project||{}).main_by_topic||{};
|
||
var main=(S.filter(function(x){return x.role==='主会话'})[0])||null;
|
||
var runningS=S.filter(function(x){return x.status==='working'});
|
||
var works=S.filter(function(x){return x.role!=='主会话'});
|
||
/* 🔴🔴 2026-10-01 用户定 —— **这一排第三次换语义**(前两次的来历见 §0.5.6):
|
||
「**主会话 下面 那一排只放协作会话**,横着排 有几个放几个,当前没有对应协作会话 就空着。
|
||
**唤醒机制如果是主会话的 事就放到主会话框里去**」
|
||
⇒ 这一排 = **协作会话**(本项目里 `role !== '主会话'` 的会话),⛔ 不再按"任务类别"铺格:
|
||
· 🔴 **任务类别是主会话自己的事** ⇒ 搬进 R2 主会话框,一行「负责类别:…」;
|
||
· 🔴 台账里的**旧线(legacy)** 也⛔ 不占这一排 ⇒ 汇总改到**图外说明**里摊开说(⛔ 不静默丢)。
|
||
⚠️ 仍算 `_lg`:**图外说明**要报"几条线、多少件"(本线红线:读到了就得说出口)。 */
|
||
var _lg=LB.filter(function(x){return x.kind==='legacy'});
|
||
var _fold=null;
|
||
if(_lg.length){
|
||
var _ft=0,_fd=0,_fo=0,_fr=[];
|
||
_lg.forEach(function(x){_ft+=x.total||0;_fd+=x.done||0;_fo+=x.open||0;
|
||
_fr=_fr.concat(x.running||[])});
|
||
_fold={total:_ft,done:_fd,open:_fo,running:_fr};
|
||
}
|
||
/* 这一排画谁:**协作会话**。用户要"有几个放几个" ⇒ 放宽上限(MAXW 只防极端,超了在图外点名)。
|
||
⛔ 不再按宽度截断类别 —— 类别不在这排了。 */
|
||
var MAXW=8;
|
||
var cells=works.slice(0,MAXW), moreW=Math.max(0,works.length-MAXW);
|
||
var TOPS=(d.project||{}).topics||[];
|
||
var working=S.filter(function(x){return x.status==='working'}).length;
|
||
var o=[];
|
||
|
||
/* 🔴 两类"让主会话动起来"的外部来源,⛔ 别混为一谈:
|
||
· **Hook进程**(R5 那格)—— **事件驱动**:WorkBuddy 在钩子时机起一个短命子进程,跑完即退;
|
||
· **唤醒 等**(画在 ② 主会话**左边**)—— **时间驱动**:主会话建的那个定时任务。
|
||
用户 2026-09-30 三连:「把心跳的节点也放到看板协作架构图中」→「把心跳 图框做小一点 放到
|
||
主会话左边就行 本来就是主会话创建的」→「把心跳 改为 唤醒 里面是每5分钟核对目标状态」。
|
||
⚠️ 它们**不由技能侧写死**(⛔ 技能里不出现任何项目的组件名)——
|
||
由**使用方**经 `front.triggers` 给,形状:
|
||
`[{name, detail, detail2, label, up, edge, tip}]`。空 ⇒ 主会话左边不画格子。 */
|
||
var TG=(d.front&&d.front.triggers)||[];
|
||
/* 🔴 行距(用户 2026-09-30:「摆不下就**加高**些,线都没连全 有些线都看不到」)
|
||
⇒ **只加高、⛔ 不加宽**:`svg.arch{width:100%}` ⇒ 缩放比由**宽度**定 ⇒ 加高**不会**把字变小,
|
||
只是每层之间更宽敞(走线带、拐点、标签都放得下)。 */
|
||
/* 🔴 2026-10-01 再改:R2↔R3 的间隔 **28 → 68**(用户:「增加 主会话框图 和 下方协作会话框图
|
||
区域的距离」)—— 原来那条带只有 28px,而 R1↔R2 是 60、R3↔R4 是 66 ⇒ 唯独主会话到协作会话
|
||
那条**挤得连 ① 标签都快贴上框**:主会话往下一排放射线 + ① 标签都要在这条带里落位。
|
||
⇒ R3 起整体下移 40(`viewBox` 同步加高 40,⛔ 否则底部地基带会被裁)。 */
|
||
var R1={y:36,h:80}, R2={y:176,h:112}, R3={y:356,h:134}, R4={y:556,h:112},
|
||
R5={y:702,h:96}, RB={y:834,h:56}; /* R5 = Hook进程(时间驱动源另画在 主会话 左侧) */
|
||
/* ⚠️ R2 由 84 → **112**(2026-10-01):主会话框多一行「**负责类别:…**」——
|
||
用户:「**唤醒机制如果是主会话的 事就放到主会话框里去**」⇒ 类别从第三层搬进主会话框。 */
|
||
var CX=640, SX0=60, SX1=1220, CH=1254; // 会话行 60..1220;右侧竖井 1254
|
||
var B1=(R2.y+R2.h+R3.y)/2, B2=(R3.y+R3.h+R4.y)/2; // ① 派活带 / ② 上报带(自动取中,⛔ 别写死)
|
||
var B3=(R4.y+R4.h+R5.y)/2; // R4↔R5 之间的走线带(⛔ 别写死 554)
|
||
var PX=280, PW=300, UX=700; // 协作程序 280..580;监督程序 700..1000
|
||
|
||
function rect(x,y,w,h,tone){
|
||
var m={on:'node-on',off:'node-off',base:'node-base'};
|
||
o.push('<rect class="'+(m[tone]||'node-bg')+'" x="'+x+'" y="'+y+'" width="'+w+'" height="'+h+'" rx="14"/>');
|
||
}
|
||
function tx(x,y,t,cls,anchor){
|
||
o.push('<text class="'+(cls||'n-sub')+'" x="'+x+'" y="'+y+'"'+(anchor?' text-anchor="'+anchor+'"':'')+'>'+esc(t)+'</text>');
|
||
}
|
||
function edge(dstr,grey,noArrow){
|
||
o.push('<path class="edge'+(grey?'-grey':'')+(noArrow?'':' live')+'" d="'+dstr+'"'
|
||
+(noArrow?'':' marker-end="url(#ar)"')+'/>');
|
||
}
|
||
function lbl(x,y,t,anchor){tx(x,y,t,'e-label',anchor||'middle')}
|
||
function toneOf(st){return st==='working'?'n-ok':(st==='completed'?'n-idle':'n-warn')}
|
||
function wordOf(st){return st==='working'?'执行中':(st==='completed'?'已完成':(st||'—'))}
|
||
|
||
// ── ① 用户 —— 🔴 写**职责 + 可执行动作**(用户 2026-09-30 要求;⛔ 不写"你 · 一张看板"这种空话)
|
||
var w1=300, x1=CX-w1/2, in1=w1-40;
|
||
rect(x1,R1.y,w1,R1.h,'bg');
|
||
tx(x1+20,R1.y+28,'用户','n-title');
|
||
tx(x1+20,R1.y+50, fitText('职责:制定目标 · 调整方向 · 做决策', in1, 11, 6), 'n-sub');
|
||
tx(x1+20,R1.y+70, fitText('可做:看板 · 放行授权 · 改目标 · 喊停', in1, 11, 6), 'n-sub');
|
||
|
||
// ── ② 主会话 —— 🔴 显示**会话名**(用户 2026-09-30 要求),⛔ 不只给一个 id
|
||
var w2=380, x2=CX-w2/2, in2=w2-40;
|
||
var mOn=!!(main&&main.status==='working');
|
||
rect(x2,R2.y,w2,R2.h, main?(mOn?'on':'bg'):'bg');
|
||
tx(x2+20,R2.y+26,'主会话','n-title');
|
||
tx(x2+20,R2.y+50, main?fitText(main.title, in2, 13, 7):'(未声明主会话)', 'n-title-sm');
|
||
tx(x2+20,R2.y+70, main?fitText('判断 + 派活 · '+wordOf(main.status)+' · '+main.id8, in2, 11, 6)
|
||
:'——', main?(mOn?'n-ok':'n-idle'):'n-idle');
|
||
/* 🔴 2026-10-01:**它负责哪几个任务类别** —— 用户:「**唤醒机制如果是主会话的 事就放到主会话框里去**」
|
||
⇒ 类别不再占第三层,落在这里。⚠️ 按 `main_by_topic` **反查**(⛔ 别假设"只有一类"),
|
||
也⛔ 不因为查不到就闭嘴 —— 没说明过时明说「未在对话里说明」(同族红线)。 */
|
||
var _myT=TOPS.filter(function(t){return main && String(MBT[t]||'')===String(main.id8)});
|
||
tx(x2+20,R2.y+92, _myT.length? fitText('负责类别:'+_myT.join('、'), in2, 11, 6)
|
||
: '负责类别:—(未在对话里说明)', _myT.length?'n-sub':'n-warn');
|
||
|
||
// 用户 → 主会话
|
||
edge('M'+CX+' '+(R1.y+R1.h)+' L'+CX+' '+R2.y);
|
||
lbl(CX+56, (R1.y+R1.h+R2.y)/2, '发消息 · 看板');
|
||
|
||
// ── ②′ 主会话**创建**的时间驱动触发源(用户 2026-09-30:「把心跳 图框做小一点 放到**主会话左边**
|
||
// 就行,本来就是主会话创建的」)⇒ 小框贴在主会话左侧,一条「创建」箭头从主会话指过去。
|
||
// 数据仍由**使用方**给(`front.triggers`),⛔ 技能里不写死"唤醒"这个名字。
|
||
// ⚠️ 左边界 SX0 之外放不下时就只画**最靠前的一个**,其余在图外注释里点名(⛔ 不硬塞、不压连线)。
|
||
if(TG.length){
|
||
/* 🔴 形状=**会话同族的圆角矩形**(2026-10-01 二次改)—— 用户定性:
|
||
「**唤醒脉冲会话,本质还是会话**」⇒ 它 ⛔ 不是"调度配置/触发器",是**一条会话**
|
||
⇒ 形状回到 `rect rx=14`(与主会话/协作会话同族),**靠位置区分**(贴在主会话左边、
|
||
⛔ 不进下方协作会话排)。**外圈虚线保留** —— 表示"它靠脉冲被叫醒、不是一直在跑"。
|
||
(前一版画成**六边形**,理由是用户当时要求"和协作会话区分开";定性变了故改回。)
|
||
🔴 颜色与徽章管**运行状态** —— 用户:「要**实时展示运行状态**,包括**图框样式要能体现**」:
|
||
`t.state`:`running` ⇒ 绿 + ● 运行中 / `paused` ⇒ 黄 + ‖ 已暂停 / `none` ⇒ 灰 + ○ 未登记。
|
||
⚠️ 缺 `state` 时回落 `up`(向后兼容:老的使用方只给 `up` 也能画)。
|
||
⛔ 技能侧**不写死**这一格叫什么 —— 名字由使用方经 `front.triggers[].name` 给。 */
|
||
var TGAP=64, TBH=78, TBR=x2-TGAP; // 右边界 = 主会话左边界 − 连接带宽度
|
||
var TBW=236, shown=TG;
|
||
if(TG.length>1 || TBR-TBW<SX0){ TBW=Math.max(150, TBR-SX0); shown=TG.slice(0,1); }
|
||
var TBY=R2.y, yc0=TBY+TBH/2; // 🔴 2026-10-01 起**不再是六边形**(见上)
|
||
shown.forEach(function(t){
|
||
var x=TBR-TBW, up=!!t.up, txi=x+14, inner=TBW-28;
|
||
var st=(t.state||(up?'running':'paused'));
|
||
var stWord=(st==='running')?'运行中':((st==='none')?'未登记':'已暂停');
|
||
var stTone=(st==='running')?'n-ok':((st==='none')?'n-idle':'n-warn');
|
||
var bx=x+TBW/2;
|
||
// 外圈虚线光晕 —— 颜色跟状态走("脉冲式"的观感)
|
||
o.push('<rect class="trig-halo '+(up?'h-on':'h-warn')+'" x="'+(x-5)+'" y="'+(TBY-5)+'" width="'+(TBW+10)+'" height="'+(TBH+10)+'" rx="19"/>');
|
||
// 本体:**会话同族的圆角矩形**(⛔ 不再是六边形 —— 它本质是一条会话)
|
||
o.push('<rect class="'+(up?'node-on':'node-warn')+' trig-node" x="'+x+'" y="'+TBY+'" width="'+TBW+'" height="'+TBH+'" rx="14">'
|
||
+(t.tip?('<title>'+esc(t.tip)+'</title>'):'')+'</rect>');
|
||
// 状态徽章(本体上方)—— ⛔ 图形而非字符(字符要靠字体,缺 CJK 字体会变方框)
|
||
if(st==='running'){ o.push('<circle class="trig-dot" cx="'+(bx-21)+'" cy="'+(TBY-15)+'" r="4.2"/>'); }
|
||
else if(st==='none'){ o.push('<circle class="trig-idle-dot" cx="'+(bx-21)+'" cy="'+(TBY-15)+'" r="4.2"/>'); }
|
||
else{
|
||
o.push('<rect class="trig-pause" x="'+(bx-25)+'" y="'+(TBY-20)+'" width="3.6" height="10" rx="1.2"/>');
|
||
o.push('<rect class="trig-pause" x="'+(bx-19.4)+'" y="'+(TBY-20)+'" width="3.6" height="10" rx="1.2"/>');
|
||
}
|
||
tx(bx-12, TBY-11, stWord, stTone, 'start');
|
||
tx(txi,TBY+22, fitText(t.name||'(未命名触发源)', inner, 13, 7), 'n-title-sm');
|
||
tx(txi,TBY+42, fitText(t.detail||'', inner, 11, 6), 'n-sub');
|
||
tx(txi,TBY+58, fitText(t.detail2||'', inner, 11, 6), 'n-sub');
|
||
tx(txi,TBY+73, fitText(t.label||'状态未知', inner, 10, 5.6), up?'n-ok':'n-warn');
|
||
// 它 → 主会话 —— 🔴 用户 2026-09-30:「唤醒和主会话 **强调的是唤醒的动作**,不是创建的动作」
|
||
// ⇒ 箭头由它**指向主会话**("它把主会话叫起来"),标签也用「唤醒」;
|
||
// "主会话创建的"只作**来历**写在框里一行,⛔ 不再画成箭头。
|
||
edge('M'+(x+TBW)+' '+yc0+' L'+x2+' '+yc0);
|
||
if(t.edge) lbl((x2+x+TBW)/2, yc0-8, fitText(t.edge, TGAP-6, 11, 6), 'middle');
|
||
});
|
||
}
|
||
|
||
// ── ③ **协作会话**(🔴 2026-10-01 用户定:「**这一排只放协作会话**,横着排 有几个放几个,
|
||
// 当前没有对应协作会话 就空着」+「放一个空的框 说明 暂无协作会话」)——
|
||
// ⚠️ 这一排**换过两次语义**(按线 → 按任务类别 → 现在是**按协作会话**):
|
||
// · **任务类别是主会话自己的事** ⇒ 已搬进 R2(一行「负责类别:…」);
|
||
// · 台账旧线(legacy)的汇总 ⇒ 已搬到**图外说明**(⛔ 不静默丢)。
|
||
// 每格=**一条协作会话**;格内:会话名 / 状态·id / 所属类别 / 多久没动。
|
||
var cx=[], n=cells.length;
|
||
if(n===0){
|
||
/* 🔴 **空态也要画框**(用户 2026-10-01:「**放一个空的框 说明 暂无协作会话**」)——
|
||
空 ≠ 坏了。⛔ 不许因为"没内容"就把这一层省略掉(读者会以为图缺了一块)。 */
|
||
o.push('<rect class="node-bg" x="'+(CX-240)+'" y="'+R3.y+'" width="480" height="'+R3.h+'" rx="14"'
|
||
+' stroke-dasharray="6 5"/>');
|
||
tx(CX,R3.y+56,'暂无协作会话','n-idle','middle');
|
||
tx(CX,R3.y+82,'主会话派活时创建','n-sub','middle');
|
||
/* ⚠️ **两条通道的标签在空态也要画**(2026-10-01 修)—— 原来只画了 ① 的,
|
||
② 的标签在 `else` 分支里 ⇒ 没有协作会话时图上**只剩 ①**,看不出还有 ② 上报这条回程。
|
||
⛔ 空态 ≠ 这两条通道不存在;只是当前没有会话可以连。 */
|
||
lbl(CX,B1,'① 派活 · 自动化排期'); /* 🔴 2026-10-01:去掉 -8 偏移 ⇒ 标签**正中**落在间隔带里 */
|
||
lbl(CX,B2-8,'② 上报 · --report');
|
||
}else{
|
||
var gap=(n>5)?16:24, avail=SX1-SX0;
|
||
var w=Math.min(340, Math.max(150, (avail-(n-1)*gap)/n));
|
||
var total=n*w+(n-1)*gap, sx=CX-total/2;
|
||
cells.forEach(function(W){
|
||
var x=sx+cx.length*(w+gap); cx.push(x+w/2);
|
||
var inner=w-28;
|
||
var tone=(W.status==='working')?'on':((W.status==='completed')?'bg':'warn');
|
||
rect(x,R3.y,w,R3.h,tone);
|
||
tx(x+14,R3.y+24, fitText(W.title||'(未命名会话)', inner, 13, 7), 'n-title-sm');
|
||
tx(x+14,R3.y+50, fitText(wordOf(W.status)+' · '+W.id8, inner, 11, 6), toneOf(W.status));
|
||
/* ⚠️ 类别前缀是**会话标题里读出来的**(`session.topic`)—— 读不到就**说出来**
|
||
(⛔ 不许让格子看着正常、实际没有类别 ⇒ 那是假情报)。 */
|
||
tx(x+14,R3.y+72, W.topic? fitText('类别:'+W.topic, inner, 11, 6)
|
||
: '(标题里没读类别前缀)', W.topic?'n-sub':'n-warn');
|
||
/* ⚠️ `fmtAge()` 的返回值**自带"前"字**(`4 分钟前`)⇒ ⛔ 别再加一个"前"
|
||
(2026-10-01 实测拼成过「4 分钟前前有活动」)。 */
|
||
tx(x+14,R3.y+94, fmtAge(W.age_min)+'有活动', 'n-sub');
|
||
tx(x+14,R3.y+116, '它承接的件见下方「需求台账」', 'n-sub');
|
||
});
|
||
// ① 派活:主会话 → 各条协作会话(走中间带)
|
||
edge('M'+CX+' '+(R2.y+R2.h)+' L'+CX+' '+B1, null, true);
|
||
edge('M'+Math.min.apply(null,cx)+' '+B1+' L'+Math.max.apply(null,cx)+' '+B1, null, true);
|
||
cx.forEach(function(c){ edge('M'+c+' '+B1+' L'+c+' '+R3.y) });
|
||
lbl(Math.max.apply(null,cx), B1, '① 派活 · 自动化排期', 'end'); /* 同上:正中落在间隔带 */
|
||
// ② 上报:各条协作会话 → 协作程序(走下方带)
|
||
cx.forEach(function(c){ edge('M'+c+' '+(R3.y+R3.h)+' L'+c+' '+B2, null, true) });
|
||
edge('M'+Math.min.apply(null,cx.concat([PX+PW/2]))+' '+B2+' L'+Math.max.apply(null,cx.concat([PX+PW/2]))+' '+B2, null, true);
|
||
edge('M'+(PX+PW/2)+' '+B2+' L'+(PX+PW/2)+' '+R4.y);
|
||
lbl(Math.max.apply(null,cx), B2-8, '② 上报 · --report', 'end');
|
||
}
|
||
if(moreW>0){
|
||
tx(SX1-6, R3.y+R3.h+16, '另有 '+moreW+' 条协作会话未画(图上最多 '+MAXW+' 条)', 'n-warn', 'end');
|
||
}
|
||
|
||
// ── ④ 协作程序 + 监督程序
|
||
var pr=rt.prog||{}, gu=rt.deliver||{}, q=d.queue||{}, qb=q.by||{}; /* gu = 上报(旧名「监督程序」,再早叫「投递」) */
|
||
// 协作程序:🔴 带**队列计数**(用户 2026-09-30:「协作程序 也要显示当前待验收队列数量」)
|
||
o.push('<rect class="'+(pr.up?'node-on':'node-off')+'" x="'+PX+'" y="'+R4.y+'" width="'+PW+'" height="'+R4.h+'" rx="14">'
|
||
+'<title>队列 = 需求台账 tasks.json 四态:待执行 '+((qb.pending)||0)+' / 执行中 '+((qb.running)||0)
|
||
+' / 已完成 '+((qb.done)||0)+' / 有阻碍 '+((qb.blocked)||0)+'。机制里没有「待验收」这个态。'
|
||
+'<br>'+esc(pr.how||'')+'</title></rect>');
|
||
tx(PX+18,R4.y+26,'协作程序','n-title');
|
||
tx(PX+18,R4.y+50, '队列 '+((q.total)||0)+' 件 · 已完成 '+((qb.done)||0)+' · 未完 '+((q.open)||0)
|
||
+(((qb.blocked)||0)?(' · 受阻 '+qb.blocked):''), ((q.open)||0)?'n-warn':'n-idle');
|
||
/* 🔴 2026-10-01:补一句**它怎么起来的** —— 实测 `runtime.prog.by = "钩子 --tick"`
|
||
⇒ 协作程序和右边的上报一样是**钩子按需唤起**的,⛔ 不是常驻进程。
|
||
原句只写「在线」,会被读成"有个服务一直开着"(同族:让格子看着正常却与事实不符)。 */
|
||
tx(PX+18,R4.y+70, fitText('需求台账 / 队列 · 只维护不上报 · 按需唤起', PW-36, 11, 6), 'n-sub');
|
||
tx(PX+18,R4.y+92, pr.label||'状态未知', pr.up?'n-ok':'n-bad');
|
||
|
||
// 监督程序:🔴 停着就把**停因**写在格子里(⛔ 不让用户只能猜"为什么停了")
|
||
var greason=String(gu.reason||'');
|
||
o.push('<rect class="'+(gu.up?'node-on':'node-warn')+'" x="'+UX+'" y="'+R4.y
|
||
+'" width="'+PW+'" height="'+R4.h+'" rx="14">'+(greason?('<title>'+esc(greason)+'</title>'):'')+'</rect>');
|
||
/* 🔴 这一格以前叫「监督程序」、并且显示一个"已停"的红框 ⇒ 让人以为有东西坏了。
|
||
实际上**它不是常驻进程**,是宿主钩子唤起的一次性 `--tick` ⇒ 没有"启动/停止"这回事。 */
|
||
tx(UX+18,R4.y+26,'上报','n-title');
|
||
tx(UX+18,R4.y+50, fitText(gu.label||'状态未知', PW-36, 12, 6.5), gu.up?'n-ok':'n-warn');
|
||
tx(UX+18,R4.y+70,'WorkBuddy 用时才喊它 · 不是一直开着','n-sub');
|
||
tx(UX+18,R4.y+92, fitText('它平时不占进程 ⇒ 没有"启动/停止"这回事', PW-36, 11, 6), 'n-sub');
|
||
|
||
// 队列:协作程序 → 监督程序
|
||
edge('M'+(PX+PW)+' '+(R4.y+R4.h/2)+' L'+UX+' '+(R4.y+R4.h/2));
|
||
lbl((PX+PW+UX)/2, R4.y+R4.h/2-10, '队列');
|
||
|
||
// ③ 上报 → 主会话(右侧竖井,⛔ 不横穿会话行)
|
||
var y4=R4.y+R4.h/2;
|
||
edge('M'+(UX+PW)+' '+y4+' L'+CH+' '+y4+' L'+CH+' '+(R2.y+R2.h/2)+' L'+(x2+w2)+' '+(R2.y+R2.h/2));
|
||
lbl(1050, R2.y+R2.h/2-10, '③ 上报 · reply/唤醒', 'middle');
|
||
|
||
// ── ⑤ 🔴 **Hook进程** —— 用户 2026-09-30 指出「图里是不是还缺少关键节点,比如宿主起的子进程」。
|
||
// 它**才是上报的真正执行者**:WorkBuddy 在钩子时机起一个短命子进程,跑完即退 ⇒ **事件驱动**。
|
||
// ⚠️ 时间驱动的那些(如「唤醒」)**不在这一行**,画在 ② 主会话左边(见上)—— 两者不是一回事:
|
||
// 整机没活动时 Hook 不会被唤起,「唤醒」补的正是这个缺口。
|
||
var hasTG=TG.length>0;
|
||
/* 🔴 2026-10-01(用户:「**Hook进程 框图宽度往右边延长一些,让 tick 那条线能竖直摆放**」)
|
||
⇒ 框宽 480 → **490**(右边界 880 → 890)。
|
||
⚠️ 加宽**不是为了好看**,是为了让 `--tick` 的出口点能落在**框顶边之内**(见下方 TICKX)——
|
||
⛔ 改宽度时别把它缩回去,否则出口点会跑到框外、线头悬空。 */
|
||
var hkX=400, hkW=490;
|
||
rect(hkX,R5.y,hkW,R5.h,'on');
|
||
tx(hkX+20,R5.y+26,'Hook进程','n-title');
|
||
tx(hkX+20,R5.y+48, fitText('WorkBuddy 在特定时机喊它 · 干完就退', hkW-36, 11, 6), 'n-sub');
|
||
tx(hkX+20,R5.y+66, fitText('它是 WorkBuddy 的子进程 ⇒ 拿得到网关口令', hkW-36, 11, 6), 'n-sub');
|
||
tx(hkX+20,R5.y+84, fitText('两个模式:--once(队列)/ --tick(上报)', hkW-36, 11, 6), 'n-sub');
|
||
// WorkBuddy → 钩子(它在钩子时机起它)
|
||
edge('M'+(hkX+70)+' '+RB.y+' L'+(hkX+70)+' '+(R5.y+R5.h), true);
|
||
lbl(hkX+82, (RB.y+R5.y+R5.h)/2, '用时才喊它', 'start');
|
||
// 钩子 → 协作程序(--once):直接向上
|
||
edge('M'+(hkX+40)+' '+(R5.y)+' L'+(hkX+40)+' '+(R4.y+R4.h), true);
|
||
lbl(hkX+52, (R5.y+R4.y+R4.h)/2, '--once', 'start');
|
||
/* Hook进程 → 上报(--tick):🔴 **一条纯竖直的线** —— 用户 2026-10-01:
|
||
「Hook进程 框图宽度往右边延长一些,**让 tick 那条线能竖直摆放**」。
|
||
⚠️ 原版出口在 `hkX+hkW-40`=840、而入口在上报框中心 `UX+PW/2`=850 ⇒ 中间夹了一段
|
||
**10px 的横折**(`L840 B3 → L850 B3`),看着像线画歪了。
|
||
⇒ 现在出口 x **≡ 上报框中心**,整条线只剩竖直一段(R5 顶 → R4 底),⛔ 不再横折。
|
||
⚠️ 出口点必须仍落在 Hook 框**顶边之内**(hkX..hkX+hkW)—— 这就是上面加宽到 490 的原因;
|
||
改这两个数时**必须一起改**,否则线头会悬在框外(几何自检 ⑥ 段守着这条)。 */
|
||
var TICKX=UX+PW/2;
|
||
edge('M'+TICKX+' '+R5.y+' L'+TICKX+' '+(R4.y+R4.h), true);
|
||
lbl(TICKX+12, B3+4, '--tick', 'start');
|
||
|
||
// ── 最底一条:WorkBuddy
|
||
// (用户 2026-09-30:「**宿主地基 这个就叫 WorkBuddy** 不要用这些AI语言」⇒ ⛔ 不用自造词)
|
||
rect(SX0,RB.y,SX1-SX0,RB.h,'base');
|
||
tx(SX0+20,RB.y+33,'WorkBuddy · 只读它的数据库 / 排期 / 钩子 / 文件占用登记','n-sub');
|
||
// ④ 收结果:WorkBuddy → 协作程序(只读,灰线)。
|
||
// ⚠️ 走 x=340 —— **必须落在协作程序框底边(280..580)上**,否则线头悬空
|
||
// (2026-09-30 用户一眼看出:"WorkBuddy 左边那根线是要连到哪里?")。
|
||
// x=340 同时⛔ 不穿 R5 的 Hook进程框(400..890 —— 2026-10-01 框宽 480→490 后右边界右移)。
|
||
edge('M340 '+RB.y+' L340 '+(R4.y+R4.h), true);
|
||
// ⚠️ 标签放线的**左边**(anchor end)—— 放右边会和「在钩子时机起它」挤成一行连读。
|
||
lbl(326, RB.y-14, '④ 收结果 · 读 WorkBuddy 库', 'end');
|
||
|
||
o.push('<defs><marker id="ar" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6" orient="auto-start-reverse">'
|
||
+'<path d="M2 1L8 5L2 9" fill="none" stroke="var(--color-accent)" stroke-width="1.5" stroke-linecap="round"/></marker></defs>');
|
||
document.getElementById('arch').innerHTML=o.join('');
|
||
/* ══════════════════════════════════════════════════════════════════════════
|
||
🔴 2026-10-01(用户:「**这段乱七八糟的 也精简 分段落描述**」)
|
||
|
||
原版把八九条说明用 ` · ` 串成**一整行**(还要加上面那行静态小字),读起来是一堵墙 ——
|
||
看不出哪句在说哪件事。⇒ 改成 **分块 + 一行一条**:
|
||
|
||
· 「连线」 —— ①→④ 与队列**分别是什么意思**(静态语义)
|
||
· 「当前」 —— **实时读数**(每次刷新会变的那几项)
|
||
· 「<唤醒会话名>」 —— 那一格**和别的是什么关系**(仅当画了它)
|
||
|
||
⛔ 只**拆句、删冗余**,⛔ **不新增信息**;与右上角 `?` 重复的那段("第三层横着排/
|
||
空框占位")就在这里去掉 —— 它本来就是 `?` 里的内容,露两遍反而像两回事。
|
||
══════════════════════════════════════════════════════════════════════════ */
|
||
function _blk(lead, items){
|
||
items=(items||[]).filter(function(x){return !!x});
|
||
if(!items.length) return '';
|
||
return '<div class="anBlock"><div class="anLead">'+lead+'</div>'
|
||
+items.map(function(x){return '<div class="anItem">'+x+'</div>'}).join('')+'</div>';
|
||
}
|
||
/* ── 块①:当前(**实时读数**)──
|
||
🔴 2026-10-01 用户:「**连线 · 图上那几条线分别是什么** 这段话不需要,**看框架图就行了**」
|
||
⇒ 删掉原来那块 ①派活/②上报/③上报/④收结果/队列 的图例 —— 那几条线**图上本来就标了字**
|
||
(`① 派活 · 自动化排期` 等 SVG 标签就在线旁边),图外再列一遍是**重复**。
|
||
⚠️ 但其中"**① 是唯一能开新会话的通道**"这一条**图上看不出来**(是硬约束,不是线名)
|
||
⇒ 它搬进「协作架构」右上角 `?`,⛔ 不丢(同族红线:读到了就要说)。 */
|
||
|
||
var _now=[
|
||
'主会话<b>负责的任务类别</b>:'+(TOPS.length
|
||
? TOPS.map(function(t){var s=MBT[t]||''
|
||
return '<code>'+esc(t)+'</code>'+(s?('('+esc(s)+')'):'(⚠ 无主会话)')}).join('、')
|
||
: '⚠ 未在对话里说明过'),
|
||
'协作会话:在跑 <b>'+runningS.length+'</b> 个 · 第三层画 <b>'+cells.length+'</b> 格'
|
||
+((moreW>0)?('(另有 <b>'+moreW+'</b> 条未画)'):'')
|
||
+(cells.length?'':'(一条都没有 ⇒ 画空框占位)'),
|
||
'本项目会话共 <b>'+S.length+'</b> 个(含历史)· 明细见下方「会话」表',
|
||
/* 🔴 2026-10-01:**同工作区但没归入本项目**的会话 —— 数据披露,⛔ 不许静默丢。
|
||
⚠️ 用 `is-warn` 的语气说,但**措辞不许判它是本项目**(归属判据没变,它确实不算)。
|
||
位置说明:这是**机制层**的问题(棒命名不合规 ⇒ 会被漏管),所以留在**版面上**、⛔ 不进 `?`。 */
|
||
/* ⚠️ **只把"在跑的 / 近 3 小时的"摊在版面上**(用户反复要求"精简")——
|
||
陈年历史(早已 completed 的旧棒)**只计一个数**,⛔ 不铺半屏。
|
||
🔴 但**计数必须是全量**(⛔ 不许因为"它很旧"就少说一句)—— 这是同族红线的底线。 */
|
||
UNREC.length ? (function(){
|
||
var _hot=UNREC.filter(function(u){return u.status==='working'||Number(u.age_min||1e9)<=180});
|
||
var _show=_hot.slice(0,3);
|
||
return '⚠️ 本工作区另有 <b>'+UNREC.length+'</b> 条会话**按命名判据未归入本项目**'
|
||
+'(⛔ 不算本项目会话 ⇒ **会被漏管**):'
|
||
+(_show.length?('在跑/近 3 小时 <b>'+_hot.length+'</b> 条 —— '
|
||
+_show.map(function(u){return '<code>'+esc(u.id8)+'</code> '+esc(u.title)}).join(' · ')
|
||
+(_hot.length>_show.length?(' … 等 <b>'+_hot.length+'</b> 条'):''))
|
||
:'其中**近 3 小时内没有**(都是历史)')
|
||
+' · 其余 <b>'+(UNREC.length-_hot.length)+'</b> 条是历史'
|
||
+'(全量见 `board.json` 的 `sessions_unrecognized`)'
|
||
+' —— 成因:棒名须为 `[协作]-[<类别>]-<具体>`(第 2 级**带方括号**、'
|
||
+'值取 `goal.json` 的 `topics`(任务类别),⛔ 不是 `short` 简称)';
|
||
})() : '',
|
||
// ⚠️ 「另有 N 个未画」是**数据披露**(⛔ 不许静默丢)⇒ 留在版面上;它随数据变,⛔ 不能进 `?`
|
||
(TG.length>1)?('另有 <b>'+(TG.length-1)+'</b> 个时间驱动源未画(图上只画最靠前的一个)'):'',
|
||
];
|
||
/* 🔴 「未归类」必须在**图外说清**(⛔ 不许静默丢):这些线⛔ 不属任何当前任务类别、
|
||
也⛔ 不画在第三层(那排只放协作会话)—— 但"几条线、多少件"必须说出口。 */
|
||
if(_fold) _now.push('<b>未归类</b>:<b>'+_lg.length+'</b> 条线('
|
||
+_lg.map(function(z){return '<code>'+esc(z.name)+'</code>'}).join('、')
|
||
+')不在当前任务类别清单里,共 <b>'+_fold.total+'</b> 件 · 已完成 <b>'+_fold.done+'</b>'
|
||
+'(⛔ 不是第三层的协作会话,明细见下方台账表)');
|
||
/* 🔴 2026-10-01 用户:「这个**不需要**吧,看框架图就行了」⇒ 原来那块「<唤醒会话名>」**整块删掉**。
|
||
它的三条定性(**独立会话**/⛔ **不在主会话上处理**/**随需求确定时才创建**)+
|
||
徽章色义(绿 ● 在跑 · 黄 ‖ 停着 · 灰 ○ 还没建)**一条都没丢** ——
|
||
全在「协作架构」右上角 `?` 里(那块本来就是小字说明的正式落点:
|
||
`agent-operating-rules`「⛔ 别往版面摊大段说明」)。
|
||
⛔ 也别"在图里再标一遍" —— 图上已经有格名、类别、状态色、位置这四样了。
|
||
⚠️ 形状改过两次(六边形 → 圆角矩形,靠**位置**区分)—— 那段历史留在 CSS 与本文注释里。 */
|
||
document.getElementById('archNote').innerHTML=_blk('当前', _now);
|
||
/* 标题提示:按"图上真画了哪几层"说,⛔ 不用自造词(用户 2026-09-30:
|
||
「宿主地基 这个就叫 WorkBuddy 不要用这些AI语言」) */
|
||
var _ah=document.getElementById('archHint');
|
||
if(_ah) _ah.textContent='用户 · 会话 · 协作会话 · 程序 · WorkBuddy';
|
||
}
|
||
|
||
function render(d){
|
||
D=d; document.getElementById('err').hidden=true;
|
||
var iv=(d.board&&d.board.refresh_interval)||3;
|
||
POLL=Math.max(1000, Math.round(iv*1000));
|
||
document.getElementById('iv').textContent=iv;
|
||
/* 🔴 本文件(布局/样式)改了就自动重载一次 —— 首次拿到当作基线,之后只在**变了**那一刻 reload。 */
|
||
var hs=(d.board&&d.board.html_sig)||'';
|
||
if(hs){
|
||
if(HTML_SIG===null) HTML_SIG=hs;
|
||
else if(HTML_SIG!==hs){ HTML_SIG=hs; location.reload(); return; }
|
||
}
|
||
/* ⏱「保留的快照时刻」只在这一处显示(协作架构右上角)⇒ ⛔ 不再往 header 放第二份 */
|
||
document.getElementById('ts2').textContent=(d.ts||'').slice(11)||'—';
|
||
paintAge();
|
||
var wb=document.getElementById('warnbar'), wn=(d.warn||[]);
|
||
if(wn.length){ wb.hidden=false; wb.textContent='⚠️ 有数据没读到(看板仍可用):'+wn.join(';'); }
|
||
else wb.hidden=true;
|
||
/* ⚠️ 2026-10-01:`renderProject(d)` **从这一行搬走了** —— 它属于"**随 tab 变**"的那一组,
|
||
统一由 `paintScope(d)` 在下面画(⛔ 这里再画一次会被立刻覆盖,白画)。 */
|
||
|
||
/* 🔴 「前置」整块**通用渲染** —— 技能侧⛔ 不认识任何具体组件。
|
||
内容全部来自**使用方**的 `board_ext.py`(经 board.json 的 `front`):
|
||
chips[{label,up,ok,bad,tip}] · paragraphs[{text,tone}] · tip
|
||
⛔ 技能里不许再出现某个项目的端口/路径/真名(用户 2026-09-30 定则:
|
||
「技能就是技能 程序就是程序,谁用产生的文件 放在他自己那里」)。 */
|
||
/* 🔴 `deliver` = 旧名「监督程序」的那条职责(**上报**)。2026-09-30 改名:
|
||
它不是"该常驻的进程",而是宿主钩子唤起的一次性 `--tick` ⇒ ⛔ 不再显示"已停"。 */
|
||
var f=d.front||{}, rt2=d.runtime||{}, pg=rt2.prog||{}, gd=rt2.deliver||{};
|
||
var chips='';
|
||
(f.chips||[]).forEach(function(c){
|
||
chips+=chip(String(c.label||'')+(c.up?' '+(c.ok||''):' '+(c.bad||'')),
|
||
c.up?'is-ok':'is-bad', c.tip||'');
|
||
});
|
||
var paras='';
|
||
(f.paragraphs||[]).forEach(function(p){
|
||
var t=String(p.text||'');
|
||
paras+='<p class="lede" style="margin:8px 0 0'+(p.tone==='warn'?';color:var(--color-warning)':'')+'">'+esc(t)+'</p>';
|
||
});
|
||
document.getElementById('front').innerHTML='<div class="rowchips">'
|
||
+chip('协作程序 '+(pg.label||'未知'), pg.up?'is-ok':'is-bad')
|
||
+chip('上报 '+(gd.label||'未知'), gd.up?'is-ok':'is-warn')
|
||
+'</div>'
|
||
+(chips?'<div class="rowchips" style="margin-top:8px">'+chips+'</div>':'')
|
||
+paras
|
||
+((f._missing||f._err)?('<p class="lede" style="margin:8px 0 0">看板扩展:'+esc(String(f._missing||f._err))+'</p>'):'');
|
||
/* 🔴 长解释一律进「?」—— ⛔ 不摊在版面上(用户 2026-09-30 要求) */
|
||
var tip=document.getElementById('frontTip');
|
||
if(tip){
|
||
if(!tip.dataset.base) tip.dataset.base=tip.innerHTML; /* 首轮把静态文案存下来 */
|
||
tip.innerHTML=tip.dataset.base; /* 每轮先还原,防重复追加 */
|
||
if(f.tip) tip.innerHTML+='<br><br>'+String(f.tip); /* 项目侧说明(允许含标签) */
|
||
if(gd.stopped && gd.reason){
|
||
tip.innerHTML+='<br><br><b>监督程序为什么是停的</b>(主会话主动止损,不是崩溃):'
|
||
+esc(String(gd.reason)).replace(/\*\*/g,'').replace(/`/g,'');
|
||
}
|
||
}
|
||
/* 板块标题与类别标签也由使用方给(⛔ 不在技能里写死"链路前置/组件状态") */
|
||
var fh=document.querySelector('#frontCard h2');
|
||
if(fh) fh.innerHTML=esc(f.title||'前置')+(f.tag?('<span class="hint">'+esc(f.tag)+'</span>'):'');
|
||
|
||
var n=d.notify||{};
|
||
document.getElementById('notify').innerHTML=
|
||
'<table><tr><th>本条待处理</th><td><code>'+esc(n.awaiting||'(无)')+'</code></td></tr>'
|
||
+'<tr><th>握手阶段</th><td>'+esc(n.phase||'—')+'</td></tr>'
|
||
+'<tr><th>最近发出</th><td><span class="ellip"><code>'+esc(n.sent||n.notice||'(无)')+'</code></span></td></tr></table>';
|
||
|
||
var w=(d.wakeups||[]).slice().reverse();
|
||
document.getElementById('wake').innerHTML = w.length
|
||
? '<table><tr><th>时间</th><th>类型</th><th>结果</th></tr>'+w.map(function(x){
|
||
return '<tr><td class="num">'+esc(ago(x.ts))+'</td><td>'+esc(x.kind)+'</td><td>'
|
||
+(x.ok?chip('HTTP '+x.http,'is-ok'):chip('HTTP '+x.http,'is-bad'))+'</td></tr>'}).join('')+'</table>'
|
||
: emptyBox('还没有上报记录','队列有变化,或者长时间没动静时,会出现第一条。');
|
||
|
||
/* 🔴 剩下的四块(本项目 / 架构图 / 需求台账 / 会话明细)**随 tab 变** ⇒ 交给 `paintScope(d)`。
|
||
⛔ 不在这里画 —— 这里画的是**工作区级**的(前置 / 队列通知 / 最近上报)。 */
|
||
paintScope(d);
|
||
}
|
||
|
||
/* ══════════════════════════════════════════════════════════════════════════════
|
||
🔴🔴 多目标 tab(用户 2026-10-01:「把协作实时看板改为 tab 支持多个目标执行协作状态展示」)
|
||
|
||
数据:快照的 `goals[]`(`board.py` 产)—— **一格 = 一个目标**,活跃的排第一。
|
||
⛔ 老快照(没有 `goals` 键)⇒ 回落成"一格 tab",**行为与旧版一致**(向后兼容)。
|
||
作用域:切 tab **只换四块**(本项目 / 协作架构 / 需求台账 / 会话明细);
|
||
「前置 / 队列通知 / 最近上报」是**工作区级**的 ⇒ ⛔ 不随 tab 变(它们本就不属于某一个目标)。
|
||
记住选择:当前 tab 写进 URL hash(`#g=<目标 key>`)⇒ **刷新、分享链接都不丢**当前选的是哪个目标。
|
||
⛔ 此处**不含任何项目知识**(目标名一律来自快照)—— 同 `board_ext` 那条职责边界。
|
||
══════════════════════════════════════════════════════════════════════════════ */
|
||
var GIDX=0;
|
||
|
||
function goalKey(g){
|
||
return String((g&&(g.key||(g.goal&&(g.goal.short||g.goal.title))))||'');
|
||
}
|
||
|
||
function pickGoalIdx(gs){
|
||
var m=String(location.hash||'').match(/[#&]g=([^&]*)/);
|
||
if(!m) return 0;
|
||
var k='';
|
||
try{ k=decodeURIComponent(m[1]); }catch(e){ k=m[1]; }
|
||
for(var i=0;i<gs.length;i++) if(goalKey(gs[i])===k) return i;
|
||
return 0; /* ⛔ hash 里的目标没了 ⇒ 回到第一个,不报错 */
|
||
}
|
||
|
||
/* 一格 tab = 目标名 + 状态徽章 + 完成情况摘要(都是实读,⛔ 不写死)。 */
|
||
function renderTabs(gs){
|
||
var box=document.getElementById('tabs');
|
||
if(!box) return;
|
||
if(!gs.length){ box.hidden=true; return; }
|
||
box.hidden=false;
|
||
box.innerHTML=gs.map(function(g,i){
|
||
var acc=(g.goal||{}).acceptance||{}, ks=Object.keys(acc);
|
||
var pass=ks.filter(function(k){return String(acc[k]).toLowerCase()==='pass'}).length;
|
||
var cls=!ks.length?'is-warn':(pass===ks.length?'is-ok':'is-bad');
|
||
var txt=!ks.length?'未声明判据':(pass+'/'+ks.length+' 通过');
|
||
/* 🔴 2026-10-01(用户:「再确认下**目标名称**显示在哪里的,没看到呢」)
|
||
⇒ 根因是我上一版**第一行只写了目标简称**(`short`),全名只藏在 `title=` 提示里
|
||
—— 简称认不出是"哪个目标"。**改:第一行就是完整目标名**(超出省略 + 悬停看全),
|
||
简称退到第二行当小字前缀。⛔ 名字一律来自快照,技能侧不写死任何目标名。 */
|
||
var full=String((g.goal&&(g.goal.title||g.goal.short))||'(未声明目标)');
|
||
var short=String((g.goal&&g.goal.short)||'');
|
||
return '<button type="button" class="tab'+(i===GIDX?' is-on':'')+'" data-gi="'+i+'" role="tab"'
|
||
+' aria-selected="'+(i===GIDX?'true':'false')+'" title="目标:'+esc(full)+'">'
|
||
+'<span class="tt">'+(g.active?'<span class="badge is-ok">当前</span>':'')
|
||
+(g.paused?'<span class="badge is-warn">已停</span>':'')+esc(full)+'</span>'
|
||
+'<span class="tsub">'+(short&&short!==full?esc(short)+' · ':'')+chip(txt,cls)+'</span>'
|
||
+'</button>';
|
||
}).join('');
|
||
/* 🔴 2026-10-01(用户:「**下面这段话是干啥的,为什么也在 tab 区域**」)——
|
||
**tab 条上⛔ 不放任何说明文字**。上一版在两格之间塞了一句「只登记了 1 个目标 ⇒ 只有一格 /
|
||
要并行看第二个目标:在 goals/ 里放一份 …json」—— 那是**使用说明**,不是**数据**,
|
||
摊在版面上就成了噪声(同族老毛病:把小字说明写到版面上)。
|
||
⇒ **删掉**;"怎么加第二个目标"搬进「本项目」右上角 `?` 里(见 projCard 的 qtip)。
|
||
⛔ 也别退回「共 N 个目标」那种话 —— 有几格**看格子就知道**,写出来信息量为零。 */
|
||
[].forEach.call(box.querySelectorAll('.tab'), function(b){
|
||
b.onclick=function(){
|
||
var i=Number(b.dataset.gi)||0;
|
||
var k=goalKey(gs[i]);
|
||
location.hash='g='+encodeURIComponent(k);
|
||
GIDX=i;
|
||
if(D) paintScope(D); /* ⛔ 不重新拉数据 ⇒ 切 tab 是**零延迟零请求**的 */
|
||
};
|
||
});
|
||
}
|
||
|
||
/* 把"当前目标"那一段投影成顶层字段的形状 ⇒ 下面四个渲染器**原样复用**(⛔ 不复制代码)。 */
|
||
function scopeView(d,g){
|
||
if(!g||g===d) return d;
|
||
var v={}, k;
|
||
for(k in d) if(Object.prototype.hasOwnProperty.call(d,k)) v[k]=d[k];
|
||
v.goal=g.goal||d.goal; v.project=g.project||d.project;
|
||
v.tasks=g.tasks||{}; v.sessions=g.sessions||[];
|
||
v.labor=g.labor||[]; v.orphan=g.orphan||{};
|
||
v.queue=g.queue||{}; v.progress=g.progress||{};
|
||
v.others_running=g.others_running;
|
||
return v;
|
||
}
|
||
|
||
function paintScope(d){
|
||
/* ⛔ 老快照没有 `goals` ⇒ 回落成"一格 tab = 整份快照",行为与旧版一致。 */
|
||
var gs=(d.goals&&d.goals.length)?d.goals:[d];
|
||
GIDX=pickGoalIdx(gs);
|
||
if(GIDX<0||GIDX>=gs.length) GIDX=0;
|
||
renderTabs(gs);
|
||
var v=scopeView(d,gs[GIDX]);
|
||
renderProject(v);
|
||
renderTasks(v);
|
||
renderSessions(v);
|
||
drawArch(v);
|
||
}
|
||
|
||
/* 需求台账表(⛔ 随 tab 变 —— 台账条目按**任务类别**归属到目标)。 */
|
||
function renderTasks(d){
|
||
var LN={}, LK={}; (d.labor||[]).forEach(function(L){ LN[L.line]=L.name; LK[L.line]=L.kind||'topic' });
|
||
/* 🔴 台账条目的 `line` 现在语义=**任务类别**。旧数据里存的是**跨工作区时代的工作区名**
|
||
⇒ 列名与值对不上。⛔ 不隐藏、⛔ 不改历史记录,只**如实标一句**(同族红线:读到了就要说)。 */
|
||
var TP={}; (((d.project||{}).topics)||[]).forEach(function(t){ TP[t]=1 });
|
||
var tk=Object.keys(d.tasks||{}).sort();
|
||
document.getElementById('tasks').innerHTML = tk.length
|
||
? '<table><tr><th>需求</th><th>状态</th><th>任务类别</th></tr>'+tk.map(function(k){var v=d.tasks[k];
|
||
var cls=v.state==='done'?'is-ok':(v.state==='blocked'?'is-bad':'is-warn');
|
||
var _ln=v.line||'', _stale=(_ln && LK[_ln]==='legacy') || (_ln && !TP[_ln] && LK[_ln]!=='topic');
|
||
return '<tr><td><code>'+esc(k)+'</code></td><td>'+chip(zhState(v.state),cls)+'</td><td>'
|
||
+esc(LN[_ln]||_ln||'—')
|
||
+(_stale?'<span class="oldline">(旧值 · 不在当前任务类别清单)</span>':'')
|
||
+'</td></tr>'}).join('')+'</table>'
|
||
: emptyBox('台账为空','协作会话用 --report 上报后,条目进这里。');
|
||
}
|
||
|
||
/* 会话明细表(⛔ 随 tab 变 —— 会话按"标题里的任务类别"归属到目标)。 */
|
||
function renderSessions(d){
|
||
var ss=d.sessions||[];
|
||
document.getElementById('sess').innerHTML = (ss.length
|
||
? '<table><tr><th>会话</th><th>角色</th><th>任务类别</th><th>状态</th><th>活动</th></tr>'+ss.map(function(x){
|
||
return '<tr><td><code>'+esc(x.id8)+'</code> <span class="ellip">'+esc(x.title)+'</span></td>'
|
||
+'<td><span class="role'+(x.role==='主会话'?' is-main':'')+'">'+esc(x.role||'—')+'</span></td>'
|
||
+'<td>'+(x.topic?esc(x.topic):'—')+'</td><td>'
|
||
+(x.status==='working'?chip('执行中','is-ok'):chip(zhSess(x.status)))+'</td><td class="num">'+esc(x.age_min)+' 分钟前</td></tr>'}).join('')+'</table>'
|
||
: emptyBox('本项目暂无相关会话','只含主会话,和标题带 [任务类别] 的协作会话。'));
|
||
}
|
||
|
||
/* 数据延迟 = 现在 − 快照生成时刻(同机时钟可比)。⛔ 延迟不是错误 —— 是"看板不打扰程序"的代价。 */
|
||
function paintAge(){
|
||
if(!D||!D.epoch) return;
|
||
var s=Math.max(0, Math.round(Date.now()/1000 - D.epoch));
|
||
var el=document.getElementById('lag');
|
||
if(el) el.textContent=s+' 秒';
|
||
var w=document.getElementById('herometa'); /* 超 15 秒 ⇒ 整块转警示色 */
|
||
if(w) w.classList.toggle('is-old', s>15);
|
||
}
|
||
|
||
function load(){
|
||
if(!LIVE){
|
||
document.getElementById('err').hidden=false;
|
||
document.getElementById('err').textContent='要经本地服务打开:先跑 python board.py --serve 8788,再开 http://127.0.0.1:8788/(直接 file:// 打不开,取不到数据)';
|
||
return;
|
||
}
|
||
fetch('board.json?t='+Date.now(),{cache:'no-store'}).then(function(r){
|
||
if(!r.ok) throw new Error('HTTP '+r.status);
|
||
return r.json();
|
||
}).then(render).catch(function(e){
|
||
var el=document.getElementById('err'); el.hidden=false;
|
||
el.textContent='快照读取失败:'+e.message+'(服务还开着吗?重跑 python board.py --serve 8788)';
|
||
});
|
||
}
|
||
/* 🔴 轮询节奏 = 服务的快照节奏(⛔ 页面不自行加频:加了只是空转,不会更快) */
|
||
var POLL=3000;
|
||
function loop(){ load(); setTimeout(loop, POLL); }
|
||
initStyleSwitcher();
|
||
if(LIVE){ loop(); setInterval(paintAge,1000); } else { load(); }
|
||
</script>
|
||
</body>
|
||
</html>
|