1) stop-dialog-guard: session_budget() 早退路径返回 2 值、末尾返回 3 值,调用方按 3 值解包
⇒ transcript > 64 MiB 时每轮 ValueError。因 fail-open(异常仍 exit 0),
宿主零报错、install.py --verify 只判 rc=0 ⇒ 假绿;实测 86 条 EXCEPTION,
死掉的是整条(水位/收口、接续机制起点、预算告警、门禁自检、路径自检)。
2) session-rules-check 三处判据:
· hook_reg 按旧文件名找 ⇒ 合并成 prompt-guards.py 后每轮假红 ⇒ 改为一组可接受名
· snap_sync 拿 mtime 当内容判据 ⇒ 连续 4 天假红 ⇒ 改为复用抽取器本体比对内容
(变异对照:截断快照能报 fail,非恒绿)
· mem_ptr 只查全局技能根 ⇒ 工作区自带技能被判悬空 ⇒ 改查「全局 ∪ 工作区」
3) pitfalls 新增 P0-95(改判据必须重跑变异对照;fail-open + 只看 rc=0 = 假绿温床)
4) 回复排版核心块新增「变相征询同样禁止」(先只报不动/等你发话/我倾向X你看呢
这类不带选项的待定清单,一律按待拍板项写:问题+说明+各候选优缺点+倾向)
1859 lines
148 KiB
HTML
1859 lines
148 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-10-05 随「任务类别」整行删除,`.topics` 规则也删掉了(用户:「这个没用就删除 看着烦」)。
|
||
⛔ 别把它当"少了个样式"找回来:版面上已经没有 `.topics` 这个容器了
|
||
(类别改由「协作架构」图 + `?` 提示承载)。 */
|
||
.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)}
|
||
/* 🔴 2026-10-02 作废(用户逐字:「**完成情况 已经通过的部分 改为绿色 数字也是** 6 /7 通过 也是绿色」):
|
||
原规则 `.accBar.is-badn i{background:var(--color-danger)}` 把**已通过的那一段填充**也染成危险色
|
||
—— 等于把"已经过了的"画成"坏的",读起来是"全都没过"。⇒ 改为**填充段恒绿**(=已过)。
|
||
"还没到的地方"仍由轨道底色表达(上一行 `.accBar.is-badn` 的 `--color-danger-subtle`);
|
||
"还差哪几条"仍由右侧 `未完:…` 逐条点名 ⇒ ⛔ 信息一条没少。
|
||
⛔ 原文留痕(不再生效):`.accBar.is-badn i{background:var(--color-danger)}` */
|
||
.accBar.is-badn i{background:var(--color-success)}
|
||
.accNum{font-variant-numeric:tabular-nums;font-size:14px}
|
||
.accNum.is-ok{color:var(--color-success)}
|
||
/* 🔴 2026-10-02 作废(同上一处,用户:「**数字也是** 6 /7 通过 也是绿色」):
|
||
原 `.accNum.is-bad{color:var(--color-danger)}` 在"还有没过项"时把数字染红 ⇒ 用户要的是**绿色**。
|
||
⛔ 原文留痕(不再生效):`.accNum.is-bad{color:var(--color-danger)}` */
|
||
.accNum.is-bad{color:var(--color-success)}
|
||
/* ⛔ 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}
|
||
/* 🔴🔴 2026-10-04 用户定案:「**看板中的 tab 宽度改为一样宽**」
|
||
⛔ 原写法 `min-width:180px; max-width:420px`(**内容自适应**)⇒ 每格宽取决于标题字数
|
||
⇒ 「目标甲」窄、「检查会话协作是否运行正常 + 协作机制问题排查与修复」把整行撑到420
|
||
⇒ 参差不齐,扫读时找不到"现在在第几格"。
|
||
✅ 改成**等宽**:`flex:1 1 0` = 每格**等分**剩余宽度(`flex-basis:0` 是关键,
|
||
⛔ 用 `flex:1` 默认 `1 1 auto` 仍按内容分配 ⇒ 不会等宽);
|
||
`max-width:420px` 保留 ⇒ 目标很少时不会拉成整屏那么宽(配合 `justify-content:flex-start`)。
|
||
⚠️ 文字多的格靠 `.tt/.tsub` 已有的 `text-overflow:ellipsis` 截断(见下),
|
||
⛔ 不许改成 `white-space:normal` —— 那会让每格**高度**又不一样。 */
|
||
.tab{display:flex;flex-direction:column;gap:3px;flex:1 1 0;min-width:0;
|
||
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}
|
||
/* 🔴 2026-10-03 20:31x **工作区名用大字体**(用户:「工作区名称 用大字体」)
|
||
⇒ 它是这一行的**主体**(读者第一眼要分清"哪个工作区"),⛔ 不该跟判据徽章一样是 11px 小字。 */
|
||
.tab .tsub .wsname{font-size:13px;font-weight:600;color:var(--color-text-secondary);margin-left:6px}
|
||
.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 条**只放数据**;使用说明一律进各板块右上角 `?`。 */
|
||
/* 🔴🔴 2026-10-04 **超出 6 个的目标收进下拉**(用户定案:「最多显示 6 个目标,超出的用下拉菜单显示」)。
|
||
⚠️ `position:relative` 在 `.tabwrap` 上 ⇒ 菜单**相对它**定位(⛔ 别挂到 `.tabs` 上,
|
||
那会让菜单跟着 flex 换行跑到版面别处—— flex 容器里绝对定位的参照物是**最近的定位祖先**)。 */
|
||
.tabwrap{position:relative;display:flex;flex:0 0 auto}
|
||
/*「更多」是**操作件**不是数据格 ⇒ 固定宽度,⛔ 不跟着 `flex:1 1 0` 一起等分
|
||
(那样它会跟旁边 6 格一样宽,⛔ 而内容只有「更多」两个字 ⇒ 版面浪费)。 */
|
||
.tabmore-btn{flex:0 0 auto;min-width:132px;max-width:220px}
|
||
.tabmore-btn .tsub{display:flex;align-items:center;gap:5px}
|
||
.tabmenu{position:absolute;top:calc(100% + 6px);left:0;z-index:40;min-width:260px;max-width:420px;
|
||
max-height:320px;overflow:auto;padding:6px;display:flex;flex-direction:column;gap:4px;
|
||
background:var(--color-bg-surface);border:1px solid var(--color-border-default);
|
||
border-radius:var(--radius-12);box-shadow:var(--shadow-pop)}
|
||
.tabmenu[hidden]{display:none}
|
||
.tabmenu-item{display:flex;flex-direction:column;gap:2px;align-items:flex-start;width:100%;
|
||
text-align:left;padding:7px 10px;border:1px solid transparent;border-radius:var(--radius-8);
|
||
background:transparent;color:var(--color-text-primary);font:inherit;font-size:13px;cursor:pointer}
|
||
.tabmenu-item:hover{border-color:var(--color-accent);background:var(--color-accent-subtle)}
|
||
.tabmenu-item .tt{white-space:normal;font-weight:500}
|
||
.tabmenu-item .wsname{font-size:11px;font-weight:600;color:var(--color-text-tertiary)}
|
||
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}
|
||
/* 🔴🔴 2026-10-04 21:3x 用户报障:「看板中 **未完成的描述 分段落展示一条一行**」。
|
||
原写法=`<span>未完:A(…)、B(…)、C(…)</span>`,用 `、` 全拼在**一行**里
|
||
⇒ 条数一多就挤成一大坨(实测 vibe-product 区 15 条未完项 ⇒ 整块糊住、根本读不出来)。
|
||
⇒ 改成 `<ul><li>` **一条一个 `<li>`**(每 li 里:`<code>` 判据名 + 说明)。
|
||
⚠️ 三条约束:
|
||
① **每 li 内部允许折行**(说明文字很长)⇒ 用 `flex` + `min-width:0` 让说明自己换行,
|
||
⛔ 不用 `white-space:nowrap`(那会横向溢出,撞 J5「零横向溢出」判据)。
|
||
② 🔴 **全展开,⛔ 不设 `max-height`/内滚动** —— 我第一版设了 `max-height:190px`,
|
||
结果**只能看到前 6 条、后面 9 条被裁掉**(实测 vibe-product 区 15 条)。
|
||
⇒ 这正是用户要"看成情况"的那块信息,**藏起来就等于没说**
|
||
(同族红线:读到了就要说)。整页本来就纵向滚动,跟着滚即可。
|
||
③ 判据名用**等宽体**(`code`)并**左对齐定宽**,让 15 行的说明文字**竖向对齐**、能扫读。 */
|
||
.accList{margin:6px 0 0;padding:0;list-style:none;flex:1 1 100%;min-width:0;
|
||
border-top:1px solid var(--color-border-tertiary)}
|
||
/* ⚠️ 「未完:」这个小标签单独占一行(它是 `.accList` 的**上一行**)⇒ 去掉下边距、
|
||
与列表首行贴住,视觉上是一段而不是两段。 */
|
||
.accLead{flex:1 1 100%;margin:0}
|
||
.accList li{display:flex;gap:10px;align-items:baseline;padding:5px 0 5px 2px;
|
||
border-bottom:1px solid var(--color-border-tertiary);font-size:12px;line-height:1.5}
|
||
.accList li:last-child{border-bottom:0}
|
||
.accList .accKey{flex:0 0 auto;min-width:64px;font-weight:600;
|
||
color:var(--color-text-secondary);font-family:ui-monospace,Consolas,monospace}
|
||
.accList .accWhy{flex:1 1 auto;min-width:0;color:var(--color-text-tertiary);
|
||
overflow-wrap:anywhere}
|
||
.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)}
|
||
/* 🔴 唤醒会话(**位置:主会话下面那一行(`RW`)的左格**)—— 用户 2026-10-01:先要「换个样式 和 协作会话区分开」,
|
||
随后**定性**「**唤醒脉冲会话,本质还是会话**」⇒ **形状改回会话同族的 `rect rx=14`**,
|
||
⛔ **不再用六边形** —— 形状已不是区分手段,**位置**才是(`RW` 行左格、⛔ 不进任务会话那一排)。
|
||
⇒ 只剩一条视觉标记:外圈一圈**虚线光晕** ⇒ 一眼看出「它靠**脉冲**被叫醒,不是一直在跑」。
|
||
⚠️ 状态色仍然照旧(`up` ⇒ 绿/否则黄)—— 位置管"它是谁",颜色管"它活着没"。 */
|
||
/* 🔴🔴 2026-10-01 加:**虚线分组大框** —— 用户原话:
|
||
「用一个**虚线大框**把 主会话 唤醒会话 和 跟进会话都框起来,
|
||
这个虚线大框 **连接 协作会话 虚线大框**就好」
|
||
⛔ **不填色**:它只负责"把几格圈在一起",填了色里面的格就被背景淹没了。
|
||
⚠️ 线比节点边框**细一点、虚一点**(1.6px / 8-7 虚线)—— 它是**分组**,⛔ 不是又一个节点。 */
|
||
.grp{fill:none;stroke:var(--color-border-strong);stroke-width:1.6;stroke-dasharray:8 7;stroke-linejoin:round}
|
||
.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 变(它们本来就不属于某一个目标)。
|
||
🗑️ 2026-10-03 **删掉第三张卡「最近上报 wakeups.jsonl」**(用户口径「没用了就删除」):
|
||
投递机制整套退役 ⇒ `wakeups.jsonl` **已不再被写**(实测文件根本不存在)⇒ 那张卡恒空,
|
||
留着就是一块永远显示「还没有上报记录」的**假面板**。 -->
|
||
<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>
|
||
<!-- 🔴🔴 2026-10-05 加(用户逐字:「看板里面应该明确指出 对应的 工作区 和 目标」):
|
||
下面第一行**固定是「目标」**,它先说**所属工作区**(真实目录名,放在 pill 里),
|
||
再说**目标全称** —— 值由 `renderProject()` 填(⛔ 不在这儿写死任何值)。 -->
|
||
<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>2026-10-03 改版:会话只剩两类</b>(用户逐字:「让你把 架构图里面的 唤醒和跟进 以及
|
||
上报都删除」)—— <b>唤醒会话 / 跟进会话 / 队列上报机制整套退役</b>,图上<b>整行删除</b>
|
||
(⛔ 不是画成灰格占位)。⇒ 图形顺序=<b>用户 → 主会话 → 任务会话 → 检查程序</b>。<br>
|
||
<b>主会话</b>只管<b>判断 + 派活</b>,⛔ <b>只由用户触发</b>(图上指进主会话的线<b>只有上面那一条</b>)。<br>
|
||
<b>虚线大框</b>=<b>分组</b>(⛔ <b>不是节点</b>、⛔ 不是"新的一层"):上面框住<b>主会话</b>,
|
||
下面框住<b>任务会话</b>;两个大框之间<b>只有一条线 = ① 派活</b>(⛔ 不从主会话往每格画射线)。<br>
|
||
🔴 <b>① 派活</b>的特别之处:它是主会话建一条<b>自动化排期</b>、宿主到点开新会话 ——
|
||
这是<b>唯一能开新会话的通道</b>(钩子开不了会话)。<br>
|
||
主会话<b>下方那一排</b>是<b>任务会话</b> —— 一格一条,横着排、有几条画几条,
|
||
一条都没有时画个空框写明「暂无协作目标」。<br>
|
||
任务类别是<b>同一个工作区</b>里用<b>会话名称前缀</b>区分的(每个类别各有自己的主会话)。<br>
|
||
会话共<b>两类</b>:<b>主会话 · 任务会话</b>,靠<b>标题两级前缀</b>区分、⛔ 不看目录;<br>
|
||
某条会话<b>撞到上限</b>时由它自己建出的<b>接续</b>是<b>形态</b>、⛔ 不是第 3 类
|
||
(它<b>继承被接续那条的角色</b>)。<br>
|
||
<b>协作与投递按定案一直运行(常驻)</b>,宿主钩子只作<b>补充</b>;⛔ 不再用"自动任务当闹钟"。
|
||
⚠️ 唤醒时钟=<b>检查程序</b>(<b>--supervise</b>),⛔ 不再靠"唤醒会话"那一套。<br>
|
||
状态都是实读的,会话读 WorkBuddy 的库,程序读它自己留下的时间记录。哪一格停着不动,看框的颜色就知道。<br>
|
||
看板只读。后台每 <span id="iv">—</span> 秒生成一份快照,页面直接读缓存。
|
||
</span></span>
|
||
<div id="archWrap"><svg class="arch" id="arch" viewBox="0 20 1280 926" role="img"
|
||
aria-label="多会话协作架构(2026-10-03 改版:会话只剩两类)。两个虚线大框=两组(不是节点)。上组框住主会话(只管判断和派活,只由用户触发,指进它的线只有上面那一条);下组框住任务会话(有几条画几条,一条都没有时画一个空框写明暂无任务会话);两个大框之间只有一条线=① 派活。图上只画现役的两类会话与检查程序,不画已下线的东西。再下面是协作(全称检查程序)、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>队列通知 / 握手</h2><div id="notify"><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 / 默认主会话 / 工作区)与规则(归属判据)压成**最后那一列描述**,⛔ 不再各占一行抢戏。 */
|
||
/* 🔴🔴 2026-10-02:`accIsPass` **必须放全局作用域**。
|
||
它同时被 `renderProject()`(主区「完成情况」进度条)与 `renderTabs()`(tab 摘要徽章)调用,
|
||
而这两个是**并列的顶层函数**、不是嵌套 —— 我第一版把它定义在 `renderProject()` 内部 ⇒
|
||
`renderTabs()` 里 `ReferenceError` ⇒ **整页崩在那行之后**(实测:主区 `accNum` 与 tab 双双消失)。
|
||
⇒ 教训:**跨函数共用的判据,必须放在两者的共同作用域**;且改完必须**渲染取一次证**,
|
||
⛔ 不能只靠 `node --check`(语法过 ≠ 不抛异常 —— 这一条本轮当场被证实)。 */
|
||
/* 🔴🔴🔴 **判定词白名单 —— 必须与 `board.py::ACC_PASS_WORDS` 逐字同款**(2026-10-04 立)。
|
||
用户原话:「为什么还是0通过」——真源写 `达(…)`,而判据只认 pass/过/通过 ⇒ 15 条全判非 pass。
|
||
⚠️ **改这里必须同时改 `board.py` 的 `ACC_PASS_WORDS`**,⛔ 别只改一边(本规则已漂过 3 次)。 */
|
||
var ACC_PASS_WORDS=['pass','过','通过','达','达标','合格','完成'];
|
||
/* 🔴🔴🔴 **判定词前缀「副词」白名单 —— 必须与 `board.py::ACC_ADV_PREFIX` 逐字同款**
|
||
(2026-10-05 立 —— **第 5 次复发**,这次改成治形状、⛔ 不再补词)。
|
||
|
||
病根:判定词前会带一个**表已完成/确认的副词**,而 `indexOf(...)===0` 要求**从第一个字符**
|
||
就命中 ⇒ 带副词就**永远不命中** ⇒ 明明写着「已过」却判「非过」。
|
||
· 实测(vibe-product 页签):9 条真值**全写 `已过|…`**,看板显示 **0 / 9 通过**
|
||
—— 因为 `已过` 的第一个字是 `已`、⛔ 不是 `过`。用户原话:
|
||
「完成情况 0 / 9 通过,但是我看到都显示已过呀」。
|
||
⛔ 前四次(10-02「过」/10-03「逐段找」/10-04「达」)都在**补词** ⇒ 换个措辞必然再栽
|
||
—— 这一次**换个方向**:**先把副词剥掉再比**。
|
||
|
||
⚠️ 白名单**只收表"完成/确认"的副词**;⛔ **绝不许收否定副词**(未/不/没/非/待)——
|
||
它们一律**留给 fail-closed**(不剥 ⇒ 开头不是判定词 ⇒ 判非过)。
|
||
⚠️ 改这里**必须同时**改 `board.py::ACC_ADV_PREFIX`。 */
|
||
var ACC_ADV_PREFIX=['已','已经','均','都','经复核','复核后'];
|
||
function accIsPass(v){
|
||
var s=String(v==null?'':v).replace(/^\*+/,'').trim();
|
||
if(!s) return false; /* 空 ⇒ 判不出来 ⇒ ⛔ 不算过(fail-closed) */
|
||
var head=s.split(/[((]/)[0]; /* 只取判定词,丢掉后面那段说明 */
|
||
/* 🔴🔴 2026-10-03 19:4x **必须与 `board.py::acc_is_pass()` 逐条同款** ——
|
||
原来这里是 `split(/[::]/).pop()`=**只取最后一段**,而真源值里常有多处冒号
|
||
(说明文字 + **时间里的**)⇒ 明明写着「过」被判非过
|
||
⇒ 前端显示「判据 3/4 通过」、后端却是「全部 pass(4 条)」=**同一条规则两份实现打架**(本轮实测撞到)。
|
||
✅ 改法:与后端同款——**逐段找**「某一段以判定词开头」;`不过`/`未过`/`待重验` 天然不命中 ⇒ 仍 fail-closed。
|
||
⚠️ 改这一处**必须同时**改 `board.py::acc_is_pass()`;⛔ 别只改一边。 */
|
||
var segs=head.split(/[::]/), ok=false;
|
||
for(var i=0;i<segs.length&&!ok;i++){
|
||
var t=segs[i].replace(/^\*+/,'').trim().toLowerCase();
|
||
for(var j=0;j<ACC_PASS_WORDS.length;j++){
|
||
if(t.indexOf(ACC_PASS_WORDS[j])===0){ ok=true; break; }
|
||
}
|
||
if(ok) break;
|
||
/* 🔴🔴 2026-10-05 第 5 次复发:**剥掉"完成副词"再比**(治形状,⛔ 不补词)。
|
||
⚠️ **可叠加**:实测真源写 `均已通过`/`都已达标` ⇒ 要能连剥(`已`+`均`/`都`)。
|
||
⚠️ 上限 3 轮(有界):正常最多两级;⛔ 不是省事。 */
|
||
var t2=t;
|
||
for(var r=0;r<3;r++){
|
||
var cut='';
|
||
for(var q=0;q<ACC_ADV_PREFIX.length;q++){
|
||
if(t2.indexOf(ACC_ADV_PREFIX[q])===0 && ACC_ADV_PREFIX[q].length>cut.length){
|
||
cut=ACC_ADV_PREFIX[q]; /* 取**最长**匹配(`已经` 优先于 `已`) */
|
||
}
|
||
}
|
||
if(!cut) break;
|
||
t2=t2.slice(cut.length).replace(/^\*+/,'').trim();
|
||
for(var k=0;k<ACC_PASS_WORDS.length;k++){
|
||
if(t2.indexOf(ACC_PASS_WORDS[k])===0){ ok=true; break; }
|
||
}
|
||
if(ok) break;
|
||
}
|
||
}
|
||
return ok;
|
||
}
|
||
|
||
function renderProject(d){
|
||
/* 🔴🔴 2026-10-04(用户:「统一完成情况的状态标准,不要换个工作区换个目标就统计不准确」):
|
||
**`_` 开头的键是说明行**(`_说明`/`_更新`…),⛔ 不是判据 ⇒ **不许进分母**。
|
||
旧写法 `keys=Object.keys(acc)` 把它们算进分母 ⇒ 通过率**永远到不了 100%**
|
||
(实测真源 14/15 真判据全过,却显示 `14 / 17 通过`)。
|
||
⇒ 与后端 `board.py`(L1616 已剔 `_`)/`collabd.py`(L2613 已剔)**同一条口径**:
|
||
**分母=有效判据条数**。三处必须同款。 */
|
||
/* 🔴🔴 2026-10-05 修 **`acc is not defined`**(用户报障:「快照读取失败:acc is not defined」)。
|
||
真因=**变量从来没声明过**:本函数收到的实参是 `paintScope()` 传来的
|
||
`scopeView(d, g)` 视图对象,而**验收判据不在顶层** —— 它挂在 `goal.acceptance`
|
||
(实测快照顶层键里**没有** `acc`,只有 `goal.acceptance`)。
|
||
上一版直接写了 `acc` ⇒ `ReferenceError` ⇒ `.catch()` 把**渲染期异常**当成
|
||
**取数失败**来报 ⇒ 页面显示「快照读取失败」+「服务还开着吗」——
|
||
**把"代码 bug"伪装成"服务挂了"**,排查方向整个被带偏(同族 P0:假归因)。
|
||
✅ 取法与 tab 摘要(本文件 `renderTabs` 的 `var acc=(g.goal||{}).acceptance||{}`)
|
||
**同款** —— 同一份数据⛔ 不许两种取法。
|
||
⚠️ 教训:**`.catch()` 里一律标清是"取数失败"还是"渲染失败"**,⛔ 别把两者混成一句话。 */
|
||
var acc=(d.goal||{}).acceptance||{};
|
||
var keys=Object.keys(acc).filter(function(k){return k.charAt(0)!=='_'}).sort();
|
||
var noteKeys=Object.keys(acc).filter(function(k){return k.charAt(0)==='_'}).sort();
|
||
/* 🔴🔴 2026-10-05 修(**静默错文案**,与上面 `acc` 同族、但更隐蔽):
|
||
实测快照顶层**没有** `short`/`title`/`criteria`/`id`/`main_sid8`/`workspace`/`topics*`
|
||
—— 它们全挂在 **`d.project`** 下(`goal` 那份只装 `acceptance`)。
|
||
⇒ 原来裸写 `d.xxx` 一律取到 `undefined` ⇒ **不抛异常**,只是**悄悄显示兜底值/'—'**:
|
||
「目标(未声明主题)(未声明目标)」+ 归属判据全 '—'(用户 2026-10-05 报障原话)。
|
||
🔴 判据:**同一份数据⛔ 不许两种取法** —— 一律 `pj=d.project||{}` 取。 */
|
||
var pj=d.project||{};
|
||
var src=pj.topics_source||{}, mbt=pj.main_by_topic||{}, tops=pj.topics||[];
|
||
/* ── ① 目标(主角)── */
|
||
/* 🔴🔴 2026-10-05 修 **「目标(未声明主题)(未声明目标)」**(用户报障原话)。
|
||
真因=**取值层级错**:目标数据不在顶层 —— 它挂在 `goal` 下(`d.goal.short` / `d.goal.title`)。
|
||
实测快照:顶层 `short`/`title` **都是 None**,`goal.short='本机协作'`、`goal.title=完整目标`。
|
||
本行上一版写的是裸 `d.short` / `d.title` ⇒ 永远取不到 ⇒ **恒显兜底文案**
|
||
⇒ 用户看到「目标(未声明主题)(未声明目标)」,而**数据其实完整**。
|
||
⚠️ 与上面 `acc is not defined` 是**同族病**:那里是"少写一层(直接写 `acc`)"⇒ 抛异常;
|
||
这里是"少写一层(直接写 `d.short`)"⇒ **不抛异常、静默显示错文案**(更难发现)。
|
||
✅ 正解=与 `acc`/`renderTabs` **同款取法**:一律走 `d.goal`。
|
||
🔴 判据:**同一份数据⛔ 不许两种取法**(本文件里 `d.goal.*` 已是既有口径)。 */
|
||
var gj=d.goal||{};
|
||
/* 🔴🔴🔴 2026-10-05 **目标标题前面带上「所属工作区名」**(用户逐字:
|
||
「看板里面应该明确指出 对应的 工作区 和 目标」)。
|
||
为什么必须加:看板把**别的工作区**也并列成 tab(peer 格),而**各目标标题常常同一句**
|
||
(或同一模板换参数),⚠️ **tab 名与标题都不足以让读者一眼确认"这是哪个工作区的"**
|
||
⇒ 用户要"明确"。
|
||
✅ 口径(与 `ws_name` 字段同一条来源,⛔ 不另造一份):
|
||
· **peer 格**(`d.peer` 有值)⇒ 前缀用**对方工作区真实目录名**;
|
||
· **本区格**(`peer` 为空)⇒ 前缀用**本工作区真实目录名**(`d.ws_name`)
|
||
—— 用户要"明确",本区**也要写**,⛔ 不搞"自己就不标"的特例。
|
||
⚠️ `d.ws_name` 已由后端保证是**真实目录名**(⛔ 不是"本工作区"这种占位词,见 `_goal_block()`)。
|
||
⛔ 兜底 `'—'`:宁可显示"未知",⛔ 也不许拼出半个假名。 */
|
||
var _wsn=String(d.ws_name||'').trim()||'—';
|
||
var goalHtml='<div class="projGoal"><span class="critLead">目标</span>'
|
||
+'<span class="pill" title="所属工作区">'+esc(_wsn)+'</span>'
|
||
+'<b>'+esc(gj.title||'(未声明目标)')+'</b></div>';
|
||
/* ── ② 完成情况(第二主角)──
|
||
⚠️ 2026-09-30:**没有有效验收判据时 ⛔ 不许显示成"无验收记录"那种中性字样** ——
|
||
判据是"**有没有有效项**",不是"字典空不空"(只写 `_说明` 行时旧代码会当成"全过")。
|
||
⚠️ 非 pass 的**逐条点名** —— 只写"4/5 通过"看不出**还差什么**(用户要的是"看完成情况")。 */
|
||
/* 🔴🔴 2026-10-02 修「恒算 0 通过」的假红(用户报:「还是0通过」)。
|
||
原判据 `String(acc[k]).toLowerCase()==='pass'` **只认英文 `pass`**,而 goal.json 的
|
||
`acceptance_state` 真值写的是**中文**:`过(…)`/`**复核:过(…)**`/`待重验(…)`
|
||
⇒ 前端恒算 **0 / N 通过**。后端 `board.py::_acc_is_pass` 当天 05:4x 已修、**这里漏同步**
|
||
⇒ 同族「判据写死期望值、永远不命中真源」(pitfalls P0-13/P0-20)。
|
||
🔴 **两处判据必须同款** ⇒ 改这里就去看 board.py 那一个函数,反之亦然。 */
|
||
var pass=keys.filter(function(k){return accIsPass(acc[k])}).length;
|
||
var bad =keys.filter(function(k){return !accIsPass(acc[k])});
|
||
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 accLead">未完:</span>'
|
||
+'<ul class="accList">'+bad.map(function(k){
|
||
return '<li><code class="accKey">'+esc(k)+'</code>'
|
||
+'<span class="accWhy">'+esc(acc[k])+'</span></li>'}).join('')+'</ul>'
|
||
: '<span class="oldline">全部通过</span>')
|
||
+'</div>';
|
||
}else{
|
||
accHtml='<div class="accRow"><span class="critLead">完成情况</span>'
|
||
+chip('⚠️ 未声明验收判据(⛔ 不因此判完成)','is-warn')+'</div>';
|
||
}
|
||
/* 🔴 2026-10-04:只有说明行时**补一句**它们不是判据 —— ⛔ 否则读者看到「未声明判据」会以为
|
||
目标没写任何东西,而其实写了 `_说明`/`_更新`。措辞与后端 `_acc_summary` 同向。 */
|
||
if(keys.length===0&¬eKeys.length){
|
||
accHtml+='<div class="accRow"><span class="oldline">另有 %d 条说明行(%s)⇒ ⛔ 不计入通过率</span></div>'
|
||
.replace('%d',noteKeys.length).replace('%s',esc(noteKeys.join('、')));
|
||
}
|
||
/* 🔴🔴 2026-10-05 **删掉「任务类别」整行**(用户逐字:「任务类别(声明任务类别)这个没用就删除 看着烦」)。
|
||
依据(⛔ 不是口味,是可核对的读数):
|
||
① **它对用户没有操作价值** —— 这行的事实在「协作架构」那块**已经画了**
|
||
(`TOPS` + `TSRC` + `main_by_topic` 一起渲染成有向图,见下方 `TOPS`/`TSRC` 段);
|
||
这里再摆一遍 chips 属**同一份数据两处渲染**(同族红线)。
|
||
② **它最容易说假话** —— 实测:`topics=['会话协作自检']`、`topics_source.kind='declared'`,
|
||
但 `main_by_topic` 为空时,这行渲染出「任务类别 ⚠ 未声明」⇒ **明明声明过却说没声明**。
|
||
⚠️ 本轮已把 `main_by_topic` 的默认类别兜底修好(`board.py::_resolve_main`),
|
||
"未声明"这条假话没了;但**重复渲染**这条仍在 ⇒ 按用户口径删。
|
||
③ 用户 2026-10-01 已定口径:「**看板要精简的展示重点有效的信息,不是写说明书**」。
|
||
⛔ 删的是**版面这一行**;`tops`/`mbt`/`src` 三个变量**保留**(下方架构图仍在用,⛔ 别顺手删变量)。
|
||
⚠️ 类别清单与其来源**一条都没丢** —— 全在「协作架构」图 + `?` 提示里。 */
|
||
document.getElementById('proj').innerHTML = goalHtml + accHtml;
|
||
|
||
/* 🔴🔴 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=(pj.criteria||[]).map(esc).join(' / ')||'—';
|
||
tip.innerHTML+='<br><br><b>这一格目标的依据</b>'
|
||
+'(目标 id <code>'+esc(pj.id||'—')+'</code>'
|
||
+' · 默认主会话 <code>'+esc(pj.main_sid8||'—')+'</code>'
|
||
+' · 工作区 <code>'+esc(tailOf(pj.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(**个数是活的**,按本项目在册会话渲染)
|
||
④ 协作 + 上报(旧名「监督程序」,并排) ⑤ Hook进程 + 底部一条「宿主地基」带
|
||
🔴🔴 2026-10-01 **第四改**:上面这些**不全是"层"了** —— 用户要求「用一个**虚线大框**把
|
||
主会话 唤醒会话 和 跟进会话都框起来,这个虚线大框 **连接 协作会话 虚线大框**就好」
|
||
⇒ 版面上现在有**两个虚线分组大框**(`.grp`):
|
||
`UA` = ① 用户下头那一坨(**主会话 + 唤醒 + 跟进**)—— 机制侧的三个角色同组;
|
||
`UB` = 任务会话那一排(**个数活的那一层**);
|
||
两个大框之间**一条线**=① 派活(**取代**了原来"主会话往每一格射线"的扇形)。
|
||
⚠️ 分层数没变(还是那几层),变的是**读图方式**:以前是"一层层看",现在是
|
||
"**先看两个组、再看组里几格**";大框⛔ 不填色、线细一点虚一点 ⇒ 表示"这是**分组**,不是节点"。
|
||
⚠️ 🔴🔴 **2026-10-03 会话只两类**(主/协作)—— 唤醒、跟进、**队列上报**整套退役。
|
||
本行原写"共**四类**(主/协作/唤醒/跟进)"**已作废**。
|
||
⚠️ `UA` 那个虚线大框**也已删除**(它原本是为了把"主+唤醒+跟进"圈成一组;
|
||
唤醒与跟进退役后框里只剩一格 ⇒ 一个只装一格的分组框是**噪声**)⇒ 现在图上**只剩 `UB` 一个 `.grp`**。
|
||
每条边都走**空白带**(⛔ 不横穿节点框):
|
||
① 派活带(两个大框之间那 58px) ② 写台账带(任务会话 → 检查程序 `--report`)
|
||
④ 收结果走地基带上方 ⛔ **原 ③「上报走右侧竖井」已删**(那条线的收件人是跟进会话,已退役)
|
||
⚠️ 走线带的 y **一律自动取中**(`B1/B2/B3` 由行位算出)⇒ ⛔ 别在注释或代码里写死数字
|
||
(写死过一次:改布局后这里还标着旧值,读的人按旧值去图上找,找不到)。
|
||
🔴 每格都带真状态:会话取宿主库 `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-10-01 加(用户:「**通过接续会话的时候,如何处理过期的会话,避免越堆越多**」):
|
||
**已退场/超窗口**的会话。它们**不进 `S`**(⇒ ⛔ 不进第三层、⛔ 不进分工板、⛔ 不进「会话」表),
|
||
**但必须报数** —— 否则读者会以为那些棒**凭空消失了**(同族红线:读到了却不说)。
|
||
⚠️ 退场 ≠ 删除:宿主库里原样在;判据与写入口在服务端
|
||
(`board.py::_sessions()` 的退场筛 / `collabd.py --retire self`),
|
||
⛔ 本文件**一个字都不许自己判**(判据两处打架是本项目踩过最狠的一类坑)。 */
|
||
var RET=d.sessions_retired||{};
|
||
/* 🆕 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'});
|
||
/* 🔴🔴 2026-10-01 修:这一排=**任务会话**,判据从 `role !== '主会话'` 改成 **`role === '任务会话'`**。
|
||
原判据**把唤醒会话与第④类跟进会话也收进来了**(服务端原来二分:不是主会话就是任务会话)⇒
|
||
唤醒会话会**画两遍**(主会话左侧一次、这排一次)、跟进会话**顶着错名字**。
|
||
已同步:`board.py::_role_label()` 现在输出**四类**(主/协作/唤醒/跟进)。
|
||
⚠️ 两者**必须同款**,⛔ 改一边不改另一边 = 这排又开始吃错格子。 */
|
||
/* 🔴🔴 2026-10-03 11:1x 这一排**只放协作会话**,⛔ **检查会话不许进来**(用户纠正逐字:
|
||
「检查会话不是协作会话,不应该出现在看板协作会话区域中」)。
|
||
⚠️ 服务端已把它单列到 `sessions_checks`,但这里用的是全量 `S` ⇒ **必须再挡一次**
|
||
(⛔ 两处只靠「服务端单列」会漏:任何把检查会话并回 S 的改动都会让它复活)。
|
||
⇒ 判据=`role==='任务会话'` **且** 标题里**不含**「检查」。 */
|
||
// 🔴🔴 2026-10-03 23:4x 改名:「协作目标」→「执行会话」(用户定案)⇒ **判据跟着标签走**
|
||
// ⚠️ 这个字符串**必须与 `board.py::_ROLE_LABEL['worker']` 逐字相同**
|
||
// ⛔ 只改一边 ⇒ 这一排**整排空掉**(19:2x 已踩过一次,根因就在这行)。
|
||
var works=S.filter(function(x){return x.role==='任务会话' && String(x.title||'').indexOf('检查')<0});
|
||
/* 🔴🔴 2026-10-01 用户定 —— **这一排第三次换语义**(前两次的来历见 §0.5.6):
|
||
「**主会话 下面 那一排只放协作会话**,横着排 有几个放几个,当前没有对应协作会话 就空着。
|
||
**唤醒机制如果是主会话的 事就放到主会话框里去**」
|
||
⇒ 这一排 = **任务会话**(判据 `role === '任务会话'`,⛔ 不再按"任务类别"铺格):
|
||
· ⚠️ **任务类别**:曾按用户 2026-10-01「**唤醒机制如果是主会话的 事就放到主会话框里去**」
|
||
搬进 R2 主会话框(一行「负责类别:…」);**同日又删掉了** —— 理由见 `② 主会话` 段注释。
|
||
⇒ 现在任务的类别**只在两处**说:顶部「本项目」卡的 **chips** + 图外说明「当前」块第 1 条。
|
||
· 🔴 台账里的**旧线(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||[];
|
||
/* 🆕 2026-10-01:**类别的来源**(`declared`/`fallback`/`none`)。
|
||
⚠️ `TOPS` 自己分不出"在对话里声明过"和"按目标简称回落来的" —— 而这两件事**必须分开说**
|
||
(见下方「当前」块第 1 条的注释:混在一起就会把回落值当成已声明的类别)。 */
|
||
var TSRC=(d.project||{}).topics_source||{};
|
||
var working=S.filter(function(x){return x.status==='working'}).length;
|
||
var o=[];
|
||
|
||
/* 🔴 两类"让主会话动起来"的外部来源,⛔ 别混为一谈:
|
||
· **Hook进程**(R5 那格)—— **事件驱动**:WorkBuddy 在钩子时机起一个短命子进程,跑完即退;
|
||
· **唤醒 等**(画在 `RW` 行的**左格**)—— **时间驱动**:由 `front.triggers` 给的那条周期任务。
|
||
用户 2026-09-30 三连:「把心跳的节点也放到看板协作架构图中」→「把唤醒 图框做小一点 放到
|
||
主会话左边就行 本来就是主会话创建的」→「把心跳 改为 唤醒 里面是每5分钟核对目标状态」。
|
||
⚠️ 它们**不由技能侧写死**(⛔ 技能里不出现任何项目的组件名)——
|
||
由**使用方**经 `front.triggers` 给,形状:
|
||
`[{name, detail, detail2, label, up, edge, tip}]`。空 ⇒ `RW` 行左格不画格子。 */
|
||
var TG=(d.front&&d.front.triggers)||[];
|
||
/* 🔴 行距(用户 2026-09-30:「摆不下就**加高**些,线都没连全 有些线都看不到」)
|
||
⇒ **只加高、⛔ 不加宽**:`svg.arch{width:100%}` ⇒ 缩放比由**宽度**定 ⇒ 加高**不会**把字变小,
|
||
只是每层之间更宽敞(走线带、拐点、标签都放得下)。 */
|
||
/* 🔴🔴 2026-10-01 **第三改布局**(用户原话逐字):
|
||
「可以把 **唤醒会话和 跟进会话 单独放一行** / **主会话** / **换新会话 跟进会话** / **协作会话**」
|
||
⇒ 第二层**拆成两行**:`R2` = **主会话**(独占一行);
|
||
`RW` = **唤醒会话 + 跟进会话**(**单独一行**,左=唤醒、右=跟进)。
|
||
⚠️ **为什么要拆**(这不是纯美观):本轮口径把「唤醒」与「队列上报」**都改指跟进会话**
|
||
⇒ 若跟进会话还贴在主会话**右边**,唤醒那条线就得**从主会话左边跨过主会话框**才能连到它
|
||
(画出来是一根跨框的绕线,读者多半会以为连的是主会话 —— 正是本轮要消灭的误读)。
|
||
拆成单独一行后:唤醒 → 跟进 是一条**同一行的短横线**,谁叫谁一眼看清;主会话上方
|
||
**只剩"用户"这一条竖线**(="主会话只能是用户触发"在图上成立)。
|
||
⚠️ 新行插在 R2 与 R3 之间 ⇒ **R3 起整体下移 94**,`viewBox` 同步加高
|
||
(⛔ 不同步 ⇒ 底部地基带被裁,几何自检 ① 段会报越界)。 */
|
||
/* 🔴🔴 2026-10-01 **第四改布局**(用户原话):
|
||
「先等等 有个办法更好,用一个**虚线大框**把 主会话 唤醒会话 和 跟进会话都框起来,
|
||
这个虚线大框 **连接 协作会话 虚线大框**就好」
|
||
⇒ 多两个**分组大框**(`.grp`,虚线、不填色):`UA` = 主会话+唤醒+跟进;`UB` = 任务会话;
|
||
**两组之间一条连线**(① 派活)**取代**原来"主会话往每一格射线"的扇形。
|
||
⚠️ 这一改顺手**消掉了两个麻烦**(⛔ 不是巧合,是分组本身带来的):
|
||
· ① 派活再也不必**穿过 RW 那一行**去找格子 ⇒ 它只剩 UA 底 → UB 顶那一段(58px);
|
||
· 唤醒 → 跟进 那条实线再也不与 ① 竖线**十字相交** ⇒ 上一版担心的"十字交叉"不存在了,
|
||
用户那句「**增加连接…的实线**」落到实处(一条干干净净的直线)。
|
||
⛔ `R2` 的上边**不能再往上**:R1 底 116 + 用户→主会话 那条箭头 + 它的标签都要塞进 40px。
|
||
⚠️ `UA` 的**上边也顶在 156**:再往上就贴到那条"用户→主会话"的箭头与标签(`y≈116..176`)。
|
||
⚠️ ⛔ 别把 `UA/UB` 当成"又加了两层" —— 它们**不占层**,只是把已有几层**套起来**
|
||
⇒ `rows` 镜像(几何自检)里**没有** UA/UB,那两个框是**单独**用包围盒判的。 */
|
||
// 🔴🔴 2026-10-03 **删掉主会话外面那个虚线大框 `UA`**(用户逐字:「主会话周围的虚线框也去掉」)。
|
||
// 理由(不是版面口味):那个框原本是**为了把"主会话+唤醒+跟进"三个角色圈成一组**才存在的;
|
||
// 唤醒与跟进退役后**框里只剩一格** ⇒ 一个只装一格的"分组框"是**无意义的框**(=噪声)。
|
||
// ⇒ 只保留 `UB`(框住任务会话那一排 —— 那一排**确实有多格**,分组有意义)。
|
||
// ⚠️ 连带改:`B1`(派活带走线带)改成 **R2 底 → UB 顶**;派活线起点改成 `R2` 底;
|
||
// `RKX` 里避让 `UA` 的那一项删掉(框没了,⛔ 留着就是减一个不存在的边界)。
|
||
var UB={y:300,h:178}; // 任务会话那排的分组大框(⛔ UA 已于 2026-10-03 整框删除)
|
||
/* ⚠️ `UB` 的横向尺寸**不在这里**(`x/w` 由 `③` 段现算)——
|
||
它取决于任务会话的**条数**与**格宽**(写在这儿会变成"两处各存一份",改一处另一处就过期)。 */
|
||
// 🔴🔴 2026-10-03 **删掉 `RW` 整行**(用户逐字:「让你把 架构图里面的 唤醒和跟进 以及上报都删除」)。
|
||
// 唤醒会话 / 跟进会话 / 队列上报**已整套退役** ⇒ 图上**不再画那一行**(⛔ 不是画成"已退役"灰格 ——
|
||
// 用户要的是**删除**;灰格那版是上一轮的选择,本轮按用户明确要求推翻)。
|
||
// ⇒ `R2`(主会话)底 → `R3`(任务会话)顶**直接相连**,`UA` 与 `UB` 之间不再夹那一行。
|
||
// ⚠️ 删行后 **`R3` 起整体上移 94**(= RW.y 300 − R2 底 260 + 56 间距),
|
||
// `viewBox` 同步缩高 94(原 1054 → **960**);⚠️ 几何自检的 `rows` 镜像**必须同批改**。
|
||
var R1={y:36,h:80}, R2={y:176,h:84}, R3={y:322,h:134},
|
||
R4={y:538,h:146}, R5={y:718,h:96}, RB={y:850,h:56};
|
||
/* ⚠️ R2 高度来回:84 → 112(2026-10-01 加第四行「负责类别:…」)→ **回到 84**(同日删掉那行)。
|
||
原 84 = 三行文案(标题 / 名称 / 状态)刚好装下;加第四行才抬到 112。
|
||
🔴 删行的理由见下方 `② 主会话` 那段注释 —— 一句话:**那行会说出假话,且同一事实别处已有两处**。
|
||
⚠️ `tmp/arch-geom-check.mjs` 的 `rows` 镜像**必须逐行同步**(那文件里写着:「必须与 board.html
|
||
同步,否则几何自检=假绿」)—— 现在共 **6** 行:R1 / R2 / R3 / R4 / R5 / RB
|
||
(⛔ `RW` 那一行 2026-10-03 随唤醒/跟进/上报退役**整行删除**)。 */
|
||
var CX=640, SX0=60, SX1=1220; // 会话行 60..1220(⚠️ 右侧竖井 x **现算**,见 ③ 段;⛔ 别在这儿写死)
|
||
/* 🔴 三条走线带**自动取中**(⛔ 别写死):B1=**UA 底 → UB 顶**(① 派活带,2026-10-03 起
|
||
**不再需要穿过 RW 那一行的空白** —— 那一行已整行删除 ⇒ 直接取两个分组大框之间那一段)。 */
|
||
/* ① 派活带 = **两个分组大框之间**那一段(2026-10-01 分组后:不再从主会话往每一格射线);
|
||
② 上报带 = 任务会话那排底 → 常驻程序顶(⛔ 不受 UB 边界影响,它本来就穿过去)。 */
|
||
var B1=(R2.y+R2.h+UB.y)/2, B2=(R3.y+R3.h+R4.y)/2; // 🔴 2026-10-03:UA 已删 ⇒ B1 改 R2 底→UB 顶
|
||
var B3=(R4.y+R4.h+R5.y)/2; // R4↔R5 之间的走线带(⛔ 别写死 779)
|
||
/* 🔴🔴 2026-10-03 **④ 这一行原本是「两格」:左=常驻程序、右=投递**(用户 2026-10-03:
|
||
「**不是 协作程序常驻吗**」)。
|
||
🔴 **两格是「投递时代」的遗留** —— 当时真的**有两个进程**(`--once` 投影 + `--supervise` 投递)。
|
||
投递退役后**只剩一个进程** ⇒ 拆两格是**历史噪声**,且右格那个名字
|
||
(我一度写成"常驻")是**用模糊词替代了具体词** —— 角色本来就叫**常驻程序**。
|
||
✅ **处置=合并成一格**(⚠️ 20:05x 已**收窄**到 470;20:16x 改为**跟随画布中心** `PX=CX-PW/2`),标题「协作程序」;⛔ 不留"常驻"这个第二节点。
|
||
⚠️ 合并后 `UX` 变量**已删**(⛔ 别再引用);`PW` 由 300 → **720**。
|
||
⚠️ `RKX`/`TICKX` 两处随之改算(原来它们指向右格中心)—— 见下方各自注释。 */
|
||
// 2026-10-03 20:16x **水平居中**(用户:「改完了协作程序水平居中」)
|
||
// ⚠️ 之前 `PX=280` 是"两格合一"时期的值 ⇒ 框 280..750、**中心 515**,而画布中心 `CX=640` ⇒ **肉眼偏左**。
|
||
// ✅ 改成**跟着画布中心算**(⛔ 不写死 405:CX 变了它自动跟随)。
|
||
// ⚠️ 联动:`PX+PW/2` 的连线中点、右侧标签 x(按连线现算)**都会自动跟随**,⛔ 不必逐个改。
|
||
// 📌 对照:`R1`/`R2`(用户/主会话)本来就居中(`x1=CX-w1/2`),`Hook进程` 框 400..890 中心 645 ≈ 居中
|
||
// ⇒ **此前只有常驻程序这一格偏左**,现已与其余元素同一中轴。
|
||
var PW=470, PX=CX-PW/2;
|
||
// ⚠️ 依据=**框内实际用到的最右位置**(实测:队列那行约 240px、检查会话条目约 288px ⇒ 收窄到 470 仍有余量)
|
||
// ⚠️ 之前是 720(280..1000),因为**要并排放 4 条检查会话**;改成「只显示最新一条」后右侧空了一大半 ⇒ 收窄。
|
||
// 🔴 `PX+PW` 被**别处**引用(连线中点、右侧节点位置):它们会**自动跟随**,⛔ 不必逐个改;
|
||
// 但凡**写死 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,tip){
|
||
/* 🔴 2026-10-03 19:55x `tip`=**悬停提示**(SVG 原生 `<title>` 子元素,⛔ 不需要 JS 事件)。
|
||
⚠️ 必须放在 `<text>` 的**第一个子元素**(SVG 规范),且内容要 `esc()` 转义。
|
||
用途:版面只留**最新一条**,其余信息**移上去才看得到**(用户要求:别的进下拉/悬停)。 */
|
||
o.push('<text class="'+(cls||'n-sub')+'" x="'+x+'" y="'+y+'"'+(anchor?' text-anchor="'+anchor+'"':'')+'>'
|
||
+(tip?'<title>'+esc(tip)+'</title>':'')+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-10-01 **第四改:两个虚线分组大框**(用户逐字原话)——
|
||
「先等等 有个办法更好,用一个**虚线大框**把 主会话 唤醒会话 和 跟进会话都框起来,
|
||
这个虚线大框 **连接 协作会话 虚线大框**就好」
|
||
⇒ `UA` = 主会话+唤醒+跟进(**机制侧那三个角色本来就在同一组里**:用户只进主会话,
|
||
机制侧的"推"只投/叫醒跟进会话);`UB` = 任务会话那一排(在下面 `③` 段画)。
|
||
🔴🔴 **UA 必须画在 ① 用户之前** —— SVG 靠**文档顺序**叠放,后画的压住先画的;
|
||
大框要垫在里面的会话格**下面**(⛔ 反了 ⇒ 虚线横穿格子上的文字,整块糊掉)。 */
|
||
/* 🔴 里面那三格的**几何必须先算出来**(⛔ 不能等画格子时再算)——
|
||
因为 `UA` 的包围盒就是从它们推的,而 UA 又要先落笔。
|
||
⚠️ **只留这一处**:这三个常量以前在 `② 主会话` 与 `②′` 段里各写一份 ⇒
|
||
改一处另一处必然过期(本轮已栽过这个坑)⇒ 统一提到这里。 */
|
||
var w2=380, x2=CX-w2/2, in2=w2-40; // 主会话那一行(R2)的横向几何
|
||
// 🔴🔴 2026-10-03:`TW/FW/RW_GAP/tbx/fbx/fin/RW_Y/RW_H/RW_MID` **全部删除**
|
||
// (唤醒会话 / 跟进会话 / 队列上报**整套退役** ⇒ 图上不再有那一行)。
|
||
// ⛔ 保留这些变量=保留一条"看起来还能用"的死路 ⇒ 后人会拿它当契约。⛔ 所以**真删**。
|
||
// 🔴🔴 2026-10-03 同批**删掉 `UA` 那个虚线大框**(用户:「主会话周围的虚线框也去掉」)
|
||
// ⇒ 它的包围盒计算(`UA_PAD/_uL/_uR/UAx/UAw`)与绘制一并删除。
|
||
// ⛔ 若将来要恢复:**按本段下面的定义重写**(`UA_PAD/_uL/_uR/UAx/UAw` + 绘制)
|
||
// **并**要恢复 `RW` 那一行(`UA` 原本是"框住三角色"的,画出来只有一格就是噪声框)。
|
||
|
||
// ── ① 用户 —— 🔴 写**职责 + 可执行动作**(用户 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
|
||
// 🔴🔴 2026-10-01 本轮两次改动落点:
|
||
// ① 用户:「**就是把主会话 跟进职责 放给跟进会话了**」⇒ 状态行**只说「判断 + 派活」**,
|
||
// ⛔ 不再提「跟进」(职责已移交;同一职责两处都写,读者就分不清谁在干)。
|
||
// ② 用户:「**唤醒会话和 跟进会话 单独放一行**」⇒ 跟进会话**不在这格里**了,
|
||
// 搬到 `RW` 那一行(见下方 `②′ 唤醒 + 跟进(RW 行)`)。
|
||
// ⚠️ `w2/x2/in2` **不在这里声明** —— 已提到上方(`UA` 的包围盒要用它们)⇒ ⛔ 别写第二份。
|
||
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 **删掉了原来贴在这里的那行「负责类别:…」**
|
||
(用户原话:「**这个是什么意思,如果没用就删掉**」)。
|
||
原实现:用 `TOPS` 按 `main_by_topic` **反查**这条主会话负责的类别,查不到就渲染
|
||
「负责类别:—(未在对话里说明)」并用警示色。**判它"没用且会误导"的三条依据**(实测,⛔ 不是口味):
|
||
① **它会说出假话** —— 实测读数 `project.topics = ['会话协作自检']`、
|
||
`topics_source.kind = 'declared'`(20:25 在对话里说明过),但 `main_by_topic = {}`
|
||
⇒ 这行渲染成「**未在对话里说明**」。**类别明明声明过**,它却说"没说过" ⇒ 假。
|
||
(真因是"**这条**主会话没对应到任何类别",与"有没有说明过"是两件事,原判据把两者混成一个。)
|
||
② **同一事实版面上已说过两处,而且方向更对**:
|
||
· 顶部「本项目」卡的**任务类别 chips** —— 每个 chip 直接带「主会话 `<id8>`」/「⚠️ 无主会话」,
|
||
`?` 里还带来源(谁说的、什么时候说的);
|
||
· 图外说明「当前」块里的「主会话**负责的任务类别**:…」。
|
||
⇒ 这一行是**第三遍**,还是**反方向**(主会话→类别),读者得倒着对一遍。
|
||
③ 用户反复要求「**看板要精简的展示重点有效的信息,不是写说明书**」。
|
||
⚠️ 它当初是照用户 2026-10-01「**唤醒机制如果是主会话的 事就放到主会话框里去**」搬进来的
|
||
⇒ 本次删除**推翻了那次摆放**;理由就是上面三条:**该说的话别处都在说,而且说得更准**。
|
||
⛔ 还原点:`E:/ProgramData/AIProject/ai1net-dsh-server/tmp/bak-负责类别行-20261001/board.html.bak`
|
||
(要恢复 ⇒ 把这段换回那三行,**并把 `R2.h` 一起改回 112**)。 */
|
||
|
||
// 用户 → 主会话 —— 🔴🔴 这是**主会话的唯一入口**(用户 2026-10-01:「**主会话只能是用户触发**」)
|
||
// ⇒ 标签必须把"唯一"写出来:图上别处**没有任何一条线指向主会话**(唤醒、队列上报都已改指跟进会话)
|
||
// ⇒ 写清"唯一"= 把**图上画出来的事实**说出口,⛔ 不是修辞。
|
||
edge('M'+CX+' '+(R1.y+R1.h)+' L'+CX+' '+R2.y);
|
||
lbl(CX+56, (R1.y+R1.h+R2.y)/2+3, '发消息 · 看板(主会话的唯一入口)', 'start');
|
||
|
||
/* 🔴 2026-10-01 **第四改**:原来**这里**画的是「① 派活」的**竖段**(主会话底 → 走线带)。
|
||
它当时必须**先于 RW 行**落笔 —— 因为要压在 `②′` 那条横线的**跨线跳弧**下面。
|
||
⇒ 分组大框落地后,**提前的理由和那段竖线一起没了**:① 现在只是「`UA` 底 → `UB` 顶」
|
||
那一小段(两个大框之间),不再穿过 RW 那一行。它挪到下方 `③` 段(`UB` 落笔之后)。
|
||
⚠️ 同时作废的还有 `②′` 里那句"与 ① 竖线十字相交" —— 那条横线现在谁也不压。 */
|
||
|
||
// ── ②′ 原「唤醒 + 跟进」那一行:**2026-10-03 整行删除**(用户逐字:「让你把 架构图里面的
|
||
// 唤醒和跟进 以及上报都删除」)⇒ 原 857–968 这 112 行**真删**,⛔ 不留代码也不留死变量
|
||
// ( 已一并删 ⛔ 留着等于留一条假契约)。
|
||
// ⇒ 图形顺序变成:用户 → 主会话(UA 组)→ **直连** 任务会话(UB 组)→ 常驻程序。
|
||
// ── ③ **任务会话**(🔴 2026-10-01 用户定:「**这一排只放协作会话**,横着排 有几个放几个,
|
||
// 当前没有对应协作会话 就空着」+「放一个空的框 说明 暂无协作会话」)——
|
||
// ⚠️ 这一排**换过两次语义**(按线 → 按任务类别 → 现在是**按任务会话**):
|
||
// · ⚠️ **任务类别**:曾在 2026-10-01 搬进 R2(一行「负责类别:…」),**同日又删掉了**
|
||
// (用户:「**这个是什么意思,如果没用就删掉**」)⇒ 现在类别**只在两处**说:
|
||
// 顶部「本项目」卡的 **chips** + 图外说明「当前」块第 1 条;
|
||
// · 台账旧线(legacy)的汇总 ⇒ 已搬到**图外说明**(⛔ 不静默丢)。
|
||
// 每格=**一条任务会话**;格内:会话名 / 状态·id / 所属类别 / 多久没动。
|
||
var cx=[], n=cells.length;
|
||
/* 🔴 2026-10-01 **第四改**:**先算布局预算、先画分组大框 `UB`,最后才画格子** ——
|
||
顺序不能反:SVG 靠**文档顺序**叠放,大框必须垫在格子**下面**(反了 ⇒ 虚线横穿格子上的文字)。
|
||
⚠️ `UB` 的包围盒**贴在内容上**(留 `UB_PAD` 内边距),⛔ 不写死成 `SX0..SX1` 那么宽 ——
|
||
只有两条任务会话时,那会画出一个 1160 宽、里面空一大半的假框:既说不出"这一组有几条",
|
||
又比上面那个框宽一大截,会被读成"下面这组更重要"(⛔ 用户要的是**分组**,不是主次)。
|
||
⚠️ 改 `gap / w / SX1 / UB_PAD` 任何一个都要**回来重算 UB**(自检里有一条守"UB 包住所有格子")。 */
|
||
var UB_PAD=18, UBx, UBw;
|
||
if(n===0){
|
||
UBx=CX-240-UB_PAD; UBw=480+2*UB_PAD; // 空态:包住那个 480 宽的空框
|
||
}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;
|
||
UBx=sx-UB_PAD; UBw=total+2*UB_PAD;
|
||
}
|
||
// ── `UB`:任务会话那一组(虚线大框,⛔ 不填色;画在格子与 ② 连线**之前**)
|
||
o.push('<rect class="grp" x="'+UBx+'" y="'+UB.y+'" width="'+UBw+'" height="'+UB.h+'" rx="20">'
|
||
+'<title>'+esc('一组:任务会话 —— 主会话建自动化排期、宿主到点开出来的棒。'
|
||
+'有几条画几条,⛔ 不是固定几格。')+'</title></rect>');
|
||
/* ── 🔴 **① 派活 = 两个分组大框之间的一条线**(2026-10-01 第四改)——
|
||
用户:「这个虚线大框 **连接 协作会话 虚线大框**就好」⇒ 它**取代**了原来"主会话往每一格
|
||
射线"的扇形。⛔ 这不只是为了少画几条线:
|
||
· 扇形要从主会话**穿过 RW 那一行的空档**再撒到各格 ⇒ 读者得顺线倒推"到底谁派的";
|
||
· 而且**空态时那条通道一个落点都没有**(上一版只能画一个孤零零的标签,看不出通向哪)。
|
||
现在它就是一条 58px 的竖线(`UA` 底 → `UB` 顶),**空态照画**
|
||
(同族红线:空态 ≠ 通道不存在,只是这条通道上暂时没有会话)。
|
||
⚠️ 它落在**格子之前**,但两头都在两个大框的边上、**不与任何格子相交** ⇒ 叠放无所谓。 */
|
||
edge('M'+CX+' '+(R2.y+R2.h)+' L'+CX+' '+UB.y); // 🔴 2026-10-03:UA 已删 ⇒ 起点改 R2 底
|
||
lbl(CX+14, B1+4, '① 派活 · 自动化排期', 'start');
|
||
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 修)——
|
||
⚠️ ① 的标签**已提到上面**(第四改后它不再属于任何分支:恒画那一条竖线的标签)。
|
||
⛔ 空态 ≠ ② 上报这条回程不存在;只是当前没有会话可以连。 */
|
||
lbl(CX,B2-8,'② 写台账 · --report');
|
||
}else{
|
||
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));
|
||
/* ⚠️ `fmtAge()` 的返回值**自带"前"字**(`4 分钟前`)⇒ ⛔ 别再加一个"前"
|
||
(2026-10-01 实测拼成过「4 分钟前前有活动」)。 */
|
||
tx(x+14,R3.y+72, fmtAge(W.age_min)+'有活动', 'n-sub');
|
||
tx(x+14,R3.y+94, '它承接的件见下方「需求台账」', 'n-sub');
|
||
});
|
||
/* ⚠️ ① 派活**不在这里**(第四改后 = 两个分组大框之间那一条,已提到 `UB` 落笔之后);
|
||
各格到 `UB` 顶的那几条射线**也一并拆掉** —— 用户:「虚线大框 **连接 协作会话 虚线大框**就好」。 */
|
||
// ② 上报:各条任务会话 → 常驻程序(走下方带)
|
||
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){
|
||
/* 🔴 2026-10-01 **第四改**:这句话原来画在任务会话排**下面**(`R3.y+R3.h+16`)——
|
||
那是"没有分组大框"的年代。现在它在 `UB` 的**底边里侧 6px**,贴着虚线 ⇒ 读起来像"被框切了一半"。
|
||
⇒ 挪到 `UB` 的**右上角内侧**(框顶 与 格子顶 之间那条 22px 的空带里),
|
||
并且**右端跟着框走**(`UBx+UBw-10`)⇒ ⛔ 不会因为框随会话数变宽变窄而跑到框外。
|
||
⛔ 不许因为"不好放"就把它删掉 —— 它是**读到了却不说**的对面:少画了就得说出口。 */
|
||
tx(UBx+UBw-10, R3.y-7, '另有 '+moreW+' 条任务会话未画(图上最多 '+MAXW+' 条)', 'n-warn', 'end');
|
||
}
|
||
|
||
// ── ④ 常驻程序(🔴 2026-10-03:原「协作」+「常驻」两格合并成一格 —— 一个进程、一个角色)
|
||
var pr=rt.prog||{}, gu=rt.deliver||{}, q=d.queue||{}, qb=q.by||{}; /* gu = 常驻那组读数(键名沿用 deliver,⛔ 不改:board.py 还按这个键喂) */
|
||
// 常驻程序:🔴 带**队列计数**(用户 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` 里 `goal_fp` 命中当前目标的那几条)四态:待执行 '+((qb.pending)||0)+' / 执行中 '+((qb.running)||0)
|
||
+' / 已完成 '+((qb.done)||0)+' / 有阻碍 '+((qb.blocked)||0)+'。机制里没有「待验收」这个态。'
|
||
+((q.legacy||0)?('<br>⚠️ 另有 '+(q.legacy)+' 件**无目标归属**的历史条目(早期写的、未打标记)⇒ 不计入本行。'):'')
|
||
+((q.fp)?'':'<br>⚠️ **未声明目标** ⇒ 台账全部按「无归属」处理,本行显示 0(⛔ 不等于"没有活")。')
|
||
+'<br>职责:接收=--report 写台账;处理=建检查会话排期;一直运行。'
|
||
+'<br>'+esc(pr.how||'')+'</title></rect>');
|
||
/* ⚠️ `R4.h=112` ⇒ **基线最大约 96**(留 16px 下边距)⇒ ⛔ 别再往这格加第 5 行;
|
||
加第 5 行必须先 `R4.h` 加高(并同步 `R5.y`/走线带,`B3` 是自动算的)。
|
||
静态职责文案已搬进上方 `<title>` tooltip(同「说明只进 ?」口径,不占版面)。 */
|
||
/* 🔴🔴 2026-10-06 **改名「常驻程序」→「检查程序」**(用户逐字:
|
||
「**看板中 常驻程序 改为 目标检查**」→ 我复述成"目标检查",用户再纠正:
|
||
「**没有常驻程序的说法 ,改为 检查程序**」)。
|
||
🔴 **这是一次"角色名"纠错,不是换词** —— 同族历史(2026-10-03):
|
||
我第一版把「上报」改成「常驻」,用户驳回「**怎么又把上报 改成常驻了 常驻什么**」,
|
||
根因=**把「角色名」与「运行形态」混成一个词**。
|
||
现在这一版同样踩了:**「常驻」是运行形态(`--supervise` 一直跑),⛔ 不是角色**。
|
||
✅ 角色实际干的两件事=**① 接收(`--report` 写台账)② 建检查会话排期** ⇒
|
||
名字取其中最可辨识的职责:**检查程序**。
|
||
⚠️ 改的只是**这一格的标题**(用户可见面);代码标识符/文件名/配置键**一律不动**
|
||
(`collabd.py`/`prog`/`rt.prog` 等 ⛔ 不改 —— 改名会波及 20+ 文件且无收益)。
|
||
⚠️ 同屏其它用户可见处一并跟(见本文件 `检查程序` 全文替换点):
|
||
`检查会话:暂无在册(…按需建)`/图例/tooltip/底部说明。 */
|
||
tx(PX+18,R4.y+26,'检查程序','n-title');
|
||
/* 🔴🔴 2026-10-06 **队列行改为「当前目标」口径**(用户逐字:
|
||
「看板中 常驻程序 改为 目标检查,里面的队列 **应该是随着目标的不是目标累积的**」)。
|
||
⚠️ 改前 = `q.total`(`tasks.json` **全量累计**)⇒ 换过目标后**把历史目标的活也数进来**。
|
||
✅ 改后 = 服务端 `_queue(..., cur_fp)` **已按目标过滤**(`q.total` 现在就是当前目标那部分);
|
||
另有 `q.legacy` =**无归属的存量条目数**(早期写的、没打 `goal_fp`)——
|
||
⚠️ 有就**如实点出来**(⛔ 不静默吞掉:否则读者会以为"台账总共就这几件")。
|
||
⚠️ 未声明目标(`q.fp` 空)⇒ 服务端把全部算 legacy ⇒ 这里显示 `0 件` + 点明"未声明目标",
|
||
⛔ 不假装"队列为空"(那会把"没有目标"说成"没有活")。 */
|
||
var _qleg=(q.legacy||0);
|
||
tx(PX+18,R4.y+50, '队列 '+((q.total)||0)+' 件 · 已完成 '+((qb.done)||0)+' · 未完 '+((q.open)||0)
|
||
+(((qb.blocked)||0)?(' · 受阻 '+qb.blocked):'')
|
||
+(_qleg?(' (另有历史 '+_qleg+' 件,不计入)'):''), ((q.open)||0)?'n-warn':'n-idle');
|
||
/* 🔴 2026-10-03 订正(用户 09-30「协作程序 应该一直运行,改成 在线」):
|
||
状态词取 `pr.label`(服务端给)—— **正常态就两个字「在线」**,
|
||
⛔ 不堆"钩子 --tick · N 分钟前"那种细节长串(细节进 tooltip 的 `pr.how`)。
|
||
⛔ 但不放假绿:真没轮动就如实说「在线 · 已 N 分钟没轮」⇒ **不写「已停」**
|
||
—— 常驻真会被打断(换会话/宿主回收),停着是**真读数**,⛔ 不是"该常驻却被停"的故障。 */
|
||
/* 🔴🔴 2026-10-03 10:1x **新增「检查程序在不在跑」这一行**(用户报障逐字:
|
||
「协作程序这个时候应该显示 **有检查程序在运行**」)。
|
||
⚠️ 改前那格只有静态职责文案 + `pr.label`(在线/停)⇒ **完全看不到它刚建的检查会话跑到哪一步**,
|
||
图上看着像"什么都没发生"(实测:检查会话 `44b547d4` 早在 09:57 就 `working`,图上毫无反应)。
|
||
🔴 判据=**服务端给**的 `rt.check.n`(=`sessions.status='working'` 且标题带 `[结果检查]`/`[目标检查]`)
|
||
⇒ **与会话明细/看板亮灭同一个口径**,⛔ 前端不自己数(那会造出第二套判据)。
|
||
⚠️ `unknown`(读库失败)⇒ 如实说「检查状态读不到」,⛔ **不假装"没有检查在跑"**
|
||
(那会把机制故障说成正常 —— 同"降级不静默"那条红线)。 */
|
||
var ck=rt.check||{};
|
||
tx(PX+18,R4.y+92, ck.label||'检查状态未知', (ck.n||ck.unknown)?'n-ok':'n-idle');
|
||
/* 🔴🔴 2026-10-03 11:1x **检查会话画在这里**(常驻程序框内),⛔ **不进任务会话那一排**。
|
||
用户纠正逐字:「检查会话不是协作会话,不应该出现在看板协作会话区域中。
|
||
检查会话属于协作程序的会话(本来也是协作程序创建),可以放在协作程序框图中展示」。
|
||
⚠️ 判据=**服务端单列的 `sessions_checks`**(⛔ 不在前端按标题猜,
|
||
否则又会出现「前端猜一套、后端判另一套」的漂移)。
|
||
⚠️ `R4.h=112` 空间已满(基线 26/50/70/92)⇒ 这几行**画在框的下沿外侧**
|
||
(`R4.y+R4.h+16` 那一带,落在 R4↔R5 的走线带里)⇒ ⛔ 别再往框内塞第五行。
|
||
📌 几何判据:这几行的 y 必须 `< R4.y+R4.h+40`(实测 R5.y=R4.y+146 ⇒ 还有 ~30px 余量)。 */
|
||
var CHK=(d.sessions_checks||[]).filter(function(x){return x.status!=='archived'});
|
||
var _cy=R4.y+112; // 🔴 画在**框内**(R4 已加高到 146);⛔ 不用 R4.y+R4.h+18(那会落进走线带、压 R5)
|
||
if(CHK.length){
|
||
/* 2026-10-03 19:55x **只显示最新一条**,其余**移上去才看得到**(用户要求逐字:
|
||
「协作程序 只显示最新的一条会话情况就行,别的可以做到下拉列表中或者鼠标移动上去的时候 展示」)。
|
||
做法=SVG 原生 **title 子元素**(tx() 的新参数 tip)—— 不用 JS 事件、不用点、⛔ 不会点开又忘收。
|
||
底色:有在跑 => node-on(绿)/只有历史 => node-base(中性;"跑完了"是正常状态,不该染成危险色)。 */
|
||
var _anyRun=CHK.some(function(x){return x.status==='working'});
|
||
var _sorted=CHK.slice().sort(function(a,b){return (a.age_min||0)-(b.age_min||0);});
|
||
var _top=_sorted[0];
|
||
var _short=function(x){
|
||
var _raw=String(x.title||'');
|
||
var _pp=_raw.match(/\[[^\]]*\]/g);
|
||
var _tag=_pp&&_pp.length?('['+_pp[_pp.length-1].replace(/[\[\]]/g,'')+'] '):'';
|
||
var _body=_raw.replace(/^(\[[^\]]*\][\s\-]*)+/,'');
|
||
var _ct=String(x.cwd_tail||'');
|
||
if(_ct&&_body.indexOf(_ct)>=0) _body=_body.split(_ct).join('');
|
||
_body=_body.replace(/-{2,}/g,'-').replace(/^[\s\-]+|[\s\-]+$/g,'');
|
||
var t=_tag+_body;
|
||
// 20:06x 上限 24→16:条目前面加了「最新会话:」和时间,⛔ 再长会撞右边的列表图标
|
||
return t.length>16?t.slice(0,16)+'…':t;
|
||
};
|
||
o.push('<rect class="'+(_anyRun?'node-on':'node-base')+'" x="'+PX+'" y="'+_cy+'" width="'+PW+'" height="22" rx="6"'
|
||
+' fill="none" stroke="var(--color-border-subtle)" stroke-dasharray="3 3"></rect>');
|
||
var _tip='检查会话(检查程序建的)共 '+CHK.length+' 条\n\n'
|
||
+'最新:'+_top.title+'\n状态:'+(wordOf(_top.status))+' 最后活动:'+Math.round(_top.age_min||0)+' 分钟前';
|
||
if(_sorted.length>1){
|
||
_tip+='\n\n其余 '+(_sorted.length-1)+' 条:\n'+_sorted.slice(1).map(function(x){
|
||
return '· '+_short(x)+'('+wordOf(x.status)+','+Math.round(x.age_min||0)+' 分钟前)';
|
||
}).join('\n');
|
||
}
|
||
// 2026-10-03 20:06x 按用户要求排版:「**最新会话:xxx 时间**」+ 右边一个**列表图标**,
|
||
// **图标与文字都能悬停看全部历史**(用户逐字:「最新会话:XXXXXXXXXX 时间 列表ICON 鼠标移动上去看历史的就可以了」)
|
||
var _when=Math.round(_top.age_min||0);
|
||
var _mins=_when<60?(_when+' 分钟前'):(Math.round(_when/60)+' 小时前');
|
||
tx(PX+10,_cy+15, '检查会话', 'n-sub', null, _tip);
|
||
tx(PX+64,_cy+15, (_top.status==='working'?'● ':'')+'最新会话:'+_short(_top)+' '+_mins,
|
||
_top.status==='working'?'n-ok':'n-idle', null, _tip);
|
||
// 🔴 列表图标(≡):**SVG 原生悬停提示** ⇒ 鼠标移上去即出全部历史清单,⛔ 不用点
|
||
var _ix=PX+PW-36, _iy=_cy+5;
|
||
o.push('<g><title>'+esc(_tip)+'</title>'
|
||
+'<rect x="'+_ix+'" y="'+_iy+'" width="24" height="12" rx="3" fill="none"'
|
||
+' stroke="var(--color-text-tertiary)" stroke-width="1"></rect>'
|
||
+'<line x1="'+(PX+PW-30)+'" y1="'+(_iy+4)+'" x2="'+(PX+PW-18)+'" y2="'+(_iy+4)+'"'
|
||
+' stroke="var(--color-text-tertiary)" stroke-width="1"></line>'
|
||
+'<line x1="'+(PX+PW-30)+'" y1="'+(_iy+8)+'" x2="'+(PX+PW-18)+'" y2="'+(_iy+8)+'"'
|
||
+' stroke="var(--color-text-tertiary)" stroke-width="1"></line>'
|
||
+'</g>');
|
||
// ⚠️ 2026-10-03 20:06x 旧的那行条目(`PX+150`)**已删** —— 上面 1042 行才是它的替代
|
||
// (带「最新会话:」前缀 + 时间);⛔ 两条都留着会**画两遍**(20:05x 改版时漏删,本轮实查才发现)。
|
||
} else {
|
||
tx(PX+10,_cy+15, '检查会话:暂无在册(检查程序按需建)', 'n-idle');
|
||
}
|
||
tx(PX+18,R4.y+70, pr.label||'状态未知', pr.up?'n-ok':'n-bad');
|
||
/* 🔴🔴 2026-10-03 09:5x **删掉那行「队列投递已退役(曾叫『上报』)」小字** —— 用户原话逐字:
|
||
「队列投递已于 2026-10-03 退役(曾叫# 看板上不要写这些没用过的东西」。
|
||
⚠️ 上一版这里写「退役这件事**必须**留在图上(⛔ 不当没发生过)」——**与用户口径直接打架,已作废**。
|
||
⛔ 判据:**没用的东西不占版面**(与「说明只进 ?」同族)—— 讲历史的话不进图面。
|
||
⚠️ 下面关于 `gu`/`rt.deliver` 的说明**仍保留**(它讲「这组数据前端不消费」=现状,不是历史)。 */
|
||
/* ⚠️ `gu`(`rt.deliver`)**此格不再渲染** —— 它读的是 `_tick.stamp`(投递轮痕迹),
|
||
而投递已退役 ⇒ 那组读数只对"钩子还在跑 --tick"有意义,⛔ 不该占版面。
|
||
⛔ 变量留着(board.py 还供这组数据,将来重建收件人要用)⇒ **刻意不删引用以外的东西**。 */
|
||
|
||
// ③ 上报 → **跟进会话**(右侧竖井,⛔ 不横穿会话行)
|
||
// 🔴🔴 2026-10-01 用户口径:「换新会话 唤醒的是 跟进会话,**主会话只能是用户触发**」
|
||
// ⇒ 这条回程**改落右半那格**(原来落主会话)—— 与 `collabd.py::_deliver_str()` 的投递目标
|
||
// **必须同款**:图上画到哪、程序就得投到哪(⛔ 图说一套、程序做一套 = 看板说谎)。
|
||
var y4=R4.y+R4.h/2;
|
||
/* 🔴 2026-10-02(用户:「**上报 到 跟进的连线 改为和 唤醒 到 协作的连线 一样可以自适应**」):
|
||
通道 x **现算**(⛔ 不再写死 `CH=1254`)—— 与 ④ 那条**同款手法**(④ 取 `Math.min(...)`=最左,
|
||
本条取 `Math.max(...)`=最右):
|
||
· `UAx+UAw+28` :避开上面的分组大框 `UA`;
|
||
· `UBx+UBw+28` :避开**任务会话分组大框 `UB`** —— 🔴 **UB 宽度随会话数变**(`:929-932`):
|
||
`w` 触到底 `150` 时(约 8 条任务会话)它右边界会越过 `SX1` ⇒ **写死 1254 必被横穿**。
|
||
⛔ **不要退到 `SX1`**(🔴 2026-10-02 用户原话:「**上报 到 跟进的连线 不要那么开**」)——
|
||
会话格**全部落在 `UB` 框内**(`sx`/`total` 都在 UB 的包围盒里)⇒ **避开 `UB` 就等于避开了整排**;
|
||
退到 `SX1` 只会让水平段凭空拉长(1254 → 1038,白绕 216px)。
|
||
⚠️ 现状(2 条协作会话)下三者取最大仍是 `SX1+34` = **1254** ⇒ **线本身与改前逐字相同**(无回归);
|
||
它买的是**防御性**:会话变多时不再穿框(几何自检 ⑧ 段守着)。
|
||
⚠️ 配套:标签 x 也**跟线现算**(=水平段中点),⛔ 不再写死 `1080`(旧值偏左、线一动就飘)。
|
||
⚠️ 待办(另记):`viewBox` 仍是写死的 `1280` —— 会话极多时 `RKX` 会超出画布右缘被裁,
|
||
届时**须一并放宽 viewBox**(本轮不改,避免扩大范围)。 */
|
||
/* 🔴🔴 2026-10-02(用户原话:「**上报 到 跟进的线 折的方向不对**,参考 **唤醒 到 协作连线** **镜像**往右侧连」):
|
||
真因=旧式 `Math.max(UAx+UAw+28, UBx+UBw+28)` **缺了"源框(上报框)右侧"这一项** ——
|
||
它只保证"通道在**别的框**右边",却**不管源框自己**。实测(`chrome --headless=new --dump-dom`
|
||
读渲染后真实坐标,当日 1 条任务会话):`UA` x=319 w=637 ⇒ 右界 956(+28=984);
|
||
`UB` x=452 w=376 ⇒ 右界 828(+28=856)⇒ 旧 `RKX=max(984,856)=984`,
|
||
而**上报框自身右边界=`UX+PW`=1000** ⇒ 通道落在源框**之内** ⇒ 第一段折线
|
||
**984−1000=−16px ⇒ 向左** =用户看到的"方向反了"。
|
||
✅ 镜像 ④:④ 用 `Math.min(PX-80, …)`(目标框**左**边再让 80)⇒ 本条镜像成
|
||
`(UX+PW)+80`(**源框右**边再让 80)。补上 ⇒ `RKX=max(1000+80,984,856)`=**1080**
|
||
⇒ 第一段 **+80px 向右**、末段 **144px 向左**;④ 则是 144 左 / 80 右 —— **逐段对称**。
|
||
⚠️ 通道取 `max` ⇒ 会话变多、`UB` 右界超过 `1080` 时通道自动右移让位(不会穿框)。 */
|
||
/* 🔴🔴 2026-10-03 **本段整体退役,变量一并删除**(用户 07:20「不是 协作程序常驻吗」):
|
||
原来这里是「③ 上报 → 跟进会话」那条右侧竖井的**通道计算**,两端(`UX` 右格、`RW` 唤醒行)
|
||
在 10-03 07:0x 那轮**都真删了** ⇒ `RKX` 这个变量**已无任何消费者**(保留就是死变量,
|
||
且它的表达式还引用已删的 `UX` ⇒ **一改就会 JS 报错**)。
|
||
✅ **处置=连注释带变量一起删**(不是留个注释占位)。
|
||
⚠️ 原文已随历史备份一并清理,⛔ 要恢复得**按本段描述重写**(`UX`=空态文案节点;`RKX`=右侧小字位)。
|
||
⚠️ 曾经踩过的坑(留着提醒):①「上报 → 跟进」折线方向反了 —— 真因是
|
||
`Math.max(UAx+UAw+28, …)` **缺"源框自己右边"这一项** ⇒ 通道落在源框**之内** ⇒
|
||
第一段算出 −16px(向左)=用户看到的"方向反了";②「唤醒 → 协作」那条线**从已删格出发**
|
||
⇒ 线头悬在空处。⇒ **判据:线段的每一端都必须真实存在**(P0-25 同族:画出来就是骗)。 */
|
||
|
||
// ── ⑤ 🔴 **Hook进程** —— 用户 2026-09-30 指出「图里是不是还缺少关键节点,比如宿主起的子进程」。
|
||
// 它**才是上报的真正执行者**:WorkBuddy 在钩子时机起一个短命子进程,跑完即退 ⇒ **事件驱动**。
|
||
// ⚠️ 时间驱动的那些(如「唤醒」)**不在这一行**,画在 `RW` 行左格(见上)—— 两者不是一回事:
|
||
// 整机没活动时 Hook 不会被唤起,「唤醒」补的正是这个缺口。
|
||
var hasTG=TG.length>0;
|
||
/* 🔴 2026-10-01(用户:「**Hook进程 框图宽度往右边延长一些,让 tick 那条线能竖直摆放**」)
|
||
⇒ 框宽 480 → **490**(右边界 880 → 890)。
|
||
⚠️ 加宽**不是为了好看**,是为了让 `--tick` 的出口点能落在**框顶边之内**(见下方 TICKX)——
|
||
⛔ 改宽度时别把它缩回去,否则出口点会跑到框外、线头悬空。
|
||
🔴🔴 2026-10-03 **左边界被我误改过(已还原为 400)**:
|
||
我曾为给 ④ 那条竖直线腾位置把它 400 → 430,用户指正「**还把 hook 图框挤到旁边
|
||
多难看**」⇒ ④ 改成**折线绕行**(见下)后,**本框位置恢复原样**。
|
||
📌 **教训(比那条线本身重要)**:**框的位置是它自己的版面决策**,
|
||
⛔ 不许为了迁就某条连线去动别人的框 —— 要动的是那条线(P0-25 同族:
|
||
**线段的每一端都必须真实存在**,但反过来也成立:**不许为凑一条线而挪框**)。 */
|
||
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');
|
||
/* 🔴🔴 2026-10-03 **改掉一句已经说假的说明**(用户「协作程序相关的 连线 有用的保留
|
||
没用的可以去掉」⇒ 我逐条去核实这三条线还活着吗,撞出下面这件更大的事):
|
||
原写「两个模式:--once(**读队列**)/ --tick(补检查排期)」——
|
||
🔴 「读队列」是**投递时代**的职责,`collabd.py` 头部已注明 `--once` **⛔ 不投递**
|
||
(投递 10-03 整体退役)⇒ 这行字在讲一个**不存在的活**(同族红线:写出来就是骗)。
|
||
✅ 改成现在的真实分工(源码 `--once` / `--tick` 两段注释逐字对齐):
|
||
`--once`=读库判定+信号+看板;`--tick`=顺手续命常驻+结束即建检查会话。
|
||
⚠️ 改这里必须**先读 `collabd.py` 的头部注释**,⛔ 别照着旧文案改(那正是它说假话的来源)。 */
|
||
// 🔴🔴 2026-10-05 `--tick` **已删**(用户:「检查程序 常驻 自己不会判断吗,非要什么 tick once
|
||
// 去触发?」)⇒ 建排期/park 探针**全搬进常驻主循环** ⇒ 钩子**只剩一条**出口=`--ensure`
|
||
// (顺手续命)。⛔ 图上⛔ 不许再画 `--tick`:机制没了还画着就是**骗**(画出来=说谎)。
|
||
tx(hkX+20,R5.y+84, fitText('两个入口:--once(读判定出信号)/ --ensure(续命常驻;判定由常驻自己做)', 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');
|
||
/* 钩子 → 常驻程序(--ensure):🔴 **一条纯竖直的线**(用户 2026-10-01:
|
||
「Hook进程 框图宽度往右边延长一些,**让 tick 那条线能竖直摆放**」)。
|
||
🔴🔴 2026-10-05 **标签 `--tick` → `--ensure`**:`--tick` 机制已删,这条线现在代表的
|
||
是**续命**(`maybe_ensure_supervise()` ⇒ `collabd.py --ensure`)。
|
||
⛔ 坐标**一律不动**(只是换标签)⇒ 下面这段几何推算**仍然成立**,⛔ 别重算。
|
||
🔴🔴 2026-10-03 **两处改算**(沿用至今):
|
||
① 原式 `TICKX=UX+PW/2`(上报框中心 850),而 `UX` 那个右格随「两格合一」**已删**
|
||
⇒ 现在取**常驻程序框中心 + 120 偏右**(`PX+PW/2+120`=760)。
|
||
② ⛔ 偏右是为了和 `--once`(`hkX+40`=470)**不重叠**(实测间隔 290px)。
|
||
⚠️ **出口 x 必须落在 Hook 框顶边之内** —— 🔴 2026-10-03 `hkX` 已 400→430(见 ⑤ 段),
|
||
故范围是 `hkX..hkX+hkW`=**430..920**,760 在其中 ✔。
|
||
⚠️ 改 `hkX/hkW/PX/PW` 时**必须一起验这条**:出口 x 若跑出该范围,线头就悬在框外
|
||
(同族红线:**线段的每一端都必须真实存在**,P0-25)。 */
|
||
var TICKX=PX+PW/2+120;
|
||
edge('M'+TICKX+' '+R5.y+' L'+TICKX+' '+(R4.y+R4.h), true);
|
||
lbl(TICKX+12, B3+4, '--ensure', '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');
|
||
// ✅ 折线几何(全部现算,⛔ 不写死):`RXV=PX-40`(框外左侧竖直通道)↑ 到 `RYY`
|
||
// (= 常驻程序框**垂直中点**)→ 右折到 `RXH=PX` ⇒ **终点落在框的左边**,箭头朝右。
|
||
// ⚠️ 两条要求同时成立的原因:`RXV=PX-40=365 < hkX=400`(⛔ 竖直段在 Hook 框左边)
|
||
// 且 `RXH=PX=405` 恰在框左边上(✅ 连对目标)。
|
||
// 🔴🔴 2026-10-03 **三改=终点改连「左边」**(用户原话:「**线连到左边不行 非的连到下边
|
||
// 是咋想的**」⇒ **我上一版绕了左边却仍从下边钻进去,那是白绕**):
|
||
// 🔴 上一版路径 `(365,850)→(365,668)→(495,668)→(495,684)` —— 最后一段**向下**钻框底边
|
||
// ⇒ 视觉上「先往左绕、再退回下边进」,**那 130px 横移毫无意义**
|
||
// (同族红线:**线的每一段都得有理由**,无意义的段=自己骗自己)。
|
||
// ✅ 既然通道本来就在左边,**终点就该直接落在左边框上**(`RXH=PX`);
|
||
// `RYY` 取框**垂直中点**(⛔ 别写死 y,改 `R4` 高度自动跟随)⇒ 折线只剩**两段**。
|
||
var RXV=PX-40, RXH=PX, RYY=R4.y+(R4.h/2);
|
||
edge('M'+RXV+' '+RB.y+' L'+RXV+' '+RYY+' L'+RXH+' '+RYY, true);
|
||
// ⚠️ 标签贴竖直段**左边**(anchor end)—— 放右边会和「在钩子时机起它」挤成一行连读。
|
||
lbl(RXV-14, 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=[
|
||
/* 🔴 2026-10-01 **修一处会说出假话的判据**(与刚删掉的那行同源,一并治)。
|
||
原来只看 `TOPS.length` —— 而 `TOPS` 走的是 `board.py::_goal_topics()`,
|
||
**它会在"没声明过"时回落到 `[goal.short]`** ⇒ `TOPS.length` 恒 ≥1
|
||
⇒ 两个后果:❶ 从没在对话里说明过时,这里把**回落值当成"已声明的类别"**报出去
|
||
(同族红线:**没有却说有**);❷ 那句「⚠ 未在对话里说明过」实际上**永远轮不到**=死分支。
|
||
⇒ 判据改成 **`topics_source.kind`**(`declared` / `fallback` / `none`),与顶部
|
||
「本项目」卡的 chips 用**同一个字段**(⛔ 否则版面上两处口径又会打架)。 */
|
||
'主会话<b>负责的任务类别</b>:'+((TSRC.kind==='declared' && TOPS.length)
|
||
? TOPS.map(function(t){var s=MBT[t]||''
|
||
return '<code>'+esc(t)+'</code>'+(s?('('+esc(s)+')'):'(⚠ 无主会话)')}).join('、')
|
||
: '⚠ <b>还没在对话里说明过</b>(现按目标简称走单类别 ⇒ ⛔ 别当成已声明)'),
|
||
'任务会话:在跑 <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` 简称)';
|
||
})() : '',
|
||
/* 🔴 2026-10-01 加:**已退场/超窗口**的会话(用户:「接续会话的时候,如何处理过期的会话,
|
||
避免越堆越多」)。
|
||
⚠️ **这一条是"退场机制的可见性"**,⛔ 不能省 ——
|
||
收起而不说=读者把"收起了"读成"本来就没有"(同族红线:读到了却不说)。
|
||
⚠️ 措辞要点:① 说**不是删除**(否则读者会去库里找,找不到又成一条假情报)
|
||
② 说**窗口多少分钟**(数值随配置变,⛔ 不许写死在文案里)
|
||
③ 给**主动退场的那条命令**(接续棒收尾该做的事,写在版面上才有人做)。 */
|
||
((Number(RET.retired||0)+Number(RET.stale||0))>0) ? (function(){
|
||
var _r=RET.recent||[];
|
||
return '🧹 已收起 <b>'+(Number(RET.retired||0)+Number(RET.stale||0))+'</b> 条过期会话'
|
||
+'(显式退场 <b>'+Number(RET.retired||0)+'</b> + 超 '+String(RET.live_min||'?')
|
||
+' 分钟未动 <b>'+Number(RET.stale||0)+'</b>):'
|
||
+(_r.length?_r.map(function(u){return '<code>'+esc(u.id8)+'</code> '+esc(u.title)}).join(' · '):'')
|
||
+' —— ⛔ **不是删除**(宿主库里原样在,明细见 <code>retired.json</code>);'
|
||
+'接续棒收尾应执行 <code>collabd.py --retire self</code> 主动退场。';
|
||
})() : '',
|
||
// ⚠️ 「另有 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 改名。
|
||
🔴 2026-10-01 晚订正:常驻定案**回归** ⇒ 投递**应当一直运行**(`--supervise`),
|
||
⇒「停着」是**有效读数**(⛔ 不再是 09-30 那句"没有启动/停止这回事");钩子只作补充。 */
|
||
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||{};
|
||
// 🔴🔴 2026-10-03 09:5x 用户口径「看板上不要写这些没用过的东西」⇒ **「已退役/曾叫」这类话一律不上图面**。
|
||
// ⛔ 卡片**保留**(删卡会动 cols-3 栅格、版面留空),正文只留**当前规则**,⛔ 不讲历史。
|
||
var _retired = (n.phase==='retired' || (d.meta&&d.meta.deliver_retired));
|
||
document.getElementById('notify').innerHTML= _retired
|
||
? emptyBox('队列通知(当前无自动通知)',
|
||
'队列变化<b>不会自动通知任何人</b>。'
|
||
+'要落这件事,得<b>显式建一条任务会话</b>去读队列干活。')
|
||
: '<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>';
|
||
|
||
// 🗑️ 2026-10-03 **删掉「最近上报 wakeups.jsonl」整块渲染**(卡片已在 HTML 里删除):
|
||
// `wakeups.jsonl` 已不再被写(实测文件不存在)⇒ 恒空 ⇒ 假面板。
|
||
// ⛔ `d.wakeups` 由 board.py 继续供:它是"判定是否退役"的另一条证据链,删它要动 board.py 两处,
|
||
// 且别的线(手机接入退役前)也可能读它 ⇒ **前端不消费、后端保留**。
|
||
|
||
/* 🔴 剩下的四块(本项目 / 架构图 / 需求台账 / 会话明细)**随 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 = 目标名 + 状态徽章 + 完成情况摘要(都是实读,⛔ 不写死)。 */
|
||
/* 🔴🔴 2026-10-04 用户定案:「**最多显示 6 个目标,超出的用下拉菜单显示**」
|
||
⇒ 前6 格照常;第 7 个起收进一个下拉(点开列出全部超出项,可直接切换)。
|
||
🔴 2026-10-06 用户改口径:「**tab 的排列顺序应该按创建时间顺序**」(推翻 10-04 的活跃度排序)。
|
||
⚠️ 排序由**后端**做(`board.py` 按 `tab_created`=`goal.json` 的 `declared_at` 升序
|
||
⇒ **最早创建的目标在最前**,顺序**不随活跃度漂**),
|
||
本函数⛔ 不再自己排序(⛔ 两处排序 = 改一处忘另一处必打架,同 P0-40)。
|
||
⚠️ 超出数用**后端给的 `tab_more`**(⛔ 不在前端重算 `gs.length-6`:
|
||
那是把"业务规则"复制一份;后端已算好,且它知道排序后的真实分界)。 */
|
||
var TAB_MAX = 6; /* 🔴 上限常量:⛔ 别在下面再写一个 6(两处数字必漂) */
|
||
function renderTabs(gs){
|
||
var box=document.getElementById('tabs');
|
||
if(!box) return;
|
||
if(!gs.length){ box.hidden=true; return; }
|
||
box.hidden=false;
|
||
var more=Math.max(0, gs.length-TAB_MAX);
|
||
var shown=more?gs.slice(0,TAB_MAX):gs;
|
||
box.innerHTML=shown.map(function(g,i){
|
||
var acc=(g.goal||{}).acceptance||{}, ks=Object.keys(acc);
|
||
/* 🔴 2026-10-02 同批修(**同族第 2 处,差点漏掉**):tab 摘要这里原本**另写了一份**英文判据
|
||
`String(acc[k]).toLowerCase()==='pass'`(变量名是 `ks` 不是 `keys`,所以按 `keys` 那条锚点搜**搜不到**)
|
||
⇒ 恒算 `0/N 通过` + 徽章恒 `is-bad`。⇒ **一个文件里同一条规则有两份实现** 就是病灶:
|
||
统一改走上面那个 `accIsPass()`,⛔ 不要再就地内联判据。 */
|
||
var pass=ks.filter(function(k){return accIsPass(acc[k])}).length;
|
||
var cls=!ks.length?'is-warn':(pass===ks.length?'is-ok':'is-bad');
|
||
var txt=!ks.length?'未声明判据':(pass+'/'+ks.length+' 通过');
|
||
/* 🔴🔴 2026-10-03 19:1x **标签与内容对齐**(用户报障逐字:「**为什么目标已完成 下面的目标状态还是不对**」)。
|
||
真因=**名不符**:第二行的标签写「目标状态」,内容却是**判据通过数**(`4/4 通过`)
|
||
⇒ 目标早就标完成了,读者在第二行看到的却还是判据数 ⇒ "不对"。
|
||
✅ 改法(两个数据源各归各位,⛔ 不互相冒充):
|
||
· **判据通过数 → 挪到第一行的徽章**(它是"判据",⛔ 不是"状态");
|
||
· 第二行「目标状态」**真显示生命周期**(`已完成`/`进行中`/`已停`),由快照的 `life` 提供。 */
|
||
var lifeS=String(g.life||''),
|
||
lifeTxt=lifeS||(g.paused?'已停':'进行中'),
|
||
lifeCls=lifeS.indexOf('已完成')>=0?'is-ok':'is-warn';
|
||
/* 🔴 2026-10-01(用户:「再确认下**目标名称**显示在哪里的,没看到呢」)
|
||
⇒ 根因是我上一版**第一行只写了目标简称**(`short`),全名只藏在 `title=` 提示里
|
||
—— 简称认不出是"哪个目标"。**改:第一行就是完整目标名**(超出省略 + 悬停看全),
|
||
简称退到第二行当小字前缀。⛔ 名字一律来自快照,技能侧不写死任何目标名。
|
||
⚠️ **2026-10-02 订正**:上面那句「简称退到第二行当小字前缀」**已作废**(用户要第二行改成状态标签,见下)。
|
||
第一行的结论不变(完整目标名 = 第一行),⛔ 不要因为第二行改了又把名字挪回去。 */
|
||
/* 🔴 2026-10-02(用户逐字:「看板的TAB选项里,有一项 本机协作 也改为 目标状态」)
|
||
⇒ tab 第二行⛔ **不再显示目标简称**,改成**固定标签「目标状态」** + 状态徽章。
|
||
真因:简称(`goal.short`)是"这个项目自己起的名字",摊在第二行会被当成**名字**读,
|
||
而这一行的本职是**说状态**("6/7 通过")—— 名字已经在第一行写全了,第二行不必再重复一遍。
|
||
⚠️ 简称**照旧来自快照**(`goal.short` 一个字没删、仍是目标 key 的回落),
|
||
只是**这一格不再展示**;该简称的正式落点=顶部「目标」行那颗 pill(`renderProject()` 内,未动)。
|
||
⛔ 原文留痕(已作废,不再生效):
|
||
var short=String((g.goal&&g.goal.short)||'');
|
||
+'<span class="tsub">'+(short&&short!==full?esc(short)+' · ':'')+chip(txt,cls)+'</span>' */
|
||
var full=String((g.goal&&(g.goal.title||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">'
|
||
/* ⚠️ 2026-10-03 20:31x **删掉「当前」徽章**(用户:「什么 当前 …… 没用的东西不要显示」)
|
||
—— 它表的是"这是当前选中的 tab",而 tab 本身已有 `is-on` 高亮 ⇒ **纯冗余**,⛔ 白占版面。 */
|
||
/* 🔴🔴 2026-10-03 15:45x **跨工作区 tab**(用户报障:「tab 不能切换看另外两个工作区的目标」)
|
||
⇒ 来自其它工作区的目标**必须标出是哪个工作区**,否则三个 tab 标题一样、读者分不清。
|
||
⚠️ 标记一律用**快照给的 `peer`**(=工作区目录名),技能侧⛔ 不写死任何工作区名。
|
||
⚠️ 这是**只读**的并列查看:⛔ 不写对方文件、⛔ 不起对方进程、⛔ 不改对方状态。 */
|
||
/* 🔴🔴 2026-10-03 18:5x **「目标已完成」徽章**(用户报障:「主会话说目标都已完成,⛔ 为什么看板上还是没完成」)
|
||
⇒ `collabd.py --set-life 已完成` 写的是 **`lifecycle`** 字段,
|
||
而看板原先**没有任何展示位**读它(tab 第二行那个是**判据通过数**,不是生命周期)⇒ 永远看不到。
|
||
⚠️ 与下面那行判据通过数**并存、互不覆盖** —— 两者是两个数据源
|
||
(一个是主会话标的生命周期,一个是判据算出来的)⇒ ⛔ **不许用一个冒充另一个**:
|
||
真出现「已完成但判据不全」时,**两个都摆出来**才是如实的。 */
|
||
+(lifeTxt?'<span class="badge '+lifeCls+'">'+esc(lifeTxt)+'</span>':'')
|
||
+(g.paused?'<span class="badge is-warn">已停</span>':'')+esc(full)+'</span>'
|
||
+'<span class="tsub">'
|
||
/* ⚠️ 2026-10-03 20:31x 顺序=**状态在前、名称在后**(用户:「你要是喜欢 状态放前面
|
||
那就把工作区名称 放在后面」)⇒ 与第一行「目标状态 + 目标名称」**同款句式**,
|
||
⛔ 不让第二行反着来(两行读法不一致=读者要重新适应一次)。
|
||
⚠️ 名称用**真实名**(后端 `ws_name`)+**大字体**;⛔ 本区不再兜底成"本工作区"(占位词)。 */
|
||
/* ⚠️ 判据徽章与**目标状态徽章同款**(用户:「判据的状态的 字体和边框 和 目标状态一致」)
|
||
⇒ ⛔ 原来用的是 `chip()`(另一套 class ⇒ 字体/边框都不一样,看着像两种东西)
|
||
⇒ 改用**同一个 `.badge`** ⇒ 字体、边框、圆角**天然一致**,⛔ 不再手工对齐两套样式。 */
|
||
+(ks.length?('<span class="badge '+cls+'">'+esc(txt)+'</span>')
|
||
:'<span class="badge is-warn">未声明判据</span>')
|
||
+'<b class="wsname">'+esc(g.ws_name||g.peer||'')+'</b></span>'
|
||
+'</button>';
|
||
}).join('')
|
||
/* 🔴🔴 2026-10-04 超出部分收进下拉(用户定案:「最多显示 6 个目标,超出的用下拉菜单显示」)。
|
||
⚠️ 为什么放最后一块而不是挤进前 6 格:超出的目标**常常正是刚启动的**(最新在跑)⇒
|
||
用户要一键切到它,下拉里能直接点选,不必先记住它在第几格。 */
|
||
+ (more? ('<div class="tabwrap">'
|
||
+'<button type="button" class="tab tabmore-btn" id="tabMoreBtn"'
|
||
+' aria-haspopup="listbox" aria-expanded="false" title="还有 '+more+' 个目标(下拉选择)">'
|
||
+'<span class="tt"><span class="badge is-warn">更多</span>还有 '+more+' 个目标</span>'
|
||
+'<span class="tsub">点此展开<svg width="10" height="6" viewBox="0 0 10 6" aria-hidden="true">'
|
||
+'<path d="M1 1l4 4 4-4" fill="none" stroke="currentColor" stroke-width="1.6"/></svg></span>'
|
||
+'</button>'
|
||
+'<div class="tabmenu" id="tabMoreMenu" role="listbox" hidden>'
|
||
+ gs.slice(TAB_MAX).map(function(g){
|
||
var f=String((g.goal&&(g.goal.title||g.goal.short))||'(未声明目标)');
|
||
return '<button type="button" class="tabmenu-item" data-gi="'+gs.indexOf(g)+'" role="option"'
|
||
+' title="目标:'+esc(f)+'">'
|
||
+'<b class="wsname">'+esc(g.ws_name||g.peer||'')+'</b>'
|
||
+'<span class="tt">'+esc(f)+'</span></button>';
|
||
}).join('')
|
||
+'</div></div>') : '')
|
||
;
|
||
/* 🔴 2026-10-01(用户:「**下面这段话是干啥的,为什么也在 tab 区域**」)——
|
||
**tab 条上⛔ 不放任何说明文字**。上一版在两格之间塞了一句「只登记了 1 个目标 ⇒ 只有一格 /
|
||
要并行看第二个目标:在 goals/ 里放一份 …json」—— 那是**使用说明**,不是**数据**,
|
||
摊在版面上就成了噪声(同族老毛病:把小字说明写到版面上)。
|
||
⇒ **删掉**;"怎么加第二个目标"搬进「本项目」右上角 `?` 里(见 projCard 的 qtip)。
|
||
⛔ 也别退回「共 N 个目标」那种话 —— 有几格**看格子就知道**,写出来信息量为零。 */
|
||
[].forEach.call(box.querySelectorAll('.tab[data-gi]'), function(b){
|
||
b.onclick=function(){
|
||
var i=Number(b.dataset.gi)||0;
|
||
if(i>=gs.length) return;
|
||
var k=goalKey(gs[i]);
|
||
location.hash='g='+encodeURIComponent(k);
|
||
GIDX=i;
|
||
var mn=document.getElementById('tabMoreMenu');
|
||
if(mn) mn.hidden=true; /* 选完自动收起(⛔ 否则菜单悬着挡版面) */
|
||
if(D) paintScope(D); /* ⛔ 不重新拉数据 ⇒ 切 tab 是**零延迟零请求**的 */
|
||
};
|
||
});
|
||
/* 下拉开合(⛔ 用 hidden 属性,不引第三方;点外面/Esc 收起) */
|
||
var mb=document.getElementById('tabMoreBtn'), mn=document.getElementById('tabMoreMenu');
|
||
if(mb&&mn){
|
||
mb.onclick=function(e){
|
||
e.stopPropagation();
|
||
var open=!mn.hidden;
|
||
mn.hidden=open;
|
||
mb.setAttribute('aria-expanded', open?'false':'true');
|
||
};
|
||
mn.onclick=function(e){ e.stopPropagation(); };
|
||
document.addEventListener('click', function(){ mn.hidden=true; mb.setAttribute('aria-expanded','false'); });
|
||
document.addEventListener('keydown', function(e){ if(e.key==='Escape'){ mn.hidden=true; mb.setAttribute('aria-expanded','false'); } });
|
||
}
|
||
}
|
||
|
||
/* 把"当前目标"那一段投影成顶层字段的形状 ⇒ 下面四个渲染器**原样复用**(⛔ 不复制代码)。 */
|
||
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;
|
||
/* 🔴🔴 2026-10-03 20:22x **补两个漏取字段**(用户报障:「为什么不同目标,协作程序框图里面的内容是一样的」)。
|
||
真因=**服务端数据是对的**(实测:本区块内 checks=4 条、两个 peer 块各 0 条),
|
||
但这里**只覆盖了 7 个字段** ⇒ 其余字段**原封不动沿用顶层 `d`**(=**本工作区**那份)
|
||
⇒ 切到别的 tab 时,「协作程序状态/检查状态/检查会话列表」全都显示**本工作区**的 ⇒ 看起来一样。
|
||
⚠️ 这就是「**加字段必须顺着数据流查到消费点**」的第三种翻法:前两处是"加了没渲染",
|
||
这一处是"**块里有、视图没取**"——⛔ 合并视图里的字段清单是**白名单**,漏一个就静默回落。 */
|
||
v.sessions_checks=g.sessions_checks||d.sessions_checks||[];
|
||
v.sessions_unrecognized=g.sessions_unrecognized||d.sessions_unrecognized||[];
|
||
v.sessions_retired=g.sessions_retired||d.sessions_retired||{};
|
||
v.runtime=g.runtime||d.runtime||{};
|
||
// 🔴 20:22x 补最后一项:**「完成情况」那一行**(数据驱动检查查出来的漏网之鱼)
|
||
v.acc_summary=g.acc_summary||d.acc_summary;
|
||
/* 🔴🔴 2026-10-05 12:0x 补 `ws_name` —— 与 10-03 那次**同族**的漏网字段。
|
||
真因(实测,不是猜):`renderProject(d)` 第 660 行读的是 **`d.ws_name`**(=所属工作区真实目录名,
|
||
用户要求「明确写出来,不搞『自己就不标』特例」),而 `d` 在第 1738 行=`scopeView(d,gs[GIDX])` 的产物
|
||
⇒ 该字段原先**不在视图白名单里** ⇒ 切到别的 tab 时**静默沿用本工作区的名字**
|
||
⇒ 「这个目标属于哪个工作区」在跨 tab 时显示错,与 10-03「几个 tab 看起来一样」是**同一个病根**。 */
|
||
v.ws_name=g.ws_name||d.ws_name;
|
||
return v;
|
||
}
|
||
|
||
/* 🔴 2026-10-03 16:1x **peer 格的来源说明**(⛔ 没有它 ⇒ 读者会误读)。
|
||
那一格是**只读概览**:下面每一块只来自**那个工作区自己的文件**,
|
||
⛔ 本看板不读它的进程、也**不拿本工作区的数据补位**(补位=把本区执行情况挂到别人名下)。
|
||
⇒ 所以必须明说"读到了什么、多旧、完整版在哪"。 */
|
||
function renderPeerNote(g){
|
||
var el=document.getElementById('peerNote');
|
||
if(!el){
|
||
el=document.createElement('div'); el.id='peerNote';
|
||
var tb=document.getElementById('tabs');
|
||
if(tb&&tb.parentNode) tb.parentNode.insertBefore(el, tb.nextSibling);
|
||
}
|
||
if(!el) return;
|
||
if(!g||!g.peer){ el.innerHTML=''; el.hidden=true; return; }
|
||
var ps=g.peer_scope||{};
|
||
el.hidden=false;
|
||
el.innerHTML='<div style="margin:0 0 14px;padding:10px 12px;border:1px dashed var(--color-warning);'
|
||
+'font-size:12px;color:var(--color-text-secondary);line-height:1.8">'
|
||
+'这一格是「<b>'+esc(g.peer)+'</b>」工作区的<b>只读概览</b>:下面每块只来自<b>它自己目录下的文件</b>,'
|
||
+'⛔ 不读它的进程、⛔ 不拿本工作区的数据补位。<br>'
|
||
+'台账文件:'+(ps.tasks_present?'<b>有</b>':'<b>暂无</b>(它还没有上报过任务)')
|
||
+' | 常驻状态文件:'+(ps.state_present?'<b>有</b>':'<b>暂无</b>(它的检查程序还没跑起来)')
|
||
+' | 常驻心跳:'+(ps.heartbeat_age_min!=null
|
||
?('<b>'+esc(String(ps.heartbeat_age_min))+' 分钟前</b>')
|
||
:'<b>查不到</b>(它的检查程序没在跑)')
|
||
+'<br>⇒ 要看它的<b>完整执行情况</b>,请打开<b>该工作区自己的看板</b>。</div>';
|
||
}
|
||
|
||
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);
|
||
renderPeerNote(gs[GIDX]);
|
||
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;
|
||
}
|
||
/* 🔴🔴 2026-10-05 **取数与渲染必须分开报**(用户报障:「快照读取失败:acc is not defined(服务还开着吗?)」):
|
||
当时两者共用一句「快照读取失败…服务还开着吗」⇒ 一个**渲染期**的 `ReferenceError`
|
||
被说成**服务挂了**,用户去重跑 `board.py`(⛔ 白费力,服务本来好的),
|
||
真正的 bug 反而被这句话盖住(同族 P0:**假归因 → 排查方向被带偏**)。
|
||
✅ 分成两段:`.then(render)` 里的异常 ⇒ "**页面渲染出错**"(附代码位置,与服务无关);
|
||
`fetch` 的异常 ⇒ 才是"取数失败"(这才该怀疑服务)。 */
|
||
var raw=null;
|
||
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(function(d){
|
||
raw=d;
|
||
try{ render(d); }
|
||
catch(e){
|
||
var el=document.getElementById('err'); el.hidden=false;
|
||
el.textContent='页面渲染出错(⛔ 与服务无关,服务是好的):'+(e&&e.message||e)
|
||
+' —— 请保留此信息报给维护者,⛔ 不用重跑 board.py';
|
||
throw e; /* ⛔ 别吞:控制台要留栈,方便定位 */
|
||
}
|
||
}).catch(function(e){
|
||
var el=document.getElementById('err'); el.hidden=false;
|
||
/* ⚠️ 只有"还没拿到快照"时才算取数失败;已拿到 ⇒ 上面那段已报过,别覆盖成误导信息 */
|
||
if(raw===null){
|
||
el.textContent='快照读取失败:'+(e&&e.message||e)+'(服务还开着吗?重跑 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>
|