@yangdcm/dsh-expert-team 1.3.12 → 1.3.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +64 -0
- package/README.en.md +6 -2
- package/README.md +6 -2
- package/client.js +112 -17
- package/lib/command.js +117 -19
- package/lib/interception.js +6 -4
- package/package.json +2 -2
- package/skills/expert-team/SKILL.md +31 -30
- package/skills/expert-team/assets/templates/SUMMARY.md +2 -2
- package/skills/expert-team/assets/templates//344/273/273/345/212/241/347/234/213/346/235/277.md +1 -1
- package/skills/expert-team/references/EFFICIENCY.md +1 -1
- package/skills/expert-team/references/LOGGING.md +7 -7
- package/skills/expert-team/references/PERSIST.md +9 -9
- package/skills/expert-team/references/PIPELINE.md +1 -1
- package/skills/expert-team/references/ROLES.md +32 -32
- package/skills/expert-team/references/WORKSPACE.md +19 -19
- package/skills/expert-team/references/workflow.team.js +6 -6
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,70 @@
|
|
|
3
3
|
本包遵循[语义化版本](https://semver.org/lang/zh-CN/)。dsh 宿主版本线的对应关系写在
|
|
4
4
|
`package.json` 的 `engines.dsh` 与 `dsh.compatibility` 里,插件市场按它判断"这个插件跟你的宿主兼不兼容"。
|
|
5
5
|
|
|
6
|
+
## 1.3.14
|
|
7
|
+
|
|
8
|
+
**性能收尾:同一时刻至多一条重活 + profile 补账**(性能修复 #4)
|
|
9
|
+
|
|
10
|
+
- **真相换了一次**:上一版(1.3.13)把首屏从 3.4 s 打到 9–16 ms ✓,但真机连续采样发现
|
|
11
|
+
**重分节单次 4 805 ms、每 ≥6 s 一次**,而且**重活在飞时 summary 被拖到 772 ms**。
|
|
12
|
+
本轮先补账、再按数据改。
|
|
13
|
+
- **profile 补账(`DSH_EXPERT_TEAM_STATE_PROFILE=1`)**:原先只有 `runs+select / subs / wfLabels / roles / tail`
|
|
14
|
+
五个点,其中 `tail` 把"从角色解析之后到回响应"的**全部**工作都吞进一段(真机上那段约 2 s 无账)。
|
|
15
|
+
现按执行顺序补齐:`people-enrich`(过滤/映射/登记/workflow 元数据/enrich)、`agents`(agents 映射 + 实时投影)、
|
|
16
|
+
`assemble`(违规/schema 校验/告警)、`artifacts`(`RUN.log` 尾 + git)、`tail`(持久化 + 序列化)
|
|
17
|
+
⇒ **账目逐段对齐,不再有"无账的 2 秒"**。
|
|
18
|
+
- **`feed` 也依赖 subs**:原先只有 `want('people')` 才算 subs,于是单独请求 `section=feed` 会**静默返回空 feed**
|
|
19
|
+
—— 典型的"两种零分不清"。现在 `needSubs = want('people') || want('feed')`,该花的一次枚举照花。
|
|
20
|
+
- **新增两道上限(都带如实降级)**:
|
|
21
|
+
- `DSH_EXPERT_TEAM_ARTIFACTS_DEADLINE_MS`(默认 2 500 ms):artifacts 段要读 `RUN.log` 尾、还要跑 `git`,
|
|
22
|
+
是**全链最不可控**的一段。超期限就跳过,并如实记 `artifacts:deadline`(读完日志再超就记 `files:deadline`),
|
|
23
|
+
跳过时**不写**那些键 —— 缺块 ≠ 空数据。
|
|
24
|
+
- `DSH_EXPERT_TEAM_MAX_FEED_AGENTS`(默认 60):feed 按子代理条数封顶,被挡住的条数记 `feed:<n>`。
|
|
25
|
+
- **客户端:重分节改为"只拉当前可见子标签那一块"**(此前无条件每 ≥6 s 拉 `people,feed,artifacts`):
|
|
26
|
+
- `team`(人)→ `people,feed`;`tasks`(事)→ `people,feed`;`info`(料)→ `artifacts`;
|
|
27
|
+
**`board`(盘)→ 不拉任何重分节**(它只渲染 `runs[]`,而 runs 在 `summary` 里就有);
|
|
28
|
+
- 「事」点开任务详情(需要 `files`/`logTail`)时才**一次性**补拉 `artifacts`,不常驻;
|
|
29
|
+
- 映射收敛成**只有两个 canonical 重分节 URL**(`people,feed` / `artifacts`)⇒ `stateHub` 的同 URL 在飞合并
|
|
30
|
+
能把面板、画布、徽章压成**同一条**。
|
|
31
|
+
- **徽章不再与「料」标签并发两条重活**:徽章原先一直自己订阅 `people,feed`(2.5 s 一拍 ⇒ 重活常驻)。现在
|
|
32
|
+
面板/画布开着时由它们**发布**同一份 payload(`publishToLive`,发布时**合并**:缺块 ≠ 空数据),
|
|
33
|
+
徽章只在**无人发布**时才自己轮询,且节奏放宽到 **10 s**(子代理出现时另有 `liveTick()` 主动催一次)。
|
|
34
|
+
- **真机数字的解释(诚实标注口径)**:`4 805 ms ≈ 2 × 2 400 ms` —— 当时画布同时订阅了 `people,feed` 与
|
|
35
|
+
`people,feed,artifacts` 两条**不同 URL** 的重活,服务端在同一事件循环上串行处理 ⇒ 各自看着慢一倍,
|
|
36
|
+
并把 `summary` 拖到 772 ms。本轮把重活收敛成"同一时刻至多一条",这类并发在结构上不再可能。
|
|
37
|
+
- 本机可复现的旁证(真实 run、进程内计时):`snapshotRun` **1.3 ms**、`runLogTail` **0.3 ms**、
|
|
38
|
+
`liveFiles`(git)**0.3 ms 冷 / 0.0 ms 命中缓存** ⇒ artifacts 段**不是**那 2 s 的来源,
|
|
39
|
+
与"并发才是主因"的判断一致。
|
|
40
|
+
- 端到端复测仍归用户(本机装不进运行中的宿主):预期**同一时刻只有一条重活**、
|
|
41
|
+
`summary` 在重活在飞时仍为**十毫秒级**(此前 772 ms)、停留在某标签时后台不再出现不相干分节。
|
|
42
|
+
## 1.3.13
|
|
43
|
+
|
|
44
|
+
**渐进式状态:`/state` 支持 `?section=`,首屏与会话体量解耦**(性能修复 #3)
|
|
45
|
+
|
|
46
|
+
- 背景(真机实测,`DSH_EXPERT_TEAM_STATE_PROFILE=1` 分步):完整负载里 `subs` **1 327–1 388 ms**、
|
|
47
|
+
`roles` **638–869 ms**、`runs+select` 15–21 ms、`tail` 3 ms;重会话(20+ 子代理、仍在活跃写日志)
|
|
48
|
+
总耗时 **2.1–6.2 s**,成本随「子代理数 × 日志体量」线性增长。
|
|
49
|
+
- 新增 `?section=summary|people|feed|artifacts`(逗号可多选;**缺省或 **ALL** = 完整负载、字段语义不变**
|
|
50
|
+
⇒ 老客户端/脚本/既有测试不受影响):
|
|
51
|
+
- `summary`(首屏用):阶段/进度/计数/违规/告警 —— 真机 `snapshotRun` 中位 **0.3 ms**;
|
|
52
|
+
- `people`:子会话清单/角色/成员时间线(最贵的那两块就在这里);
|
|
53
|
+
- `feed`:每 agent 最近事件;`artifacts`:`RUN.log` 尾 + 工作区改动文件。
|
|
54
|
+
- **上限 + 如实降级**:角色解析硬上限 `DSH_EXPERT_TEAM_MAX_ROLE_SUBS`(默认 60;超出部分如实进
|
|
55
|
+
`degraded: "roles:<n>"`);people 软期限 `DSH_EXPERT_TEAM_PEOPLE_DEADLINE_MS`(默认 2500 ms;超时跳过
|
|
56
|
+
workflow 元数据并记 `wf:deadline`)。响应新增 `sections`/`degraded`/`degradedReason`(缺块 ≠ 空数据)。
|
|
57
|
+
- **副作用修正**:`summary` 路径不再触发「派工即登记」写 `STATE.json`(原先每轮询都可能因空 `subById`
|
|
58
|
+
算出"清空成员"而覆写工件)⇒ **摘要轮询是纯读**。
|
|
59
|
+
- 客户端:**一快一慢双订阅**(`summary` 按 `pollMs`;`people,feed,artifacts` ≥ 6 s)+
|
|
60
|
+
`mergeStatePayload` **合并**(摘要那一拍不再抹掉已知成员);`stateHub` 改为**按 URL 各自计时**
|
|
61
|
+
(`stateHubBaseOf/DueAt`,tick 只发已到期的 URL)—— 旧实现"全局取最小 base、每 tick 拉全部 URL"
|
|
62
|
+
正是"慢端点被快钟拖着跑"的根源。
|
|
63
|
+
- 口径对齐(同批):`templates` 常量与两个 e2e 的"run 必需文件"清单**语义不同、关系确定**,
|
|
64
|
+
由 `artifact-ownership.test.mjs` 的 **E 断言**钉死(常量 ≡ 模板目录;e2e 清单 ≡ 常量 ∪ `RUN.log.md`);
|
|
65
|
+
README/README.en/llms 写实 `METRICS.md` 的来源与位置(**`team/` 根的聚合快照**,`/team learn` 产出),
|
|
66
|
+
并补一句 **R1 工件归属**(工件由产出它的角色自己写;lead 只读门控)。
|
|
67
|
+
- 验证:`npm run test:all` EXIT=0(**82 个测试文件**;新增 `state-sections.test.mjs`);
|
|
68
|
+
变异目录随源码文本**同提交**更新(`M70` 的 `find` 串)。
|
|
69
|
+
|
|
6
70
|
## 1.3.12
|
|
7
71
|
|
|
8
72
|
**面向"可被 AI 检索与引用"的文档与元数据补齐**(纯文档/元数据,代码与 1.3.11 完全相同)。
|
package/README.en.md
CHANGED
|
@@ -90,9 +90,10 @@ $ /team build a payments module with login
|
|
|
90
90
|
├─ implement backend … fan out along the DAG → edits your workspace directly
|
|
91
91
|
├─ review sec·reviewer independent review (cross-model) → REVIEW.md
|
|
92
92
|
├─ test qa repro, coverage, regressions → TEST.md
|
|
93
|
-
└─ deliver docs wrap-up
|
|
93
|
+
└─ deliver docs wrap-up → SUMMARY.md
|
|
94
94
|
|
|
95
95
|
Persisted throughout: TASKS.json · ROSTER.json · STATE.json · AUTHORITY.md · RUN.log.md
|
|
96
|
+
Aggregate snapshot: `METRICS.md` lives at the **`team/` root** (produced by `/team learn`; not a run-dir template)
|
|
96
97
|
On disk at: <your workspace>/team/<run-id>/
|
|
97
98
|
```
|
|
98
99
|
|
|
@@ -358,7 +359,10 @@ Full command list (`/team codeindex` code index, `/team limit` quotas, `/team se
|
|
|
358
359
|
|
|
359
360
|
**Phases (9)**: `clarify` → `research` → `design` → `spec-review` (**hard gate**) → `方案确认` plan approval (**approval gate**, on by default, can be turned off) → `implement` → `review` → `test` → `deliver`. (Ids and Chinese labels come from the single source `lib/vocab.js`.)
|
|
360
361
|
|
|
361
|
-
**Key artifacts**: `SPEC.md` (spec and boundaries) · `RESEARCH.md` · `PLAN.md` · `TASKS.json` (task ledger) · `ROSTER.json` (staffing) · `STATE.json` · `AUTHORITY.md` (single source for write authority) · `REVIEW.md` · `TEST.md` · `SUMMARY.md` · `METRICS.md` (cost and timing) · `RUN.log.md`
|
|
362
|
+
**Key artifacts**: `SPEC.md` (spec and boundaries) · `RESEARCH.md` · `PLAN.md` · `TASKS.json` (task ledger) · `ROSTER.json` (staffing) · `STATE.json` · `AUTHORITY.md` (single source for write authority) · `REVIEW.md` · `TEST.md` · `SUMMARY.md` · `METRICS.md` (cost and timing — an **aggregate snapshot at the `team/` root**, produced by `/team learn`) · `RUN.log.md`
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
**Who writes**: each artifact is written **by the role that produces it** into `<run-dir>/`; the lead **only reads artifacts to gate and decide** (it has no `write` tool). `STATE.json` and `RUN.log.md` are maintained **by the runtime**.
|
|
362
366
|
|
|
363
367
|
## Layout
|
|
364
368
|
|
package/README.md
CHANGED
|
@@ -84,9 +84,10 @@ $ /team 做一个带登录的支付模块
|
|
|
84
84
|
├─ 实现 backend … 按依赖 DAG 并行扇出 → 直接改你工作区的代码
|
|
85
85
|
├─ 审查 sec·reviewer 独立评审(换模型交叉验证) → REVIEW.md
|
|
86
86
|
├─ 测试 qa 复现、覆盖、回归 → TEST.md
|
|
87
|
-
└─ 交付 docs
|
|
87
|
+
└─ 交付 docs 收尾结论 → SUMMARY.md
|
|
88
88
|
|
|
89
89
|
全程留痕:TASKS.json · ROSTER.json · STATE.json · AUTHORITY.md · RUN.log.md
|
|
90
|
+
聚合快照:`METRICS.md` 在 **`team/` 根**(`/team learn` 产出;不是 run 目录模板)
|
|
90
91
|
落盘位置:<你的工作区>/team/<run-id>/
|
|
91
92
|
```
|
|
92
93
|
|
|
@@ -317,7 +318,10 @@ dsh plugin --profile web add dshmarket
|
|
|
317
318
|
|
|
318
319
|
**阶段(9)**:`clarify` 澄清 → `research` 调研 → `design` 设计 → `spec-review` 规格评审(**硬门**)→ `方案确认` 方案确认(**确认门**,默认开、可关)→ `implement` 实现 → `review` 审查 → `test` 测试 → `deliver` 交付。(id 与中文名的唯一真源是 `lib/vocab.js`。)
|
|
319
320
|
|
|
320
|
-
**主要工件**:`SPEC.md`(规格与边界)· `RESEARCH.md`(调研)· `PLAN.md`(方案)· `TASKS.json`(任务台账)· `ROSTER.json`(编制)· `STATE.json`(状态)· `AUTHORITY.md`(写入权限单源)· `REVIEW.md`(评审)· `TEST.md`(测试)· `SUMMARY.md`(交付总结)· `METRICS.md
|
|
321
|
+
**主要工件**:`SPEC.md`(规格与边界)· `RESEARCH.md`(调研)· `PLAN.md`(方案)· `TASKS.json`(任务台账)· `ROSTER.json`(编制)· `STATE.json`(状态)· `AUTHORITY.md`(写入权限单源)· `REVIEW.md`(评审)· `TEST.md`(测试)· `SUMMARY.md`(交付总结)· `METRICS.md`(成本与耗时:**`team/` 根的聚合快照**,`/team learn` 产出)· `RUN.log.md`(运行日志)
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
**谁写**:每个工件由**产出它的角色**自己 `write` 进 `<run-dir>/`;lead **只读工件做门控与裁决**(它没有 `write` 工具)。`STATE.json` 与 `RUN.log.md` 由**运行时**维护。
|
|
321
325
|
|
|
322
326
|
## 插件结构
|
|
323
327
|
|
package/client.js
CHANGED
|
@@ -1245,6 +1245,21 @@ window.__ModuleLoader__.load({
|
|
|
1245
1245
|
h('div', { style: { display: 'flex', flexWrap: 'wrap', gap: 6 } }, options))
|
|
1246
1246
|
}
|
|
1247
1247
|
|
|
1248
|
+
// ── 标签 → 重分节映射(性能收尾批次:把"后台一直在拉三块"变成"只拉当前可见的那一块")──
|
|
1249
|
+
// 依据是各标签**实际渲染所用的字段**(读代码得出,不是猜):
|
|
1250
|
+
// `team`(人):agents/members/roles/wfRuns(people)+ 每个成员的实时操作流(feed)
|
|
1251
|
+
// `tasks`(事):依赖图的人员标签来自 people;任务本身在 summary 里(便宜);
|
|
1252
|
+
// 任务详情要 files/logTail(artifacts)⇒ 选中时**一次性**补拉(见下)
|
|
1253
|
+
// `info`(料):违规/覆盖率在 summary 里;实时日志(logTail)需要 artifacts
|
|
1254
|
+
// `board`(盘):只渲染 runs[](summary 里已有)⇒ **不需要任何重分节**
|
|
1255
|
+
// 这条映射是单一真源:订阅与"选中任务补拉"都读它,护栏测试也钉它。
|
|
1256
|
+
function heavySectionsForTab(tb) {
|
|
1257
|
+
if (tb === 'info') return 'artifacts'
|
|
1258
|
+
if (tb === 'tasks') return 'people,feed' // feed 是内存切片(近乎免费),与徽章同 URL ⇒ 合并成一条
|
|
1259
|
+
if (tb === 'board') return ''
|
|
1260
|
+
return 'people,feed' // team(默认)与人相关视图
|
|
1261
|
+
}
|
|
1262
|
+
|
|
1248
1263
|
function Panel(props) {
|
|
1249
1264
|
var sidProp = (props && props.sessionId) || ''
|
|
1250
1265
|
var viewMode = (props && props.mode) === 'view'
|
|
@@ -1343,10 +1358,13 @@ window.__ModuleLoader__.load({
|
|
|
1343
1358
|
var planMsgS = useState(''); var planMsg = planMsgS[0], setPlanMsg = planMsgS[1]
|
|
1344
1359
|
var err = useState(''); var errV = err[0], setErr = err[1]
|
|
1345
1360
|
|
|
1346
|
-
function stateUrl() {
|
|
1361
|
+
function stateUrl(section) {
|
|
1347
1362
|
var url = '/plugins/dsh-expert-team/state', q = []
|
|
1348
1363
|
if (sessionId) q.push('sessionId=' + encodeURIComponent(sessionId))
|
|
1349
1364
|
if (selRunV && selRunV.workspace && selRunV.runId) { q.push('workspace=' + encodeURIComponent(selRunV.workspace)); q.push('run=' + encodeURIComponent(selRunV.runId)) }
|
|
1365
|
+
// `section`:渐进式状态(2026-09-15 性能修复 #3)。首屏只发 `summary`(便宜),
|
|
1366
|
+
// 人/料/事件流晚一拍、低频拉(见下面的双订阅)。不传 = 完整负载(服务端向后兼容)。
|
|
1367
|
+
if (section) q.push('section=' + encodeURIComponent(section))
|
|
1350
1368
|
return url + (q.length ? ('?' + q.join('&')) : '')
|
|
1351
1369
|
}
|
|
1352
1370
|
// ── 数据到达后的副作用(**本组件不再自带轮询器**;由 stateHub 统一驱动)─────────
|
|
@@ -1354,9 +1372,23 @@ window.__ModuleLoader__.load({
|
|
|
1354
1372
|
// 画布另走 liveStore 的 setInterval ⇒ 画布一开就有 2–3 个轮询并发压同一个重端点
|
|
1355
1373
|
// (实测 10 秒 7 发、6 次重叠、单发被拖到 5.9/8.3 s)。现在全部经 stateHub:
|
|
1356
1374
|
// 同 URL 同刻只跑一次、全局单一时钟、统一退避(见 stateHub 的注释)。
|
|
1375
|
+
// ── 分节负载的合并(2026-09-15 性能修复 #3)────────────────────────────────
|
|
1376
|
+
// 为什么必须合并而不是替换:首屏只发 `?section=summary`(便宜:阶段/进度/计数),
|
|
1377
|
+
// "人/料/事件流"晚一拍才到。若直接 `setData(d)`,摘要那一拍会把上一拍已知的成员/事件流
|
|
1378
|
+
// **抹掉**(UI 闪空、通知误判"成员消失")。规则:**新负载里有的键覆盖旧值,没有的键保留**
|
|
1379
|
+
// —— 缺块 ≠ 空数据;`sections` 字段如实告诉 UI"这份负载包含哪些块"(两种零可区分)。
|
|
1380
|
+
function mergeStatePayload(prev, d) {
|
|
1381
|
+
if (!d || typeof d !== 'object') return d
|
|
1382
|
+
if (!prev || typeof prev !== 'object' || prev.ok !== true) return d
|
|
1383
|
+
var out = {}, k
|
|
1384
|
+
for (k in prev) { if (Object.prototype.hasOwnProperty.call(prev, k)) out[k] = prev[k] }
|
|
1385
|
+
for (k in d) { if (Object.prototype.hasOwnProperty.call(d, k)) out[k] = d[k] }
|
|
1386
|
+
return out
|
|
1387
|
+
}
|
|
1357
1388
|
function onState(d) {
|
|
1358
1389
|
if (d && d.ok) {
|
|
1359
|
-
setData(d); setErr(''); reschedule(d)
|
|
1390
|
+
setData(function (prev) { return mergeStatePayload(prev, d) }); setErr(''); reschedule(d)
|
|
1391
|
+
try { publishToLive(d) } catch (e) {}
|
|
1360
1392
|
// N2: keep the composer takeover in sync with pendingDecision
|
|
1361
1393
|
// 批 0-2:把 /state 已经返回的 runId/workspace 一并传出,供横幅拍板时回传给 /decide
|
|
1362
1394
|
try { if (exports._syncPending) exports._syncPending(d.pendingDecision, sessionId, d.runId, d.workspace) } catch (e) {}
|
|
@@ -1391,14 +1423,35 @@ window.__ModuleLoader__.load({
|
|
|
1391
1423
|
}
|
|
1392
1424
|
useEffect(function () {
|
|
1393
1425
|
if (!isOpen && !viewMode) return undefined // panel closed & not canvas → no polling
|
|
1394
|
-
|
|
1395
|
-
//
|
|
1396
|
-
|
|
1397
|
-
|
|
1426
|
+
setLivePublisher(true) // 面板/画布开着 ⇒ 接管徽章的数据(见 publishToLive 的注释)
|
|
1427
|
+
// **双订阅**(性能修复 #3):`summary` 便宜(阶段/进度/计数)⇒ 按 `pollMs` 快拉,
|
|
1428
|
+
// 首屏立刻有内容;`people,feed,artifacts` 贵(成员解析/事件流/日志尾+git)⇒ 至少 6 秒一次,
|
|
1429
|
+
// 到达后由 `mergeStatePayload` 合并进同一份 data(**不会**把摘要拍掉的字段抹掉)。
|
|
1430
|
+
// stateHub 按 URL 各自计时(per-URL due),所以"一快一慢"不会互相拖。
|
|
1431
|
+
var off = stateHubSubscribe(stateUrl('summary'), dispCfg.pollMs, onState)
|
|
1432
|
+
// 重分节**按当前可见子标签**订阅(不再无条件拉 people,feed,artifacts):切标签时旧的 URL
|
|
1433
|
+
// 随卸载退订、新的立即拉一次 ⇒ 后台只保留"你正在看的那一块"。
|
|
1434
|
+
var heavy = heavySectionsForTab(tab)
|
|
1435
|
+
var offDetail = heavy ? stateHubSubscribe(stateUrl(heavy), Math.max(dispCfg.pollMs, 6000), onState) : function () {}
|
|
1436
|
+
// catch up instantly when the user returns to the tab / window(只补便宜那一份)
|
|
1437
|
+
var onVis = function () { if (document.visibilityState === 'visible') stateHubFetch(stateUrl('summary'), onState) }
|
|
1438
|
+
var onFocus = function () { stateHubFetch(stateUrl('summary'), onState) }
|
|
1398
1439
|
document.addEventListener('visibilitychange', onVis)
|
|
1399
1440
|
window.addEventListener('focus', onFocus)
|
|
1400
|
-
return function () {
|
|
1401
|
-
|
|
1441
|
+
return function () {
|
|
1442
|
+
off(); offDetail()
|
|
1443
|
+
setLivePublisher(false)
|
|
1444
|
+
document.removeEventListener('visibilitychange', onVis); window.removeEventListener('focus', onFocus)
|
|
1445
|
+
}
|
|
1446
|
+
}, [sessionId, selRunV, isOpen, viewMode, dispCfg.pollMs, tab])
|
|
1447
|
+
|
|
1448
|
+
// 任务详情(files / logTail)属于 artifacts 分节:**只在真的点开任务时补拉一次**,
|
|
1449
|
+
// 不在后台常驻(否则等于把 artifacts 又变回常驻重分节)。切换选中任务会重新拉一次。
|
|
1450
|
+
useEffect(function () {
|
|
1451
|
+
if (!selTaskV) return
|
|
1452
|
+
if (tab !== 'tasks') return // 任务详情(files/logTail)只在「事」标签渲染
|
|
1453
|
+
try { stateHubFetch(stateUrl('artifacts'), onState) } catch (e) {}
|
|
1454
|
+
}, [selTaskV && selTaskV.id])
|
|
1402
1455
|
|
|
1403
1456
|
// Make the docked panel reserve space (shift the shell frame) instead of
|
|
1404
1457
|
// covering the workspace; clears when closed or floating (md-preview trick).
|
|
@@ -2350,23 +2403,32 @@ window.__ModuleLoader__.load({
|
|
|
2350
2403
|
// ③ 间隔 = clamp(max(基础间隔, 上次耗时×2), 基础间隔, 30000);
|
|
2351
2404
|
// 团队有人在跑时基础间隔按 40% 加速(面板原有意图,现在对徽章/画布同样生效)。
|
|
2352
2405
|
// 纪律不变:**未回绝不发下一个**(晚一点看到状态,好过把 web 拖死)。
|
|
2353
|
-
var stateHub = { inflight: {}, lastMs: {}, subs: [], timer: 0, busy: false }
|
|
2406
|
+
var stateHub = { inflight: {}, lastMs: {}, lastAt: {}, subs: [], timer: 0, busy: false }
|
|
2354
2407
|
function stateHubNow() { return (window.performance && performance.now) ? performance.now() : Date.now() }
|
|
2355
2408
|
function stateHubUrls() { var out = []; stateHub.subs.forEach(function (x) { if (out.indexOf(x.url) < 0) out.push(x.url) }) ; return out }
|
|
2356
|
-
|
|
2409
|
+
// 每个 URL 的**自己的**基础间隔(同一 URL 有多个订阅者时取最小 ⇒ 谁最急听谁的)。
|
|
2410
|
+
// 性能修复 #3 的"一快一慢"就靠它:`summary` 3 s 一发、`people,feed,artifacts` ≥ 6 s 一发,
|
|
2411
|
+
// 两者互不拖拽(旧实现是"全局取最小 base、每个 tick 把所有 URL 都拉一遍" ⇒ 慢的那份被快钟拖着跑)。
|
|
2412
|
+
function stateHubBaseOf(u) {
|
|
2357
2413
|
var base = 0
|
|
2358
|
-
stateHub.subs.forEach(function (x) { if (!base || x.base < base) base = x.base })
|
|
2414
|
+
stateHub.subs.forEach(function (x) { if (x.url === u && (!base || x.base < base)) base = x.base })
|
|
2359
2415
|
if (!base) base = 3000
|
|
2360
2416
|
return stateHub.busy ? Math.max(300, Math.round(base * 0.4)) : base
|
|
2361
2417
|
}
|
|
2418
|
+
function stateHubDueAt(u) { return (stateHub.lastAt[u] || 0) + stateHubBaseOf(u) }
|
|
2362
2419
|
function stateHubSchedule() {
|
|
2363
2420
|
clearInterval(stateHub.timer)
|
|
2364
2421
|
stateHub.timer = 0
|
|
2365
2422
|
if (!stateHub.subs.length) return
|
|
2366
2423
|
var slowest = 0
|
|
2367
2424
|
stateHubUrls().forEach(function (u) { if ((stateHub.lastMs[u] || 0) > slowest) slowest = stateHub.lastMs[u] })
|
|
2368
|
-
var
|
|
2369
|
-
var
|
|
2425
|
+
var now = stateHubNow(), earliest = Infinity
|
|
2426
|
+
stateHubUrls().forEach(function (u) { var at = stateHubDueAt(u); if (at < earliest) earliest = at })
|
|
2427
|
+
if (!isFinite(earliest)) earliest = now + 3000
|
|
2428
|
+
// 基础等待 = 最早到期的那一刻;再按"最慢一次请求 ×2"放大(未回绝不发下一个的纪律仍由
|
|
2429
|
+
// in-flight 守卫保证,这里的放大只是为了别把慢端点压成队列)。
|
|
2430
|
+
var wait = Math.max(200, Math.round(earliest - now))
|
|
2431
|
+
var next = Math.max(wait, Math.min(30000, Math.round(slowest * 2)))
|
|
2370
2432
|
stateHub.timer = setInterval(stateHubTick, next)
|
|
2371
2433
|
}
|
|
2372
2434
|
function stateHubDeliver(url) {
|
|
@@ -2380,6 +2442,8 @@ window.__ModuleLoader__.load({
|
|
|
2380
2442
|
var p = stateHub.inflight[url]
|
|
2381
2443
|
if (p) { p.then(function (d) { try { onData(d) } catch (e) {} }); return } // 同刻同 URL:复用在飞结果
|
|
2382
2444
|
var t0 = stateHubNow()
|
|
2445
|
+
// 记"上次发出时刻":即使**失败**也记(否则失败会变成热循环重试)。
|
|
2446
|
+
stateHub.lastAt[url] = t0
|
|
2383
2447
|
p = fetch(url).then(function (r) { return r.ok ? r.json() : null }).then(function (d) {
|
|
2384
2448
|
delete stateHub.inflight[url]
|
|
2385
2449
|
stateHub.lastMs[url] = Math.max(0, stateHubNow() - t0)
|
|
@@ -2390,7 +2454,9 @@ window.__ModuleLoader__.load({
|
|
|
2390
2454
|
}
|
|
2391
2455
|
function stateHubTick() {
|
|
2392
2456
|
if (!stateHub.subs.length) { clearInterval(stateHub.timer); stateHub.timer = 0; return }
|
|
2393
|
-
|
|
2457
|
+
var now = stateHubNow()
|
|
2458
|
+
// **只发改到期的那些 URL**(不是每个 tick 把所有 URL 拉一遍)
|
|
2459
|
+
stateHubUrls().forEach(function (u) { if (stateHubDueAt(u) <= now && !stateHub.inflight[u]) stateHubFetch(u, stateHubDeliver(u)) })
|
|
2394
2460
|
stateHubSchedule()
|
|
2395
2461
|
}
|
|
2396
2462
|
function stateHubSubscribe(url, base, onData) {
|
|
@@ -2408,8 +2474,12 @@ window.__ModuleLoader__.load({
|
|
|
2408
2474
|
|
|
2409
2475
|
// ── 共享 state 轮询(HeaderButton / LiveCapsule 复用,单例;无订阅者即停)──
|
|
2410
2476
|
var liveStore = { data: null, sid: '', off: null, subs: new Set() }
|
|
2411
|
-
|
|
2412
|
-
|
|
2477
|
+
// 徽章/胶囊的基础节奏:它订阅的是 `people,feed`(重分节)。原先 2.5 s 等于**让重活常驻**
|
|
2478
|
+
// (即使没开面板也在拉),实测那正是"重请求在飞时 summary 被拖到 772 ms"的来源之一。
|
|
2479
|
+
// 徽章是环境状态(有没有人在跑/有没有待决策),10 s 的延迟可以接受;
|
|
2480
|
+
// 需要立刻刷新时另有 `liveTick()`(子代理出现时主动催一次),不靠高频轮询。
|
|
2481
|
+
var LIVE_BASE_MS = 10000
|
|
2482
|
+
function liveUrl(sid) { return '/plugins/dsh-expert-team/state?sessionId=' + encodeURIComponent(sid) + '§ion=people,feed' }
|
|
2413
2483
|
// 手动催一次(子代理刚出现时立刻刷新一次徽章,不必等下一个 tick)
|
|
2414
2484
|
function liveTick() {
|
|
2415
2485
|
var sid = liveStore.sid
|
|
@@ -2427,20 +2497,45 @@ window.__ModuleLoader__.load({
|
|
|
2427
2497
|
var m = (d && d.members) || {}
|
|
2428
2498
|
return Object.keys(m).some(function (k) { var v = m[k]; return v && typeof v === 'object' && (v.activity === 'running' || v.shortStatus === 'running') })
|
|
2429
2499
|
}
|
|
2500
|
+
// ── 面板/画布 → 徽章的**发布**路径(性能收尾批次)────────────────────────────
|
|
2501
|
+
// 为什么需要:徽章订阅的是 `people,feed`(重分节)。若它一直自己轮询,而当前标签是「料」
|
|
2502
|
+
// (重分节 = `artifacts`),同一时刻就有**两条重活并发** —— 真机上那正是单发从 2.4 s 变
|
|
2503
|
+
// 4.8 s 的原因(同一事件循环上串行,各自看着都慢一倍),连带把 9 ms 的 summary 拖到 772 ms。
|
|
2504
|
+
// 现在:面板/画布开着时由它们**发布**同一份 payload(零额外请求);都没开时徽章才自己轮询。
|
|
2505
|
+
var livePublishers = 0
|
|
2506
|
+
function publishToLive(d) {
|
|
2507
|
+
if (!d || typeof d !== 'object') return
|
|
2508
|
+
// 合并而不是替换:面板按标签只发一部分块(缺块 ≠ 空数据),合并后徽章不会因为
|
|
2509
|
+
// 收到一份只有 summary 的负载就把已知的成员/活动抹掉。
|
|
2510
|
+
liveStore.data = Object.assign({}, liveStore.data || {}, d)
|
|
2511
|
+
try { harvestActivity(liveStore.data) } catch (e) {}
|
|
2512
|
+
try { stateHub.busy = teamBusy(liveStore.data) } catch (e) {}
|
|
2513
|
+
liveStore.subs.forEach(function (f) { try { f(liveStore.data) } catch (e) {} })
|
|
2514
|
+
}
|
|
2515
|
+
function setLivePublisher(on) {
|
|
2516
|
+
livePublishers += on ? 1 : -1
|
|
2517
|
+
if (livePublishers < 0) livePublishers = 0
|
|
2518
|
+
notify()
|
|
2519
|
+
}
|
|
2520
|
+
function useLivePublishers() { return useExternal(function () { return livePublishers }) }
|
|
2521
|
+
|
|
2430
2522
|
function useLiveState(sid) {
|
|
2431
2523
|
var st = useState(liveStore.data); var data = st[0], setData = st[1]
|
|
2524
|
+
var pubs = useLivePublishers()
|
|
2432
2525
|
useEffect(function () {
|
|
2433
2526
|
if (!sid) return undefined
|
|
2434
2527
|
liveStore.sid = sid
|
|
2435
2528
|
var fn = function (d) { setData(d) }
|
|
2436
2529
|
liveStore.subs.add(fn)
|
|
2530
|
+
// 有人发布(面板/画布开着)⇒ 徽章**不再另开一条重分节**,靠发布的数据保持新鲜。
|
|
2531
|
+
if (pubs > 0) return function () { liveStore.subs.delete(fn) }
|
|
2437
2532
|
// 经 hub 订阅:与面板/画布共用同一个时钟与在飞守卫(这里**不再**自己 setInterval)
|
|
2438
2533
|
var off = stateHubSubscribe(liveUrl(sid), LIVE_BASE_MS, liveDeliver)
|
|
2439
2534
|
return function () {
|
|
2440
2535
|
liveStore.subs.delete(fn)
|
|
2441
2536
|
off()
|
|
2442
2537
|
}
|
|
2443
|
-
}, [sid])
|
|
2538
|
+
}, [sid, pubs])
|
|
2444
2539
|
return data
|
|
2445
2540
|
}
|
|
2446
2541
|
// 头部徽章文案:让「并排面板」入口自带团队实时状态(与画布标签职责区分)
|