1614 lines
128 KiB
HTML
1614 lines
128 KiB
HTML
<!DOCTYPE html>
|
||||
|
|
<html lang="zh-CN">
|
|||
|
|
<head>
|
|||
|
|
<meta charset="utf-8">
|
|||
|
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|||
|
|
<title>多会话协作 · 实时架构看板</title>
|
|||
|
|
<script>/* 🔴 在样式生效前就套用上次选的风格 —— ⛔ 别等 load,否则会先闪一下当前风格 */
|
|||
|
|
(function(){try{var s=localStorage.getItem('dsh-board-style');
|
|||
|
|
if(s&&s!=='base'){document.documentElement.setAttribute('data-style',s)}}catch(e){}})();
|
|||
|
|
</script>
|
|||
|
|
<style>
|
|||
|
|
/* ══════════════════════════════════════════════════════════════════════════════
|
|||
|
|
🔴 风格系统(用户 2026-09-30:「画架构图参考 archify 的样式,右上角加风格切换(保留当前风格)」)
|
|||
|
|
· **切换只换 `<html data-style="…">`** —— 颜色/字体/网格全部走令牌,SVG 里没有硬编码颜色
|
|||
|
|
⇒ 换风格**不需要重画图**。
|
|||
|
|
· 三套:`base`(**保留的当前风格**)/`archify-dark`/`archify-light`
|
|||
|
|
(后两套令牌逐字取自 archify 的 `assets/template.html`,MIT)。
|
|||
|
|
· ⛔ 新写样式一律走令牌,别在组件里写死颜色 —— 否则换风格会花。
|
|||
|
|
══════════════════════════════════════════════════════════════════════════════ */
|
|||
|
|
|
|||
|
|
/* ── 通用(与风格无关的尺寸/字体/网格令牌) ─────────────────────────────── */
|
|||
|
|
:root{
|
|||
|
|
--radius-8:8px; --radius-12:12px; --radius-16:16px; --radius-pill:999px;
|
|||
|
|
--font-ui:system-ui,-apple-system,"Segoe UI","Microsoft YaHei",sans-serif;
|
|||
|
|
--node-stroke-w:1;
|
|||
|
|
--shadow-pop:0 10px 28px rgba(0,0,0,.18);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── 风格 1 · base =**当前风格**(跟随系统深浅色) ─────────────────────── */
|
|||
|
|
:root{ /* base · light */
|
|||
|
|
--color-bg-page:#f6f8f5; --color-bg-surface:#ffffff; --color-bg-subtle:#eef2ee;
|
|||
|
|
--color-text-primary:#12261a; --color-text-secondary:#4a5c50; --color-text-tertiary:#7a8a80;
|
|||
|
|
--color-border-subtle:#e2e9e2; --color-border-default:#cdd8ce; --color-border-strong:#a9b8ab;
|
|||
|
|
--color-accent:#256d37; --color-accent-subtle:#e6f1e8; --color-on-accent:#ffffff;
|
|||
|
|
--color-success:#1f7a3d; --color-success-subtle:#e6f4ea;
|
|||
|
|
--color-warning:#8a5a00; --color-warning-subtle:#fdf3e0;
|
|||
|
|
--color-danger:#b3261e; --color-danger-subtle:#fdecea;
|
|||
|
|
--color-focus-ring:#256d37;
|
|||
|
|
--color-canvas-dot:transparent; --dot-size:22px;
|
|||
|
|
}
|
|||
|
|
@media (prefers-color-scheme: dark){
|
|||
|
|
:root:not([data-style]), :root[data-style="base"]{ /* base · dark */
|
|||
|
|
--color-bg-page:#0f1412; --color-bg-surface:#161d1a; --color-bg-subtle:#1d2622;
|
|||
|
|
--color-text-primary:#e8f1ea; --color-text-secondary:#adbcb2; --color-text-tertiary:#7f8f85;
|
|||
|
|
--color-border-subtle:#243029; --color-border-default:#2f3d36; --color-border-strong:#455349;
|
|||
|
|
--color-accent:#7fd3a0; --color-accent-subtle:#14301f; --color-on-accent:#08200f;
|
|||
|
|
--color-success:#6ede8a; --color-success-subtle:#12301c;
|
|||
|
|
--color-warning:#ffcf7a; --color-warning-subtle:#33260f;
|
|||
|
|
--color-danger:#ff8f85; --color-danger-subtle:#3a1613;
|
|||
|
|
--color-focus-ring:#7fd3a0;
|
|||
|
|
}}
|
|||
|
|
|
|||
|
|
/* ── 风格 2 · Archify 暗(令牌取自 archify 的 dark 主题) ────────────────── */
|
|||
|
|
:root[data-style="archify-dark"]{
|
|||
|
|
--color-bg-page:#020617; --color-bg-surface:#0f172a; --color-bg-subtle:#1e293b;
|
|||
|
|
--color-text-primary:#ffffff; --color-text-secondary:#94a3b8; --color-text-tertiary:#64748b;
|
|||
|
|
--color-border-subtle:#1e293b; --color-border-default:#334155; --color-border-strong:#475569;
|
|||
|
|
--color-accent:#34d399; /* archify --arrow-emphasis */
|
|||
|
|
--color-accent-subtle:rgba(52,211,153,0.14); --color-on-accent:#052e16;
|
|||
|
|
--color-success:#34d399; --color-success-subtle:rgba(52,211,153,0.14); /* backend-stroke */
|
|||
|
|
--color-warning:#fbbf24; --color-warning-subtle:rgba(251,191,36,0.14); /* cloud-stroke */
|
|||
|
|
--color-danger:#fb7185; --color-danger-subtle:rgba(251,113,133,0.16); /* security-stroke*/
|
|||
|
|
--color-focus-ring:#34d399;
|
|||
|
|
--color-canvas-dot:rgba(148,163,184,0.16); --dot-size:22px; /* --canvas-dot */
|
|||
|
|
--font-ui:ui-monospace,"Cascadia Mono","JetBrains Mono",Consolas,"Microsoft YaHei",monospace;
|
|||
|
|
--node-stroke-w:1.4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* ── 风格 3 · Archify 亮(令牌取自 archify 的 light 主题) ───────────────── */
|
|||
|
|
:root[data-style="archify-light"]{
|
|||
|
|
--color-bg-page:#f4f5f7; --color-bg-surface:#ffffff; --color-bg-subtle:#f1f5f9;
|
|||
|
|
--color-text-primary:#111827; --color-text-secondary:#5b6474; --color-text-tertiary:#9aa3b2;
|
|||
|
|
--color-border-subtle:#e5e7eb; --color-border-default:#cbd5e1; --color-border-strong:#94a3b8;
|
|||
|
|
--color-accent:#059669;
|
|||
|
|
--color-accent-subtle:rgba(5,150,105,0.10); --color-on-accent:#ffffff;
|
|||
|
|
--color-success:#059669; --color-success-subtle:rgba(52,211,153,0.20);
|
|||
|
|
--color-warning:#d97706; --color-warning-subtle:rgba(251,191,36,0.20);
|
|||
|
|
--color-danger:#e11d48; --color-danger-subtle:rgba(251,113,133,0.15);
|
|||
|
|
--color-focus-ring:#059669;
|
|||
|
|
--color-canvas-dot:#d9dee5; --dot-size:22px;
|
|||
|
|
--font-ui:ui-monospace,"Cascadia Mono","JetBrains Mono",Consolas,"Microsoft YaHei",monospace;
|
|||
|
|
--node-stroke-w:1.4;
|
|||
|
|
}
|
|||
|
|
*{box-sizing:border-box}
|
|||
|
|
body{margin:0;background:var(--color-bg-page);color:var(--color-text-primary);
|
|||
|
|
font-family:var(--font-ui);font-size:14px;line-height:1.6;font-weight:400}
|
|||
|
|
a,button{font:inherit;color:inherit}
|
|||
|
|
:focus-visible{outline:2px solid var(--color-focus-ring);outline-offset:2px}
|
|||
|
|
|
|||
|
|
header{padding:24px 32px 16px;border-bottom:1px solid var(--color-border-subtle);
|
|||
|
|
display:flex;flex-wrap:wrap;gap:16px;align-items:baseline}
|
|||
|
|
h1{margin:0;font-size:20px;font-weight:500;line-height:1.3}
|
|||
|
|
.lede{color:var(--color-text-secondary);font-size:13px}
|
|||
|
|
.spacer{flex:1}
|
|||
|
|
.stamp{color:var(--color-text-tertiary);font-size:12px;font-variant-numeric:tabular-nums}
|
|||
|
|
.hd-left{display:flex;flex-direction:column;gap:2px}
|
|||
|
|
.hd-left .lede strong,.hd-left .lede b{font-weight:500;color:var(--color-text-secondary)}
|
|||
|
|
.hd-right{display:flex;flex-direction:column;align-items:flex-end;gap:4px}
|
|||
|
|
.lag{font-size:12px;font-variant-numeric:tabular-nums;color:var(--color-text-tertiary)}
|
|||
|
|
.lag.is-old{color:var(--color-warning)}
|
|||
|
|
.ro{font-size:11px;color:var(--color-text-tertiary)}
|
|||
|
|
.warnbar{margin:0 0 24px;border:1px solid var(--color-warning);background:var(--color-warning-subtle);
|
|||
|
|
color:var(--color-warning);border-radius:var(--radius-12);padding:12px 16px;font-size:13px}
|
|||
|
|
|
|||
|
|
/* 「本项目」身份卡 —— 🔴 看板顶部第一眼就要说清"这是哪个需求目标项目" */
|
|||
|
|
.card.proj{border-color:var(--color-accent);background:var(--color-accent-subtle)}
|
|||
|
|
.pill{display:inline-flex;align-items:center;padding:3px 12px;border-radius:var(--radius-pill);
|
|||
|
|
font-size:12px;font-weight:500;color:var(--color-on-accent);background:var(--color-accent)}
|
|||
|
|
.crit{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
|||
|
|
.critLead{color:var(--color-text-secondary);font-size:12px}
|
|||
|
|
.crit .chip{background:var(--color-bg-surface)}
|
|||
|
|
/* 🆕 2026-09-30 任务类别行:一个工作区里可能有多个类别 ⇒ 每类一枚 chip,chip 里带**该类的主会话**。
|
|||
|
|
⚠️ 这里 ⛔ **不**覆盖 `.chip` 的底色 —— 要保留 `.is-warn`(无主会话那种)的警示色。 */
|
|||
|
|
.topics{margin-top:12px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
|||
|
|
.accRow{margin-top:10px;display:flex;flex-wrap:wrap;gap:8px;align-items:center}
|
|||
|
|
.accRow .critLead{color:var(--color-text-secondary)}
|
|||
|
|
.projGoal{display:flex;align-items:baseline;gap:10px;flex-wrap:wrap;font-size:17px;line-height:1.4}
|
|||
|
|
.projGoal b{font-weight:600}
|
|||
|
|
.accBar{height:6px;border-radius:3px;background:var(--color-border-tertiary);overflow:hidden;
|
|||
|
|
width:220px;flex:0 0 auto}
|
|||
|
|
.accBar i{display:block;height:100%;background:var(--color-success);transition:width .3s}
|
|||
|
|
.accBar.is-badn{background:var(--color-danger-subtle)}
|
|||
|
|
/* 🔴 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}
|
|||
|
|
.tab{display:flex;flex-direction:column;gap:3px;min-width:180px;max-width:420px;text-align:left;
|
|||
|
|
padding:9px 14px;border:1px solid var(--color-border-default);border-radius:var(--radius-12);
|
|||
|
|
background:var(--color-bg-surface);color:var(--color-text-secondary);font:inherit;font-size:13px;
|
|||
|
|
cursor:pointer;transition:border-color .15s,background .15s}
|
|||
|
|
.tab:hover{border-color:var(--color-accent)}
|
|||
|
|
.tab.is-on{border-color:var(--color-accent);background:var(--color-accent-subtle);
|
|||
|
|
color:var(--color-text-primary)}
|
|||
|
|
.tab .tt{display:flex;align-items:center;gap:6px;font-weight:500;color:var(--color-text-primary);
|
|||
|
|
white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|||
|
|
.tab .tsub{font-size:11px;color:var(--color-text-tertiary);white-space:nowrap;overflow:hidden;
|
|||
|
|
text-overflow:ellipsis}
|
|||
|
|
/* 🔴 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 条**只放数据**;使用说明一律进各板块右上角 `?`。 */
|
|||
|
|
td .role{display:inline-block;padding:1px 8px;border-radius:var(--radius-pill);font-size:11px;
|
|||
|
|
border:1px solid var(--color-border-default);color:var(--color-text-tertiary);white-space:nowrap}
|
|||
|
|
td .role.is-main{color:var(--color-accent);border-color:var(--color-accent);background:var(--color-accent-subtle)}
|
|||
|
|
|
|||
|
|
main{padding:24px 32px 48px;max-width:1440px;margin:0 auto}
|
|||
|
|
section{margin:0 0 24px}
|
|||
|
|
.card{background:var(--color-bg-surface);border:1px solid var(--color-border-subtle);
|
|||
|
|
border-radius:var(--radius-16);padding:20px}
|
|||
|
|
.card > h2{margin:0 0 16px;font-size:14px;font-weight:500;color:var(--color-text-secondary);padding-right:30px}
|
|||
|
|
.card > h2 .hint,.hero h2 .hint{font-weight:400;color:var(--color-text-tertiary);font-size:12px;margin-left:8px}
|
|||
|
|
|
|||
|
|
.hero{background:var(--color-bg-surface);border:1px solid var(--color-border-subtle);
|
|||
|
|
border-radius:var(--radius-16);padding:24px}
|
|||
|
|
.hero h2{margin:0 0 16px;font-size:16px;font-weight:500;padding-right:30px}
|
|||
|
|
.hero p{margin:0 0 16px;color:var(--color-text-tertiary);font-size:12px}
|
|||
|
|
svg.arch{width:100%;height:auto;display:block}
|
|||
|
|
/* 画布点阵(archify 的 --canvas-dot):base 风格为 transparent ⇒ 无点阵 */
|
|||
|
|
#archWrap{background-image:radial-gradient(var(--color-canvas-dot) 1px, transparent 1px);
|
|||
|
|
background-size:var(--dot-size) var(--dot-size);background-position:0 0;
|
|||
|
|
border-radius:var(--radius-12)}
|
|||
|
|
|
|||
|
|
/* ── 右上角「风格切换」(用户 2026-09-30 要求;默认=当前风格) ───────────── */
|
|||
|
|
.stylesw{display:inline-flex;border:1px solid var(--color-border-default);border-radius:var(--radius-pill);
|
|||
|
|
overflow:hidden;background:var(--color-bg-surface)}
|
|||
|
|
.stylesw button{border:0;background:transparent;color:var(--color-text-secondary);cursor:pointer;
|
|||
|
|
padding:4px 12px;font-size:12px;line-height:1.6;font-family:var(--font-ui);white-space:nowrap}
|
|||
|
|
.stylesw button + button{border-left:1px solid var(--color-border-subtle)}
|
|||
|
|
.stylesw button:hover{background:var(--color-bg-subtle);color:var(--color-text-primary)}
|
|||
|
|
.stylesw button.on{background:var(--color-accent);color:var(--color-on-accent);font-weight:500}
|
|||
|
|
.stylesw button:focus-visible{outline:2px solid var(--color-focus-ring);outline-offset:-2px}
|
|||
|
|
|
|||
|
|
/* ── 🔴 各板块右上角「?」:小字描述收进这里,鼠标移上/键盘聚焦才显示 ─────────────
|
|||
|
|
(用户 2026-09-30:「把各板块的小字描述放到各板块对应右上角 ?号图标中,鼠标移上去显示
|
|||
|
|
(**只保留标题,主体,类别标签**这类信息)」)
|
|||
|
|
⛔ 别再往版面里摊大段说明文字 —— 一律进 .qtip。 */
|
|||
|
|
.card,.hero{position:relative}
|
|||
|
|
.qh{position:absolute;top:12px;right:14px;z-index:6;width:20px;height:20px;padding:0;
|
|||
|
|
border-radius:50%;border:1px solid var(--color-border-default);background:var(--color-bg-subtle);
|
|||
|
|
color:var(--color-text-secondary);font-size:12px;font-weight:500;line-height:18px;text-align:center;
|
|||
|
|
cursor:help;font-family:var(--font-ui);user-select:none}
|
|||
|
|
.qh:hover,.qh:focus-visible{background:var(--color-accent);color:var(--color-on-accent);
|
|||
|
|
border-color:var(--color-accent);outline:none}
|
|||
|
|
.qh .qtip{position:absolute;top:26px;right:0;z-index:30;width:min(440px,78vw);padding:12px 14px;
|
|||
|
|
border-radius:var(--radius-12);border:1px solid var(--color-border-default);
|
|||
|
|
background:var(--color-bg-surface);box-shadow:var(--shadow-pop);
|
|||
|
|
color:var(--color-text-secondary);font-size:12px;font-weight:400;line-height:1.7;text-align:left;
|
|||
|
|
white-space:normal;cursor:default;opacity:0;visibility:hidden;transition:opacity .12s ease}
|
|||
|
|
.qh:hover .qtip,.qh:focus-visible .qtip{opacity:1;visibility:visible}
|
|||
|
|
@media (prefers-reduced-motion: reduce){.qh .qtip{transition:none}}
|
|||
|
|
.qtip b{color:var(--color-text-primary);font-weight:500}
|
|||
|
|
.qtip .kt{display:inline-block;padding:0 6px;border-radius:var(--radius-pill);
|
|||
|
|
border:1px solid var(--color-border-default);color:var(--color-text-tertiary);font-size:11px}
|
|||
|
|
/* 「协作架构」板块右上角:延迟算式 + 保留的快照时刻(固定可见,⛔ 不收进 ?) */
|
|||
|
|
.herometa{position:absolute;top:12px;right:52px;text-align:right;line-height:1.5;
|
|||
|
|
font-size:12px;font-variant-numeric:tabular-nums;color:var(--color-text-tertiary)}
|
|||
|
|
.herometa > span{display:block;white-space:nowrap} /* ⚠️ 只作用于直接子元素:写 `.herometa span` 会把内层 #lag 也变成块 ⇒ "当前 3 秒"被拆两行 */
|
|||
|
|
.herometa b{color:var(--color-text-primary);font-weight:500}
|
|||
|
|
.herometa .sec{color:var(--color-text-secondary)}
|
|||
|
|
.herometa.is-old .sec{color:var(--color-warning)}
|
|||
|
|
@media(max-width:820px){.herometa{position:static;text-align:left;margin:0 0 12px}}
|
|||
|
|
|
|||
|
|
.cols-3{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:24px}
|
|||
|
|
.cols-2{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:24px}
|
|||
|
|
@media(max-width:1100px){.cols-3,.cols-2{grid-template-columns:1fr}}
|
|||
|
|
|
|||
|
|
.chip{display:inline-flex;align-items:center;gap:4px;padding:4px 12px;border-radius:var(--radius-pill);
|
|||
|
|
font-size:12px;font-weight:400;border:1px solid var(--color-border-default);
|
|||
|
|
color:var(--color-text-secondary);background:var(--color-bg-subtle)}
|
|||
|
|
.chip.is-ok{color:var(--color-success);background:var(--color-success-subtle);border-color:var(--color-success)}
|
|||
|
|
.chip.is-bad{color:var(--color-danger);background:var(--color-danger-subtle);border-color:var(--color-danger)}
|
|||
|
|
.chip.is-warn{color:var(--color-warning);background:var(--color-warning-subtle);border-color:var(--color-warning)}
|
|||
|
|
.rowchips{display:flex;flex-wrap:wrap;gap:8px}
|
|||
|
|
|
|||
|
|
table{width:100%;border-collapse:collapse;font-size:13px}
|
|||
|
|
th,td{padding:8px 8px;border-bottom:1px solid var(--color-border-subtle);text-align:left;vertical-align:top}
|
|||
|
|
th{font-weight:500;color:var(--color-text-tertiary);font-size:12px}
|
|||
|
|
tr:last-child td{border-bottom:0}
|
|||
|
|
td.num{font-variant-numeric:tabular-nums;color:var(--color-text-tertiary);font-size:12px;white-space:nowrap}
|
|||
|
|
td .ellip{display:block;max-width:34ch;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
|||
|
|
/* 🔴 弱化说明(台账「任务类别」列里标旧值/项目卡里标类别来源):⛔ 只弱化,⛔ 不隐藏 */
|
|||
|
|
.oldline{color:var(--color-text-tertiary);font-size:12px}
|
|||
|
|
.warnTx{color:var(--color-warning);font-weight:600}
|
|||
|
|
code{font-family:ui-monospace,Consolas,monospace;font-size:12px;color:var(--color-text-secondary)}
|
|||
|
|
.empty{padding:16px 0;color:var(--color-text-tertiary);font-size:13px}
|
|||
|
|
.empty strong{display:block;color:var(--color-text-secondary);font-weight:500;margin-bottom:4px}
|
|||
|
|
.err{border-color:var(--color-danger);background:var(--color-danger-subtle);color:var(--color-danger);
|
|||
|
|
border-radius:var(--radius-12);padding:16px;font-size:13px}
|
|||
|
|
.skel{height:14px;border-radius:var(--radius-8);background:var(--color-bg-subtle);margin:8px 0}
|
|||
|
|
|
|||
|
|
.legend{display:flex;flex-wrap:wrap;gap:16px;color:var(--color-text-tertiary);font-size:12px;margin-top:8px}
|
|||
|
|
.legend i{display:inline-block;width:12px;height:12px;border-radius:4px;margin-right:6px;
|
|||
|
|
vertical-align:-1px;border:1px solid var(--color-border-default)}
|
|||
|
|
/* 🔴 2026-10-01(用户:「**这段乱七八糟的 也精简 分段落描述**」)——
|
|||
|
|
架构图下面的说明原先是**一整行** `… · … · …`(八九条串起来,读起来是一堵墙)。
|
|||
|
|
⇒ 改成**分块 + 一行一条**:块 = 一个小标题(「当前」「<唤醒会话名>」),
|
|||
|
|
块内每条独立成行、行首一个小点。⛔ 别再退回 `join(' · ')` 那种串行。 */
|
|||
|
|
.archNotes{margin-top:12px;display:flex;flex-direction:column;gap:10px}
|
|||
|
|
.archNotes .anBlock{display:flex;flex-direction:column;gap:3px}
|
|||
|
|
.archNotes .anLead{color:var(--color-text-secondary);font-size:12px;font-weight:500}
|
|||
|
|
.archNotes .anItem{color:var(--color-text-tertiary);font-size:12px;line-height:1.65;
|
|||
|
|
padding-left:14px;position:relative}
|
|||
|
|
.archNotes .anItem::before{content:'·';position:absolute;left:3px}
|
|||
|
|
|
|||
|
|
.n-title{fill:var(--color-text-primary);font-size:16px;font-weight:500}
|
|||
|
|
.n-title-sm{fill:var(--color-text-primary);font-size:13px;font-weight:500}
|
|||
|
|
.n-sub{fill:var(--color-text-tertiary);font-size:11px}
|
|||
|
|
.n-stat{fill:var(--color-accent);font-size:13px;font-weight:500}
|
|||
|
|
.n-ok{fill:var(--color-success);font-size:12px;font-weight:500}
|
|||
|
|
.n-idle{fill:var(--color-text-tertiary);font-size:12px}
|
|||
|
|
.n-warn{fill:var(--color-warning);font-size:12px;font-weight:500}
|
|||
|
|
.n-bad{fill:var(--color-danger);font-size:12px;font-weight:500}
|
|||
|
|
.e-label{fill:var(--color-text-tertiary);font-size:12px}
|
|||
|
|
.node-bg{fill:var(--color-bg-surface);stroke:var(--color-border-default)}
|
|||
|
|
.node-on{fill:var(--color-success-subtle);stroke:var(--color-success)}
|
|||
|
|
.node-warn{fill:var(--color-warning-subtle);stroke:var(--color-warning)}
|
|||
|
|
.node-off{fill:var(--color-danger-subtle);stroke:var(--color-danger);stroke-dasharray:4 3}
|
|||
|
|
.node-base{fill:var(--color-bg-subtle);stroke:var(--color-border-strong);stroke-dasharray:5 4}
|
|||
|
|
.node-bg,.node-on,.node-warn,.node-off,.node-base,.trig-node{stroke-width:var(--node-stroke-w)}
|
|||
|
|
/* 🔴 唤醒会话(**位置:主会话下面那一行(`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>
|
|||
|
|
<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`(语法过 ≠ 不抛异常 —— 这一条本轮当场被证实)。 */
|
|||
|
|
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;i++){
|
|||
|
|
var t=segs[i].replace(/^\*+/,'').trim().toLowerCase();
|
|||
|
|
if(t.indexOf('pass')===0||t.indexOf('过')===0||t.indexOf('通过')===0){ ok=true; break; }
|
|||
|
|
}
|
|||
|
|
return ok;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function renderProject(d){
|
|||
|
|
var p=d.project||{}, acc=(d.goal||{}).acceptance||{}, keys=Object.keys(acc).sort();
|
|||
|
|
var src=p.topics_source||{}, mbt=p.main_by_topic||{}, tops=p.topics||[];
|
|||
|
|
/* ── ① 目标(主角)── */
|
|||
|
|
var goalHtml='<div class="projGoal"><span class="critLead">目标</span>'
|
|||
|
|
+'<span class="pill">'+esc(p.short||'(未声明主题)')+'</span>'
|
|||
|
|
+'<b>'+esc(p.title||'(未声明目标)')+'</b></div>';
|
|||
|
|
/* ── ② 完成情况(第二主角)──
|
|||
|
|
⚠️ 2026-09-30:**没有有效验收判据时 ⛔ 不许显示成"无验收记录"那种中性字样** ——
|
|||
|
|
判据是"**有没有有效项**",不是"字典空不空"(只写 `_说明` 行时旧代码会当成"全过")。
|
|||
|
|
⚠️ 非 pass 的**逐条点名** —— 只写"4/5 通过"看不出**还差什么**(用户要的是"看完成情况")。 */
|
|||
|
|
/* 🔴🔴 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">未完:'+bad.map(function(k){
|
|||
|
|
return esc(k)+'('+esc(acc[k])+')'}).join('、')+'</span>'
|
|||
|
|
: '<span class="oldline">全部通过</span>')
|
|||
|
|
+'</div>';
|
|||
|
|
}else{
|
|||
|
|
accHtml='<div class="accRow"><span class="critLead">完成情况</span>'
|
|||
|
|
+chip('⚠️ 未声明验收判据(⛔ 不因此判完成)','is-warn')+'</div>';
|
|||
|
|
}
|
|||
|
|
/* ── ③ 任务类别:**保留成 chips**(⛔ 不压进描述列)——
|
|||
|
|
因为它带"**该类的主会话是谁**",是**操作性信息**;而"属性/规则"只是背景。
|
|||
|
|
🆕 任务类别(用户:「同一个工作区 多会话协作…**通过协作会话名称前缀**区分具体任务会话」)
|
|||
|
|
⇒ 每个类别一枚 chip,且 chip 里带**该类的主会话** —— ⛔ 否则看不出"哪条管哪一类";
|
|||
|
|
没有主会话的类别用警示色(那类的件投出去会被降级报告)。
|
|||
|
|
🔴 2026-10-01 用户:「**看板要精简的展示重点有效的信息,不是写说明书**」
|
|||
|
|
⇒ 括注("同一个工作区 · 按名称前缀区分")**删掉** —— 标签就叫「任务类别」,够了;
|
|||
|
|
**来源细节**(什么时候、谁说明的)挪进 chip 的悬停提示,⛔ 不占版面。 */
|
|||
|
|
var tp=tops.length
|
|||
|
|
? tops.map(function(t){ var s=mbt[t]||'';
|
|||
|
|
var _tip='来源:'+(src.kind==='declared'
|
|||
|
|
? ('调用时在对话里说明'+(src.at?(' · '+src.at):'')+(src.by?(' · '+src.by):''))
|
|||
|
|
: (src.kind==='fallback' ? '⛔ 还没在对话里说明过(暂按目标简称走单类别)' : '⛔ 目标与类别都还没声明'));
|
|||
|
|
return '<span class="chip'+(s?'':' is-warn')+'" title="'+esc(_tip)+'">'+esc(t)
|
|||
|
|
+(s?(' 主会话 <code>'+esc(s)+'</code>'):' ⚠️ 无主会话')+'</span>'}).join('')
|
|||
|
|
: '<span class="chip is-warn">(未声明任务类别)</span>';
|
|||
|
|
/* ⚠️ 「**没声明过**」必须留在**版面上**(同族红线:有却不说 / 没有却说有)——
|
|||
|
|
但只有一句,⛔ 不再带"怎么说明"那种指导语(那是说明书,进 `?`)。 */
|
|||
|
|
var tpLead = (src.kind==='fallback')
|
|||
|
|
? '<span class="critLead">任务类别 <b class="warnTx">⚠ 未声明</b></span>'
|
|||
|
|
: '<span class="critLead">任务类别</span>';
|
|||
|
|
document.getElementById('proj').innerHTML = goalHtml + accHtml
|
|||
|
|
+'<div class="topics">'+tpLead+tp+'</div>';
|
|||
|
|
|
|||
|
|
/* 🔴🔴 2026-10-01(用户两次:「**看板要精简的展示重点有效的信息,不是写说明书**」)
|
|||
|
|
⇒ 版面上**只留三行**:目标 / 完成情况 / 任务类别。
|
|||
|
|
其余全是**背景与规则**(目标 id、默认主会话、工作区、归属判据、类别来源的时间与人),
|
|||
|
|
⛔ 不再占版面 —— 它们**一条都没丢**,全部追加进这一块右上角 `?` 里
|
|||
|
|
(`?` 本来就是"小字说明"的正式落点;本文件 `frontTip` 早就是这个写法,此处同款)。
|
|||
|
|
⚠️ 动态部分每轮**先还原 static 文案**再追加 ⇒ ⛔ 不会一轮轮叠加。 */
|
|||
|
|
var tip=document.getElementById('projTip');
|
|||
|
|
if(tip){
|
|||
|
|
if(!tip.dataset.base) tip.dataset.base=tip.innerHTML; /* 首轮把静态文案存下来 */
|
|||
|
|
tip.innerHTML=tip.dataset.base; /* 每轮先还原,防重复追加 */
|
|||
|
|
var _crit=(p.criteria||[]).map(esc).join(' / ')||'—';
|
|||
|
|
tip.innerHTML+='<br><br><b>这一格目标的依据</b>'
|
|||
|
|
+'(目标 id <code>'+esc(p.id||'—')+'</code>'
|
|||
|
|
+' · 默认主会话 <code>'+esc(p.main_sid8||'—')+'</code>'
|
|||
|
|
+' · 工作区 <code>'+esc(tailOf(p.workspace))+'</code>)<br>'
|
|||
|
|
+'<b>归属判据</b>(任一命中即算作本项目):'+esc(_crit)+'<br>'
|
|||
|
|
+'<b>任务类别的来源</b>:'+(src.kind==='declared'
|
|||
|
|
? ('调用时在对话里说明'+(src.at?(' · '+esc(src.at)):'')+(src.by?(' · '+esc(src.by)):''))
|
|||
|
|
: (src.kind==='fallback'
|
|||
|
|
? '⛔ 还没在对话里说明过 ⇒ 现按目标简称走单类别;说明方式:调用本技能时讲清「任务类别」'
|
|||
|
|
: '⛔ 目标与类别都还没声明(调用本技能时在对话里说明)'));
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 🔴🔴 架构图布局(**用户 2026-09-30 起逐次指定**)—— 纵向,从上到下:
|
|||
|
|
① 用户 ② **主会话**(独占一行) ②′ **唤醒 + 跟进**(单独一行,左唤醒右跟进)
|
|||
|
|
③ 协作会话1 … 协作会话n(**个数是活的**,按本项目在册会话渲染)
|
|||
|
|
④ 协作 + 上报(旧名「监督程序」,并排) ⑤ 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`)与绘制一并删除。
|
|||
|
|
// ⛔ 若将来要恢复:从备份 `board.html.bak-两类改造-20261002-2343` 取,
|
|||
|
|
// **并**要恢复 `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 四态:待执行 '+((qb.pending)||0)+' / 执行中 '+((qb.running)||0)
|
|||
|
|
+' / 已完成 '+((qb.done)||0)+' / 有阻碍 '+((qb.blocked)||0)+'。机制里没有「待验收」这个态。'
|
|||
|
|
+'<br>职责:接收=--report 写台账;处理=建检查会话排期;常驻一直运行。'
|
|||
|
|
+'<br>'+esc(pr.how||'')+'</title></rect>');
|
|||
|
|
/* ⚠️ `R4.h=112` ⇒ **基线最大约 96**(留 16px 下边距)⇒ ⛔ 别再往这格加第 5 行;
|
|||
|
|
加第 5 行必须先 `R4.h` 加高(并同步 `R5.y`/走线带,`B3` 是自动算的)。
|
|||
|
|
静态职责文案已搬进上方 `<title>` tooltip(同「说明只进 ?」口径,不占版面)。 */
|
|||
|
|
tx(PX+18,R4.y+26,'执行检查','n-title');
|
|||
|
|
tx(PX+18,R4.y+50, '队列 '+((q.total)||0)+' 件 · 已完成 '+((qb.done)||0)+' · 未完 '+((q.open)||0)
|
|||
|
|
+(((qb.blocked)||0)?(' · 受阻 '+qb.blocked):''), ((q.open)||0)?'n-warn':'n-idle');
|
|||
|
|
/* 🔴 2026-10-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 报错**)。
|
|||
|
|
✅ **处置=连注释带变量一起删**(不是留个注释占位)。原文照抄见备份
|
|||
|
|
`board.html.bak-投递退役-20261003-0700` 与 `board.html.bak-两格合一-20261003-0720`。
|
|||
|
|
⚠️ 曾经踩过的坑(留着提醒):①「上报 → 跟进」折线方向反了 —— 真因是
|
|||
|
|
`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` 的头部注释**,⛔ 别照着旧文案改(那正是它说假话的来源)。 */
|
|||
|
|
tx(hkX+20,R5.y+84, fitText('两个模式:--once(读判定出信号)/ --tick(续命常驻+结束即检查)', hkW-36, 11, 6), 'n-sub');
|
|||
|
|
// WorkBuddy → 钩子(它在钩子时机起它)
|
|||
|
|
edge('M'+(hkX+70)+' '+RB.y+' L'+(hkX+70)+' '+(R5.y+R5.h), true);
|
|||
|
|
lbl(hkX+82, (RB.y+R5.y+R5.h)/2, '用时才喊它', 'start');
|
|||
|
|
// 钩子 → 协作程序(--once):直接向上
|
|||
|
|
edge('M'+(hkX+40)+' '+(R5.y)+' L'+(hkX+40)+' '+(R4.y+R4.h), true);
|
|||
|
|
lbl(hkX+52, (R5.y+R4.y+R4.h)/2, '--once', 'start');
|
|||
|
|
/* 钩子 → 协作程序(--tick):🔴 **一条纯竖直的线**(用户 2026-10-01:
|
|||
|
|
「Hook进程 框图宽度往右边延长一些,**让 tick 那条线能竖直摆放**」)。
|
|||
|
|
🔴🔴 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, '--tick', 'start');
|
|||
|
|
|
|||
|
|
// ── 最底一条:WorkBuddy
|
|||
|
|
// (用户 2026-09-30:「**宿主地基 这个就叫 WorkBuddy** 不要用这些AI语言」⇒ ⛔ 不用自造词)
|
|||
|
|
rect(SX0,RB.y,SX1-SX0,RB.h,'base');
|
|||
|
|
tx(SX0+20,RB.y+33,'WorkBuddy · 只读它的数据库 / 排期 / 钩子 / 文件占用登记','n-sub');
|
|||
|
|
// ✅ 折线几何(全部现算,⛔ 不写死):`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 = 目标名 + 状态徽章 + 完成情况摘要(都是实读,⛔ 不写死)。 */
|
|||
|
|
function renderTabs(gs){
|
|||
|
|
var box=document.getElementById('tabs');
|
|||
|
|
if(!box) return;
|
|||
|
|
if(!gs.length){ box.hidden=true; return; }
|
|||
|
|
box.hidden=false;
|
|||
|
|
box.innerHTML=gs.map(function(g,i){
|
|||
|
|
var acc=(g.goal||{}).acceptance||{}, ks=Object.keys(acc);
|
|||
|
|
/* 🔴 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-01(用户:「**下面这段话是干啥的,为什么也在 tab 区域**」)——
|
|||
|
|
**tab 条上⛔ 不放任何说明文字**。上一版在两格之间塞了一句「只登记了 1 个目标 ⇒ 只有一格 /
|
|||
|
|
要并行看第二个目标:在 goals/ 里放一份 …json」—— 那是**使用说明**,不是**数据**,
|
|||
|
|
摊在版面上就成了噪声(同族老毛病:把小字说明写到版面上)。
|
|||
|
|
⇒ **删掉**;"怎么加第二个目标"搬进「本项目」右上角 `?` 里(见 projCard 的 qtip)。
|
|||
|
|
⛔ 也别退回「共 N 个目标」那种话 —— 有几格**看格子就知道**,写出来信息量为零。 */
|
|||
|
|
[].forEach.call(box.querySelectorAll('.tab'), function(b){
|
|||
|
|
b.onclick=function(){
|
|||
|
|
var i=Number(b.dataset.gi)||0;
|
|||
|
|
var k=goalKey(gs[i]);
|
|||
|
|
location.hash='g='+encodeURIComponent(k);
|
|||
|
|
GIDX=i;
|
|||
|
|
if(D) paintScope(D); /* ⛔ 不重新拉数据 ⇒ 切 tab 是**零延迟零请求**的 */
|
|||
|
|
};
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
/* 把"当前目标"那一段投影成顶层字段的形状 ⇒ 下面四个渲染器**原样复用**(⛔ 不复制代码)。 */
|
|||
|
|
function scopeView(d,g){
|
|||
|
|
if(!g||g===d) return d;
|
|||
|
|
var v={}, k;
|
|||
|
|
for(k in d) if(Object.prototype.hasOwnProperty.call(d,k)) v[k]=d[k];
|
|||
|
|
v.goal=g.goal||d.goal; v.project=g.project||d.project;
|
|||
|
|
v.tasks=g.tasks||{}; v.sessions=g.sessions||[];
|
|||
|
|
v.labor=g.labor||[]; v.orphan=g.orphan||{};
|
|||
|
|
v.queue=g.queue||{}; v.progress=g.progress||{};
|
|||
|
|
v.others_running=g.others_running;
|
|||
|
|
/* 🔴🔴 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;
|
|||
|
|
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;
|
|||
|
|
}
|
|||
|
|
fetch('board.json?t='+Date.now(),{cache:'no-store'}).then(function(r){
|
|||
|
|
if(!r.ok) throw new Error('HTTP '+r.status);
|
|||
|
|
return r.json();
|
|||
|
|
}).then(render).catch(function(e){
|
|||
|
|
var el=document.getElementById('err'); el.hidden=false;
|
|||
|
|
el.textContent='快照读取失败:'+e.message+'(服务还开着吗?重跑 python board.py --serve 8788)';
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
/* 🔴 轮询节奏 = 服务的快照节奏(⛔ 页面不自行加频:加了只是空转,不会更快) */
|
|||
|
|
var POLL=3000;
|
|||
|
|
function loop(){ load(); setTimeout(loop, POLL); }
|
|||
|
|
initStyleSwitcher();
|
|||
|
|
if(LIVE){ loop(); setInterval(paintAge,1000); } else { load(); }
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|