dsh-plugin-teamflow 0.1.5 → 0.1.7

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 CHANGED
@@ -2,6 +2,49 @@
2
2
 
3
3
  > 本插件首次公开发布版本为 **v0.1.0**;发布前的内部迭代(v0.3~v0.13)记录于 `AGENTS.md` §5,对外统一归到 v0.1.0。
4
4
 
5
+ ## [0.1.7] - 2026-09-11
6
+
7
+ ### 新增
8
+ - **工作台产物一键预览**:任务卡详情里的「任务夹」现在按真实存在的产物列按钮(PRD / DESIGN / TECHNICAL / QA-REPORT / ACCEPTANCE / meta),点一下即在 DSH **右侧栏**打开预览(Markdown 由官方文档预览器接管)。地址由 host 用官方 `fileAddressFor` 生成(`dsh-resource://file/session/<id>/<相对路径>`)——客户端不拼地址、也不引宿主包进 client bundle;只列真实存在的文件(不出死按钮);右侧栏服务缺失时静默降级
9
+ - **产物交付(`present`)**:prd/tech/qa/acceptance 四个阶段被要求把任务夹产物交给官方 `present` 工具 → 用户在该会话得到「交付文件卡」(预览 / 默认程序打开 / 文件管理器定位)。诚实标注为 `[policy]` 增强项:文件仍是唯一事实源,缺文件依旧是硬失败;卡片渲染在**该子代理会话**的轮次尾部(主会话不显示)
10
+ - **机械阶段推理强度降档(省 token)**:DeepSeek 路由默认 `reasoningEffort: high`,而推理 token **计入 output** 且**推理内容每个带推理回合原样回传**(同时抬高后续 input)。现对两处机械阶段下发 `low`——patch 档的「单点确认」与 `scaffold`(脚手架落地);判据类阶段(PRD/设计/技术方案/QA/验收)保持宿主默认 `high`,**重试自动回升 `high`**(质量优先)。安全前提:先经 `llm.resolveModelInfo()` 探测该路由的 `reasoning.efforts`,只有声明支持才下发——宿主对不支持的值会 `UNSUPPORTED_REASONING_EFFORT` 硬失败且不降级;探测结果按 provider/model 缓存。阶段日志记录实际下发的档位
11
+
12
+ ### 修复
13
+ - **token 计量改走官方 Session 投影(宿主弃用同步事件读取器)**:dsh 0.1.5-rc.2 起 `Session.eventAt()` / `snapshotEvents()` / `ownEvents()` 标记为 deprecated(存量可留、新调用禁止,宿主方向是不再把完整事件序列常驻内存)。计量来源改为**官方投影优先**——`ctx.sessionProjections.stateOf(session,'tokenUsage')` 取四桶(与宿主 token-meter 同一份 fold,重试替换语义更准)+ `'sessionStats'.steps` 取调用数,**零历史扫描**;投影缺失/无数据/读取异常时静默回退原事件扫描(最小 profile 与存量宿主不受影响,不虚报 0,不中断流水线)。`sessionProjections` 走可选 `ctx.inject`,不进 `static inject`——服务缺失时插件照常加载
14
+ - **护栏适配官方通道(提醒 + 挂死检测)**:轻提醒从手写 `session.append('user/message')` + step/end flush 时序状态机,改为官方 `run.localAgent.inject()`(宿主在协议安全边界整批认领,旧注释声称的「会插进 tool_calls→tool_result 触发 400」不成立);挂死检测从「多源取最长事件视图」长度启发式改为官方 **`subagentTiming` 投影**的 `active.through`(已提交事件时间,不受视图失明影响——上次 QA 误判 stalled 的根因)。长工具静默执行仍由 agent 活动守卫豁免;投影不可用时回退旧启发式。中止语义未变
15
+
16
+ ### 改进
17
+ - **声明宿主兼容窗口**:`package.json` 新增 `engines.dsh: ">=0.1.5-rc.2 <0.2.0"` 与 `dsh.manifestVersion: 1`(dsh 0.1.5 起支持的公共 manifest 字段;当前宿主不校验,属作者声明);README「版本锚定」段同步到 v0.1.5-rc.2,并记录本次兼容核对结论与两个待跟进项
18
+ - **清理死注入**:`static inject` 长期硬注入 `tokenMeter` 却全仓从未使用 → 从 static inject / setRuntime / runtime 三处移除(假依赖会拖累插件的加载条件)
19
+ - **工作区 key 文档纠偏**:`workspaceScopeOf` 的「优先用 DSH workspace UUID」分支**当前不可达**(宿主 `resolveByPath` 是异步、我们同步调用),实际生效的是路径派生 `slugPath(cwd)`;注释与 AGENTS.md 改为事实描述,真修(改异步 + 存储 key 迁移)列入 `docs/TODO.md` 待 v0.1.8
20
+
21
+ ### 已知待办
22
+ - **护栏复读检测仍读已弃用的事件读取器**(提醒通道与挂死检测已改官方;复读需要流式文本内容):官方替代是订阅 `'session/event'` post-commit 投递,需先定等价判据,见 `docs/TODO.md`(当前行为不变,存量调用被宿主明确允许)
23
+
24
+ ## [0.1.6] - 2026-09-07
25
+
26
+ ### 新增
27
+ - **评测层 L1+L2(prompt/注入改动收益评测)**:每次改 prompt/注入后可直接验证收益——L1 行为级契约测试(直接调用 prompt 工厂断言产出锚点,29 条契约分 HOST-ENFORCED/policy/structural 三级,改 prompt 一眼看出断了哪条、丢哪级保障)+ L2 回放语料一致性门禁(冻结真实形状的 QA-REPORT/ACCEPTANCE/dev 回复/蓝图产物,喂宿主真实解析器做 golden corpus 回归,零 LLM 成本)。语料只增不改,防评测过拟合
28
+ - **dev/qaFix 验证证据块**:开发回复末尾强制「[Verification evidence]」块(命令+退出码+断言计数+失败行引用,或显式 N/A),host 提取存证、阶段详情可见、可与命令日志交叉核对——开发阶段从「单方宣称全绿」升级为「可审计的具体自述」
29
+ - **代码级英文化(为 UI i18n 铺路)**:代码判断/命名全英文(阶段键 prd/design/scaffold/tech/dev/qa/acceptance、任务结构化键 taskKey),中文只留展示层;存量数据提供迁移脚本
30
+
31
+ ### 改进
32
+ - **resume 断点续跑状态机化**:两级状态(大阶段 + 子图任务)聚合判定——有 done 尝试的任务即成功(历史失败不算失败),resume 只补跑「聚合后未成功」任务、复用已完成产物;不再读 backlog 子卡(残留失败卡不再污染判定);dev 部分成功时起点精确回开发补跑未完成,全 done 时正确落在 QA/验收
33
+ - **输出单轨制**:QA/验收的完整报告只写在任务夹文件(QA-REPORT.md/ACCEPTANCE.md),子代理回复仅摘要+路径——host 直接导入文件(缺陷表/核对表),杜绝「回复与文件不一致」的双轨问题
34
+ - **验收结论契约强度**:验收报告结论行必须为字面量模板(✅/⚠️/❌/📝 四档)且为文件最后一行;无结论行/空结论行 → 需人工确认(不再默认通过,防质量门禁漏报)
35
+ - **prompt 约束分级**:区分 [HOST-ENFORCED](host 真实强制,如单轨产物/结论行)与 [policy](自律 + 轻提醒),prompt 内不再自称 hard constraint
36
+ - **重试诊断包**:重试时把上次失败详情(outcome/护栏原因/拒绝词命中点/产出尾部)附进 prompt——盲试 → 带因重试;stalled(挂死/空转)不再自动重试
37
+
38
+ ### 修复
39
+ - **token 计量适配宿主 session v2**:宿主新版会话已无 `events` 属性,计量读不到 → 流水线卡与图卡无「TOKEN · 官方口径」。修复:多源回退(snapshotEvents/ownEvents)+ usage 双路径(含 stream 内嵌),新 run 计量恢复
40
+ - **护栏挂死误杀**:QA 子代理正常干活却被判「10 分钟无事件」(宿主事件视图失明)——事件读取多源回退 + agent 活动守卫(非 idle 且已动手即不中止)
41
+ - **任务卡 token 统计**:子卡 usage 按 stage 引用直写(并发下不再错位/超计)
42
+ - **存量迁移脚本 taskKey 误判**:合法中文括号结尾的任务键不再被当作历史残留(幂等无损但计数虚高)
43
+ - **验收空结论行/裸否定词漏报**:四档词白名单校验覆盖 accepted 分支
44
+
45
+ ### 其他
46
+ - AGENTS.md 注入面瘦身 64%;dsh 0.1.3-alpha.1 兼容性核对(依赖包名更新)
47
+
5
48
  ## [0.1.5] - 2026-09-01
6
49
 
7
50
  ### 新增
package/README.md CHANGED
@@ -119,6 +119,21 @@ dsh-plugin-teamflow/
119
119
  - Node.js ≥ 22.18;
120
120
  - 依赖宿主提供的 `@deepseek-ai/dsh-*` 与 `react`(peerDependencies,宿主注入,无需单独安装)。
121
121
 
122
+ ### 版本锚定(dsh 宿主兼容性)
123
+
124
+ 本插件开发与验证基于 **dsh v0.1.5-rc.2(2026-09-10,tag `dsh-v0.1.5-rc.2`)**;npm 侧 `next`=0.1.5-rc.2、`latest`=0.1.5-rc.1(`latest` 常滞后于 `next`,勿以 latest 判断发布线)。`peerDependencies` 保持 `*`(宿主注入,宽松兼容),并在 `package.json` 声明兼容窗口 **`engines.dsh: ">=0.1.5-rc.2 <0.2.0"`** 与 **`dsh.manifestVersion: 1`**——当前 dsh 不读取/校验这两个字段(源码内仅有类型声明),属作者声明性元数据。
125
+
126
+ 2026-09-10 兼容性核对(dsh 0.1.5-rc.2):插件面板 Slot(原 `conversation` 根 slot → `main` 下的 `conversation` key)、会话格式 V3 + Session 生命周期(`SessionHandle`、异步 `agentLoop.create()`、会话锁)、`ctx.agent` 移除与 Inbox 类型化、SDK/Headless/ACP 默认工具调整、subprocess handle 去除 pid——**插件全部兼容**(未使用被改动的接口;`conversation.view` / `conversation.input.right` 声明未变,slot 树无删除)。其中一项需要跟进:
127
+
128
+ - **Session 同步事件读取器已弃用**(`session.eventAt()` / `snapshotEvents()` / `ownEvents()`,宿主 2026-09-09 起「存量可留、新调用禁止」,方向是不再把完整事件序列常驻内存):**token 计量已改为官方 Session 投影优先**(`ctx.sessionProjections.stateOf(session,'tokenUsage')` 取四桶 + `'sessionStats'.steps` 取调用数),事件扫描降级为无投影宿主的回退;**护栏的提醒通道已改官方 `Agent.inject()`、挂死判据已改用官方 `subagentTiming` 投影的 `active.through`**(长工具静默仍由 agent 活动守卫豁免),只剩**复读检测**仍在读事件(需要流式文本内容,官方替代=订阅 `'session/event'` post-commit 投递,需先定等价判据,见 `docs/TODO.md`)。
129
+
130
+ 2026-09-04 核对(dsh 0.1.3-alpha.1):session 持久化 v2(write-lease/JSONL 快照/版本化导出)、attachment/file-upload 收口、Windows 子进程隐藏、workspace 全限定路径硬化——全部兼容,插件无需调整。
131
+
132
+ 插件侧契约约束(dsh 升级后若行为异常先核对本段;锚定版本变更会在此更新):
133
+ - 插件注入的 session 事件(`tool-workflow/agent-start`、`user/message` 带 `source.kind='plugin'`)均在宿主 `known-event-types` 词表内;**未来新增自定义事件类型须带 `ignorable: true`**,已知类型载荷不加词表外键;
134
+ - `@deepseek-ai/dsh-client-modules` 自 0.1.2-rc.1 起替代 `@deepseek-ai/dsh-client-runtime`(后者已从 monorepo 移除);
135
+ - 计量读的是**宿主投影 key**(`tokenUsage` / `sessionStats`)而非插件自有格式:宿主若改 key 或 state 版本,此段与 `host/core/metering.ts` 同步更新。
136
+
122
137
  ## 安装(对使用者)
123
138
 
124
139
  ```bash
package/lib/client.js CHANGED
@@ -308,15 +308,37 @@ window.__ModuleLoader__.load({
308
308
  interrupted: T.warn,
309
309
  superseded: T.text2
310
310
  };
311
+ /** 阶段英文键 → 图标/中文展示名(2026-09-06 英文化:journal.phase 为英文键,展示名统一走映射——未来 i18n 换表即换语言)。 */
311
312
  const PHASE_ICON = {
312
- "PRD 产品需求": "📋",
313
- "UI/UX 设计": "🎨",
314
- "架构规划": "🏗️",
315
- "技术方案": "📐",
316
- 开发: "💻",
317
- "QA 测试": "🧪",
318
- "产品验收": "✅"
313
+ prd: "📋",
314
+ design: "🎨",
315
+ scaffold: "🏗️",
316
+ tech: "📐",
317
+ dev: "💻",
318
+ qa: "🧪",
319
+ acceptance: "✅"
319
320
  };
321
+ const PHASE_NAME = {
322
+ prd: "PRD 产品需求",
323
+ design: "UI/UX 设计",
324
+ scaffold: "架构规划",
325
+ tech: "技术方案",
326
+ dev: "开发",
327
+ qa: "QA 测试",
328
+ acceptance: "产品验收"
329
+ };
330
+ const phaseNameOf = (p) => PHASE_NAME[p] || p || "—";
331
+ const phaseIconOf = (p) => PHASE_ICON[p] || "⚙️";
332
+ /** phase 归一:英文键直通;存量中文映射(防御性——新数据全英文)。 */
333
+ const phaseKeyOf = (p) => ({
334
+ "PRD 产品需求": "prd",
335
+ "UI/UX 设计": "design",
336
+ "架构规划": "scaffold",
337
+ "技术方案": "tech",
338
+ "开发": "dev",
339
+ "QA 测试": "qa",
340
+ "产品验收": "acceptance"
341
+ })[p] || String(p || "");
320
342
  const RUN_STATUS_TEXT = {
321
343
  pending: "等待中",
322
344
  running: "进行中",
@@ -642,7 +664,20 @@ window.__ModuleLoader__.load({
642
664
  textOverflow: "ellipsis",
643
665
  whiteSpace: "nowrap"
644
666
  }
645
- }, s.label), chip(stText(s.status), color, { dot: true }), h("span", { style: {
667
+ }, s.__taskKey || s.label), s.attempts && s.attempts.length > 1 ? h("span", {
668
+ title: `重试 ${s.attempts.length - 1} 次(共 ${s.attempts.length} 次尝试)`,
669
+ style: {
670
+ fontFamily: MONO,
671
+ fontSize: 10,
672
+ fontWeight: 800,
673
+ color: T.warn,
674
+ background: `color-mix(in srgb, ${T.warn} 14%, transparent)`,
675
+ borderRadius: 999,
676
+ padding: "0 6px",
677
+ lineHeight: "15px",
678
+ flex: "0 0 auto"
679
+ }
680
+ }, `↻${s.attempts.length - 1}`) : null, chip(stText(s.status), color, { dot: true }), h("span", { style: {
646
681
  color: T.text2,
647
682
  fontSize: 11,
648
683
  opacity: .5
@@ -724,12 +759,12 @@ window.__ModuleLoader__.load({
724
759
  fontWeight: 700,
725
760
  background: `linear-gradient(90deg, color-mix(in srgb, ${g.headColor} 13%, transparent), color-mix(in srgb, ${g.headColor} 5%, transparent))`,
726
761
  border: `1px solid color-mix(in srgb, ${g.headColor} 30%, transparent)`
727
- } }, h("span", { style: { fontSize: 13.5 } }, PHASE_ICON[g.phase] || "⚙️"), h("span", { style: {
762
+ } }, h("span", { style: { fontSize: 13.5 } }, phaseIconOf(g.phase)), h("span", { style: {
728
763
  flex: 1,
729
764
  overflow: "hidden",
730
765
  textOverflow: "ellipsis",
731
766
  whiteSpace: "nowrap"
732
- } }, g.phase), g.stages.length > 1 ? h("span", { style: {
767
+ } }, phaseNameOf(g.phase)), g.stages.length > 1 ? h("span", { style: {
733
768
  fontFamily: MONO,
734
769
  fontSize: 10,
735
770
  fontWeight: 800,
@@ -752,26 +787,32 @@ window.__ModuleLoader__.load({
752
787
  paddingTop: 10
753
788
  } }, g.stages.map((s) => FlowStageCard(s, s.seq, onOpen))));
754
789
  }
755
- /** 阶段详情抽屉(卡片点击打开;浮于画布右侧,不参与拖动/缩放)。 */
790
+ /** 阶段详情抽屉(卡片点击打开;浮于画布右侧,不参与拖动/缩放)。
791
+ * 2026-09-06 状态机化:同任务多次尝试 → 顶部尝试时间线 + 选中展开(默认最新);
792
+ * 单次尝试保持现状(不渲染时间线)。 */
756
793
  function StageDetailDrawer({ det, onClose, sessionId, sessions }) {
794
+ const [sel, setSel] = react.default.useState(null);
757
795
  const st = det.stage;
758
796
  const d = det.data;
797
+ const attempts = d && d.attempts && d.attempts.length > 1 ? d.attempts : null;
798
+ const selIdx = attempts ? sel === null ? attempts.length - 1 : sel : null;
799
+ const cur = attempts ? attempts[selIdx] || d : d;
759
800
  const color = st && st.status ? stColor(st.status) : T.text2;
760
801
  const ownerSession = d && d.ownerSession ? String(d.ownerSession) : null;
761
802
  const mySession = sessionId ? String(sessionId) : null;
762
803
  const crossSession = !!ownerSession && !!mySession && ownerSession !== mySession;
763
- const hasChild = !!(!crossSession && d && d.childId && sessions && typeof sessions.openSubagent === "function");
804
+ const hasChild = !!(!crossSession && cur && cur.childId && sessions && typeof sessions.openSubagent === "function");
764
805
  const openChild = () => {
765
806
  if (crossSession || !hasChild) return;
766
807
  try {
767
808
  sessions.openSubagent({
768
809
  parentSessionId: ownerSession || sessionId,
769
- childSessionId: d.childId,
810
+ childSessionId: cur.childId,
770
811
  mode: "one-shot"
771
812
  });
772
813
  } catch (e) {}
773
814
  };
774
- const outText = d && d.output ? d.output : d && d.summary ? `(该 run 未保存完整正文,展示摘要)\n\n${d.summary}` : det.err ? `⚠ 加载失败:${det.err}` : det.loading ? "加载中…" : "(无产物正文)";
815
+ const outText = cur && cur.output ? cur.output : cur && cur.summary ? `(该 run 未保存完整正文,展示摘要)\n\n${cur.summary}` : det.err ? `⚠ 加载失败:${det.err}` : det.loading ? "加载中…" : "(无产物正文)";
775
816
  const closeBtn = {
776
817
  font: "inherit",
777
818
  width: 26,
@@ -820,7 +861,7 @@ window.__ModuleLoader__.load({
820
861
  padding: "12px 14px",
821
862
  borderBottom: `1px solid ${T.border}`,
822
863
  background: `linear-gradient(135deg, color-mix(in srgb, ${color} 14%, transparent), transparent 62%)`
823
- } }, h("span", { style: { fontSize: 17 } }, PHASE_ICON[st && st.phase] || "⚙️"), h("div", { style: {
864
+ } }, h("span", { style: { fontSize: 17 } }, phaseIconOf(st && st.phase)), h("div", { style: {
824
865
  flex: 1,
825
866
  minWidth: 0
826
867
  } }, h("div", {
@@ -864,7 +905,66 @@ window.__ModuleLoader__.load({
864
905
  fontFamily: MONO,
865
906
  color: T.text,
866
907
  lineHeight: 1.65
867
- } }, usageDetail(d || st || {}))), h("div", { style: {
908
+ } }, usageDetail(cur || st || {}))), attempts ? h("div", { style: {
909
+ display: "flex",
910
+ flexDirection: "column",
911
+ gap: 5
912
+ } }, h("span", { style: {
913
+ fontSize: 10.5,
914
+ fontWeight: 700,
915
+ color: T.text2,
916
+ letterSpacing: .3
917
+ } }, `↻ 尝试历史(${attempts.length} 次 · 点击查看该次详情)`), h("div", { style: {
918
+ display: "flex",
919
+ flexDirection: "column",
920
+ gap: 4
921
+ } }, attempts.map((a, idx) => {
922
+ const aColor = a.status === "done" ? T.success : a.status === "failed" ? T.error : T.warn;
923
+ const sel = idx === selIdx;
924
+ return h("div", {
925
+ key: a.seq,
926
+ onClick: () => setSel(idx),
927
+ title: (a.summary || a.outcome || "").slice(0, 200),
928
+ style: {
929
+ cursor: "pointer",
930
+ borderRadius: 9,
931
+ padding: "6px 9px",
932
+ display: "flex",
933
+ gap: 7,
934
+ alignItems: "center",
935
+ border: `1px solid ${sel ? color : T.border}`,
936
+ background: sel ? `color-mix(in srgb, ${color} 10%, transparent)` : T.layer1
937
+ }
938
+ }, h("span", { style: {
939
+ fontFamily: MONO,
940
+ fontSize: 10.5,
941
+ color: T.text2,
942
+ flex: "0 0 52px"
943
+ } }, `#${a.seq}`), h("span", { style: {
944
+ fontSize: 11,
945
+ color: aColor,
946
+ flex: "0 0 64px",
947
+ fontWeight: 700
948
+ } }, a.status === "done" ? "✅ 成功" : a.status === "failed" ? `❌ ${a.outcome || "失败"}` : "⏳ 进行中"), h("span", { style: {
949
+ flex: 1,
950
+ minWidth: 0,
951
+ fontSize: 10.5,
952
+ color: T.text2,
953
+ overflow: "hidden",
954
+ textOverflow: "ellipsis",
955
+ whiteSpace: "nowrap"
956
+ } }, (a.summary || a.outcome || "(无摘要)").slice(0, 80)), h("span", { style: {
957
+ fontFamily: MONO,
958
+ fontSize: 10,
959
+ color: T.text2,
960
+ flex: "0 0 auto"
961
+ } }, a.startedAt ? fmtDur(a.startedAt, a.endedAt) : "—"), h("span", { style: {
962
+ fontFamily: MONO,
963
+ fontSize: 10,
964
+ color: T.text2,
965
+ flex: "0 0 auto"
966
+ } }, a.usage ? fmtTokens(totalTokens(a.usage)) : ""));
967
+ }))) : null, h("div", { style: {
868
968
  display: "flex",
869
969
  flexDirection: "column",
870
970
  gap: 5
@@ -898,7 +998,37 @@ window.__ModuleLoader__.load({
898
998
  color: T.text2,
899
999
  textAlign: "center",
900
1000
  lineHeight: 1.55
901
- } }, "跳转成功后,请切「对话」tab 查看该子代理的完整会话轨迹") : null), h("div", { style: {
1001
+ } }, "跳转成功后,请切「对话」tab 查看该子代理的完整会话轨迹") : null), st && phaseKeyOf(st.phase) === "dev" ? h("div", { style: {
1002
+ display: "flex",
1003
+ flexDirection: "column",
1004
+ gap: 5
1005
+ } }, h("span", { style: {
1006
+ fontSize: 10.5,
1007
+ fontWeight: 700,
1008
+ color: T.text2,
1009
+ letterSpacing: .3
1010
+ } }, "🔬 验证证据"), cur && cur.verifyEvidence ? h("div", { style: {
1011
+ whiteSpace: "pre-wrap",
1012
+ wordBreak: "break-word",
1013
+ fontSize: 11.5,
1014
+ lineHeight: 1.62,
1015
+ color: T.text,
1016
+ background: `color-mix(in srgb, ${T.layer2} 55%, transparent)`,
1017
+ border: `1px solid ${T.border}`,
1018
+ borderRadius: 10,
1019
+ padding: "10px 12px",
1020
+ maxHeight: 180,
1021
+ overflowY: "auto",
1022
+ fontFamily: MONO
1023
+ } }, cur.verifyEvidence) : h("div", { style: {
1024
+ fontSize: 11,
1025
+ color: T.warn,
1026
+ background: `color-mix(in srgb, ${T.warn} 8%, transparent)`,
1027
+ border: `1px dashed color-mix(in srgb, ${T.warn} 45%, transparent)`,
1028
+ borderRadius: 10,
1029
+ padding: "8px 12px",
1030
+ lineHeight: 1.55
1031
+ } }, "(缺失——契约未兑现,host 已记警告;可与 logs/teamflow/<runId>/ 命令输出日志对照)")) : null, h("div", { style: {
902
1032
  display: "flex",
903
1033
  flexDirection: "column",
904
1034
  gap: 5
@@ -932,6 +1062,7 @@ window.__ModuleLoader__.load({
932
1062
  marginBottom: 8
933
1063
  } }, "🏭"), "暂无运行中的流水线——让模型调用 teamflow_start,或在上方输入需求");
934
1064
  const groups = [];
1065
+ const taskKeyOf = (s) => String(s.taskKey || String(s.label || "").replace(/^开发 · /, "").replace(/((?:第 \d+ 次重试|补跑))$/, "").trim());
935
1066
  for (const st of active.stages || []) {
936
1067
  let g = groups.length ? groups[groups.length - 1] : null;
937
1068
  if (!g || g.phase !== st.phase) {
@@ -941,7 +1072,31 @@ window.__ModuleLoader__.load({
941
1072
  };
942
1073
  groups.push(g);
943
1074
  }
944
- g.stages.push(st);
1075
+ const key = taskKeyOf(st);
1076
+ const prev = g.stages.find((x) => x.__taskKey === key);
1077
+ if (prev) {
1078
+ prev.attempts = prev.attempts || [prev];
1079
+ prev.attempts.push(st);
1080
+ if ((st.seq || 0) > (prev.seq || 0)) {
1081
+ for (const f of [
1082
+ "status",
1083
+ "outcome",
1084
+ "usage",
1085
+ "output",
1086
+ "summary",
1087
+ "childId",
1088
+ "startedAt",
1089
+ "endedAt",
1090
+ "verifyEvidence"
1091
+ ]) prev[f] = st[f];
1092
+ prev.seq = st.seq;
1093
+ prev.label = st.label;
1094
+ }
1095
+ } else g.stages.push({
1096
+ ...st,
1097
+ __taskKey: key,
1098
+ attempts: [st]
1099
+ });
945
1100
  }
946
1101
  const wrapRef = react.default.useRef(null);
947
1102
  const [vw, setVw] = react.default.useState(900);
@@ -1237,7 +1392,7 @@ window.__ModuleLoader__.load({
1237
1392
  sessions
1238
1393
  }) : null);
1239
1394
  }
1240
- function BoardPanel({ backlog, api, onRefresh, sessionId, onShowRun }) {
1395
+ function BoardPanel({ backlog, api, onRefresh, sessionId, onShowRun, openArtifact }) {
1241
1396
  const [drag, setDrag] = react.default.useState(null);
1242
1397
  const [over, setOver] = react.default.useState(null);
1243
1398
  const [det, setDet] = react.default.useState(null);
@@ -1527,7 +1682,8 @@ window.__ModuleLoader__.load({
1527
1682
  }), det ? h(ItemDetailDrawer, {
1528
1683
  det,
1529
1684
  onClose: () => setDet(null),
1530
- onShowRun
1685
+ onShowRun,
1686
+ openArtifact
1531
1687
  }) : null);
1532
1688
  }
1533
1689
  function fmtAt(ts) {
@@ -1536,7 +1692,7 @@ window.__ModuleLoader__.load({
1536
1692
  minute: "2-digit"
1537
1693
  }) : "—";
1538
1694
  }
1539
- function ItemDetailDrawer({ det, onClose, onShowRun }) {
1695
+ function ItemDetailDrawer({ det, onClose, onShowRun, openArtifact }) {
1540
1696
  const d = det && det.data;
1541
1697
  const loading = det && det.loading;
1542
1698
  const err = det && det.err;
@@ -1795,7 +1951,30 @@ window.__ModuleLoader__.load({
1795
1951
  borderRadius: 8,
1796
1952
  padding: "7px 10px",
1797
1953
  wordBreak: "break-all"
1798
- } }, d.runDocs + "/")) : null,
1954
+ } }, d.runDocs + "/"), Array.isArray(d.artifacts) && d.artifacts.length > 0 ? h("div", { style: {
1955
+ display: "flex",
1956
+ flexWrap: "wrap",
1957
+ gap: 6,
1958
+ marginTop: 1
1959
+ } }, d.artifacts.map((a) => h("button", {
1960
+ key: a.name,
1961
+ onClick: () => {
1962
+ if (openArtifact) openArtifact(a.address, a.name);
1963
+ },
1964
+ title: `在右侧栏预览 ${d.runDocs}/${a.name}`,
1965
+ style: {
1966
+ fontSize: 10.5,
1967
+ fontWeight: 600,
1968
+ padding: "3px 9px",
1969
+ borderRadius: 6,
1970
+ cursor: "pointer",
1971
+ lineHeight: "16px",
1972
+ border: `1px solid color-mix(in srgb, ${T.brand} 38%, transparent)`,
1973
+ background: `color-mix(in srgb, ${T.brand} 10%, transparent)`,
1974
+ color: T.brand,
1975
+ whiteSpace: "nowrap"
1976
+ }
1977
+ }, `📄 ${String(a.name).replace(/\.md$/, "")}`))) : null) : null,
1799
1978
  kind === "task" && d.usage ? h("div", { style: {
1800
1979
  display: "flex",
1801
1980
  flexDirection: "column",
@@ -2363,6 +2542,7 @@ window.__ModuleLoader__.load({
2363
2542
  api,
2364
2543
  onRefresh: refresh,
2365
2544
  sessionId: props.sessionId,
2545
+ openArtifact: props.openArtifact,
2366
2546
  onShowRun: (rid) => {
2367
2547
  setTab("pipeline");
2368
2548
  setRunId(rid);
@@ -2381,6 +2561,18 @@ window.__ModuleLoader__.load({
2381
2561
  async function apply(ctx) {
2382
2562
  await ctx.remote.$mount(TEAMFLOW_REMOTE_CONTRIBUTION);
2383
2563
  const teamflow = ctx.get("remote.teamflow");
2564
+ const openArtifact = (address, name) => {
2565
+ try {
2566
+ const sidebarRight = ctx.get("sidebarRight");
2567
+ if (!sidebarRight || typeof sidebarRight.openResource !== "function") {
2568
+ console.warn("[teamflow] 右侧栏服务不可用,无法预览产物", name);
2569
+ return;
2570
+ }
2571
+ sidebarRight.openResource(address);
2572
+ } catch (e) {
2573
+ console.warn("[teamflow] 打开产物失败", name, e && e.message);
2574
+ }
2575
+ };
2384
2576
  ctx.slots.inject("conversation.view", () => ctx.slots.register({
2385
2577
  name: "conversation.view",
2386
2578
  id: "teamflow",
@@ -2389,7 +2581,8 @@ window.__ModuleLoader__.load({
2389
2581
  inject: (sessionId) => ({
2390
2582
  sessionId,
2391
2583
  remote: teamflow,
2392
- sessions: ctx.get("sessions")
2584
+ sessions: ctx.get("sessions"),
2585
+ openArtifact
2393
2586
  })
2394
2587
  }, TeamFlowView));
2395
2588
  ctx.slots.inject("conversation.input.right", () => ctx.slots.register({