@xdxer/dingtalk-agent 0.1.5-beta.8 → 0.1.5-beta.9

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.en.md +1 -0
  3. package/README.md +1 -0
  4. package/dist/bin/dingtalk-agent.js +115 -14
  5. package/dist/bin/dingtalk-agent.js.map +1 -1
  6. package/dist/src/agent-audit.js +108 -9
  7. package/dist/src/agent-audit.js.map +1 -1
  8. package/dist/src/development-workspace.js +37 -1
  9. package/dist/src/development-workspace.js.map +1 -1
  10. package/dist/src/dws.js +9 -0
  11. package/dist/src/dws.js.map +1 -1
  12. package/dist/src/multica-deploy.js +347 -105
  13. package/dist/src/multica-deploy.js.map +1 -1
  14. package/dist/src/multica-provider.js +151 -27
  15. package/dist/src/multica-provider.js.map +1 -1
  16. package/dist/src/multica-runtime-vocabulary.js +110 -0
  17. package/dist/src/multica-runtime-vocabulary.js.map +1 -0
  18. package/dist/src/promotion.js +2 -1
  19. package/dist/src/promotion.js.map +1 -1
  20. package/dist/src/schedule-plan.js +192 -24
  21. package/dist/src/schedule-plan.js.map +1 -1
  22. package/docs/AGENT-IN-PRODUCTION.md +255 -0
  23. package/docs/ARCHITECTURE.md +5 -1
  24. package/docs/PLATFORM-GUARDRAILS.md +188 -0
  25. package/docs/schemas/multica-deployment-plan.schema.json +3 -1
  26. package/docs/schemas/multica-deployment-receipt.schema.json +15 -1
  27. package/docs/schemas/multica-deployment-status.schema.json +6 -2
  28. package/docs/schemas/multica-workspace-inspection.schema.json +16 -0
  29. package/docs/schemas/multica-workspace-status.schema.json +2 -0
  30. package/docs/schemas/workspace-scaffold.schema.json +1 -0
  31. package/lab/project-workspace/fake-multica-provider.mjs +97 -7
  32. package/lab/project-workspace/multica-deploy.fixture.json +2 -2
  33. package/lab/project-workspace/multica-readonly.fixture.json +2 -2
  34. package/lab/project-workspace/project.fixture.json +1 -1
  35. package/lab/robot-eval/suite.json +1 -1
  36. package/package.json +5 -2
  37. package/skills/core/dta-agent-compose/SKILL.md +1 -1
  38. package/skills/core/dta-agent-compose/references/drive-and-schedules.md +22 -2
  39. package/skills/core/dta-basic-behavior/SKILL.md +1 -1
  40. package/skills/core/dta-basic-behavior/references/memory-and-evolution.md +1 -1
  41. package/skills/core/dta-people-group-memory/SKILL.md +4 -4
  42. package/skills/core/dta-people-group-memory/references/model.md +1 -1
  43. package/skills/platforms/multica-dingtalk/PLATFORM.md +2 -2
  44. package/skills/platforms/multica-dingtalk/dta-deploy-multica/SKILL.md +6 -5
  45. package/skills/platforms/multica-dingtalk/dta-deploy-multica/references/multica-deployment-contract.md +14 -4
  46. package/skills/platforms/multica-dingtalk/dta-ops-multica/SKILL.md +33 -7
  47. package/skills/platforms/multica-dingtalk/dta-ops-multica/scripts/multica_ext.py +118 -12
@@ -49,6 +49,22 @@
49
49
 
50
50
  **② 是心脏。** 判断标准:如果你写不出"完成 = 数据里某个可查的状态",那你要的不是可维护的定时任务,而是一根迟早断的接力链——退回去重新设计判据,别加定时器。
51
51
 
52
+ ### 唤醒提示词带策略,Skill 只带能力
53
+
54
+ ①里那句"唤醒只带类别、不带工作清单",还有一层同样重要的推论:**投递策略属于这条节律,不属于 Skill。**
55
+
56
+ 分界线只有一句:**换个场景还成立的是能力,换个场景就得改的是策略。**
57
+
58
+ | | 归 Skill(原子能力) | 归唤醒提示词(这条节律的策略) |
59
+ |---|---|---|
60
+ | 日报 | 怎么采数、怎么归纳、怎么出图出文、降级链 | 发到**哪个群**、几点发、只发群还是也发单聊 |
61
+ | 巡检 | 怎么判健康、异常长什么样 | 卡住了**报给谁** |
62
+ | 交付 | 怎么生成、怎么回读确认送达 | **收件人**、窗口、频次 |
63
+
64
+ Skill 里仍然写死通用红线(如"默认只发主人单聊;发群必须本次明确指定"),而那个"明确指定"由唤醒提示词提供——提示词来自版本化的 `schedules` 声明,本身就是可信来源,不是从消息正文猜来的。
65
+
66
+ **把群 ID 焊进 `SKILL.md` 的代价**:换个群、加个收件人、挪个时间,都要改 Skill 并重新 `dta deploy` 一次能力包;同一个能力也没法同时服务多个投递场景(早上发 A 群、晚上发 B 群就得复制一份 Skill)。策略留在 schedule 里,改调度不动能力。
67
+
52
68
  ---
53
69
 
54
70
  ## 在仓库里声明:`dingtalk-agent.json#schedules`
@@ -94,9 +110,11 @@
94
110
  ## 三条与平台无关的硬约束
95
111
 
96
112
  - **定时不可靠是常态**:平台会塌缩漏掉的 fire、跳过迟到过久的(Multica 阈值 5 分钟)。判据必须写成「它今天/本周发生了没有」,不是「那一拍跑了没」。
97
- - **外发类节律没有锁**:外发的留痕写在动作之后,两条节律同一分钟并发会把同一条消息发两遍给第三方。**多节律必须错开分钟**,并在 Skill 里写死"一拍内同一件外发绝不执行第二次"
113
+ - **外发类节律没有锁**:外发的留痕写在动作之后,两条节律**同一时刻**并发会把同一条消息发两遍给第三方。**多节律必须错开 fire 时刻**(分钟×小时都不相交才算错开——`0 0-6` 与 `0 23` 同分钟但不同刻,不算撞;`dta schedule show` 现在按真 fire 时刻判,不再只看分钟),并在 Skill 里写死"一拍内同一件外发绝不执行第二次"。**所有节律写同一个 timezone**:跨时区的 `0 9 * * *`@Asia/Shanghai 与 `0 1 * * *`@UTC 是同一时刻,本地算不出来,一律当撞车挡住部署。
98
114
  - **Agent 不自建定时器、不自改节律**:节律是可审计的部署产物。在线 Run 只能把新节律整理成候选交给 operator——和"在线 Run 不能热改身份/权限/Skill"是同一条边界。
99
115
 
116
+ **再两条排布经验**(真实生产换来,详见 [AGENT-IN-PRODUCTION.md](../../../../docs/AGENT-IN-PRODUCTION.md) §1):**有时效的轻活排前,别让重活饿死它**——把每次都超时的重活排在一拍最前,后面的对账/收件会整轮轮不到(真事故:留痕对账排夜拍第 7 位、夜间重建每拍超时,结果整夜没跑到);正解是轻活/有时效的排前,或给重活单独一条节律、独立预算。**入口是脉冲、出口是均匀**——用户常集中在某时段涌入,产能要按峰值窗口算,按日均算会在高峰欠账。
117
+
100
118
  ---
101
119
 
102
120
  ## 谁来设定:三角色
@@ -119,6 +137,8 @@
119
137
  - [ ] ③ 是增量 delta 不是重建;单拍在预算内、能收敛
120
138
  - [ ] ④ 完成靠效果为证(write/receipt/水位),不认自述、不认空读
121
139
  - [ ] ⑤ 有一条监督节律盯收敛、卡住报人
122
- - [ ] 多条节律错开分钟;外发类有"一拍内不重复"约束
140
+ - [ ] 多条节律错开 fire 时刻、且统一一个 timezone(`dta schedule show` exit 0);外发类有"一拍内不重复"约束
141
+ - [ ] 有时效的轻活排在重活前面(或重活单独一条节律);别让排最前的超时重活饿死尾部轻活;产能按峰值窗口算不按日均
142
+ - [ ] 每条 `mode` 的可计量性心里有数(`schedule show` 标 metered;ephemeral 不记账,成本要另建观测层——见 AGENT-IN-PRODUCTION.md)
123
143
  - [ ] 目标平台 `PLATFORM.md` 确认支持;不支持记为缺口,不静默降级
124
144
  - [ ] `dta info --json` 回读校验通过(5 段 cron、合法 mode、name 唯一)
@@ -3,7 +3,7 @@ name: dta-basic-behavior
3
3
  description: 当 Agent 作为钉钉里的真实员工处理消息、@、单聊、群聊、任务、确认、记忆或协作事项时使用;即使当前目录没有初始化 Workspace、没有 CONTEXT.md,也先用本 Skill 判断响应资格、意图、作用域、风险、授权、隐私与完成状态。Prepared Run 中只通过 dingtalk-agent 原子动作,普通会话的钉钉产品操作按需使用 dws。
4
4
  compatibility: Requires dingtalk-agent on PATH; DingTalk side effects require dws.
5
5
  metadata:
6
- version: "0.13.0"
6
+ version: "0.13.1"
7
7
  ---
8
8
 
9
9
  # 钉钉数字员工基础行为
@@ -34,7 +34,7 @@ Provider 只改变“内容放在哪里”,不改变下面的筛选规则。
34
34
 
35
35
  在线 Run 只采证并提出局部候选;离线流程把真实纠正加入回归集,执行 baseline/candidate 对照,生成候选和报告。通过 gate 也不自动扩大权限或热替换当前 Run。
36
36
 
37
- 长期写入至少携带:`source`、`scope`、`recorded_at`、`confidence` 和 `supersedes`。没有值得保存的内容时,`nothing-to-save` 是正确结果。
37
+ 长期写入至少携带:`source`、`scope`、`recorded_at`、`confidence` 和 `supersedes`。没有值得保存的内容时,`nothing-to-save` 是正确结果——但**这一步跑过、判了没什么可存,也要留一条痕迹**(哪怕就是 `nothing-to-save`)。否则「判过、无可存」和「压根没跑」从外面分不出来,这条记忆能力到底在不在工作就永远查不清。完成要有证据,不完成也要有证据。
38
38
 
39
39
  任务 checkpoint 与长期记忆必须分开:完成后可从任务证据中提炼稳定事实或 Skill 候选,但不能把进度、等待状态和临时计划整体写进 `MEMORY.md`。
40
40
 
@@ -3,7 +3,7 @@ name: dta-people-group-memory
3
3
  description: 让办公 Agent 认识「人」和「会话(群/单聊)」并记住交互。需要判断某位同事是谁、在做什么、怎么和他协作、我们之前聊过什么,或需要判断某个群是干什么的、在推什么、谁说了算、这事在哪讨论过、我该不该在这儿说话时使用;用户要求「记住这个人/沉淀一下这次聊天/以后记得我的偏好」时也使用。未绑定存储前完全惰性,不写任何东西。
4
4
  compatibility: Storage-agnostic. Ships with a DingTalk-document adapter and a local-Markdown adapter; the model and ingest layers never touch a specific medium. Binding happens on demand under the runtime identity.
5
5
  metadata:
6
- version: "0.3.0"
6
+ version: "0.3.1"
7
7
  ---
8
8
 
9
9
  # 认识人 · 认识会话 · 记住交互
@@ -23,7 +23,7 @@ metadata:
23
23
  └ adapters.md 介质特有的坑全部收在适配器里
24
24
  ```
25
25
 
26
- **存储是双介质、按 section 种类分流**(`model.md` §三):叙事类正文(identity/chronicle/digest…)走**钉钉文档**(读整页、人打开看);注册表类(索引/水位/count/tombstone/roster)走 **AI 表格**(按 key 查、真分页、不撞截断墙)。同一套模型层操作,registry 换成结构化介质就把一半病理关掉——这是端口解耦的现场兑现。
26
+ **存储是双介质、按 section 种类分流**(`model.md` §三):叙事类正文(identity/chronicle/digest…)走**钉钉文档**(读整页、人打开看);注册表类(索引/水位/count/tombstone/roster)走 **AI 表格**(**按 key / recordId 有界查**,不撞文档字符墙)。同一套模型层操作,registry 换成结构化介质就把一半病理关掉——这是端口解耦的现场兑现。**但 AI 表格的全量枚举仍会静默截断**(见四条不变式③):只有按 key/recordId 的有界查询能当存在性证据,别把它当"随便分页扫都安全"。
27
27
 
28
28
  **解耦的检验标准**:如果这世界上只有本地 Markdown 文件这一种存储,模型层会长成同样的样子吗?会,才算解耦。凡是“因为钉钉文档会截断,所以本体要按月分页”这类推理,都必须改写成“适配器声明读取上限,通用算法据此分片”。
29
29
 
@@ -54,8 +54,8 @@ metadata:
54
54
  ## 四条不变式(模型层保证,不下放给适配器)
55
55
 
56
56
  1. **原始层只增不删,派生层可重建。** 纠错用 `supersedes` 并列新增,不改写原件。
57
- 2. **写后必回读,`success` 不算数。** 没有回读证据不许声称已记。
58
- 3. **读失败 ≠ 不存在。** 读取失败、分页未完、授权缺失一律记欠账并跳过本轮,绝不写成“没有”,也绝不据此新建重复实体。
57
+ 2. **写后必回读,且按 recordId 回读那一行,`success` 不算数。** 回读若靠全表扫或按键再查,会被截断骗到别的行或读空——只有拿写入拿到的 recordId 精确回读才作数。
58
+ 3. **读不完整 ≠ 不存在。** 不只是"读报错了":AI 表格分页会**静默截断**——成功返回一个偏小的前缀却报 hasMore=false,没有任何完整性凭据。所以全量枚举一律当"未证明完整",绝不据此写"没有"、也绝不据此新建重复实体。要判"某人/某群不存在",用有界的按键查(命中 0 行),不要用全表扫。
59
59
  4. **先写事实、后推水位。** 任一失败即本批作废、水位不动——水位虚高会让这批源事件永远不再被消化。
60
60
 
61
61
  ## 两个实体,一句话说清区别
@@ -145,4 +145,4 @@ watermark: { subject: <key>, channel: im|todo|doc, value: <绝对时间> }
145
145
  4. 交叉归属三铁律。
146
146
  5. 先写事实、后推水位。
147
147
  6. 会话隐私档位白名单默认拒绝。
148
- 7. **"读失败 ≠ 不存在"**:任何读取失败、分页未完、授权缺失,一律记欠账并跳过本轮,**绝不写成"没有"**,也绝不据此新建重复实体。
148
+ 7. **"读不完整 ≠ 不存在"**:不只是读报错——AI 表格分页会**静默截断**(成功返回偏小前缀却报 hasMore=false,无完整性凭据)。读失败、分页未完、授权缺失、以及**任何全量枚举**,一律当"未证明完整",记欠账、跳过本轮,**绝不写成"没有"**,也绝不据此新建重复实体。判"不存在"只能用有界的按键查(命中 0 行)。
@@ -34,7 +34,7 @@ Multica 是钉钉 FDE fork 的托管 Agent 平台:把一个 dingtalk-agent 数
34
34
  - **run 幂等按 `(trigger, 计划时刻)`;漏掉的 fire 会塌缩成最近一次,迟到超过 5 分钟直接跳过。** 所以定点 cron 只用来开窗,不能用来保证「某时刻必须发生」——完成判据必须逾期后持续为真。
35
35
  - **多条节律不要落在同一分钟。** 外发类的留痕写在动作之后、没有锁,并发唤醒会把同一条消息发两遍给第三方。
36
36
 
37
- `autopilot-create` 的 stdout **带一行非 JSON 前缀**(`scheduled: <cron> ..., next run ...`)。直接 `json.load(stdout)` 会抛异常,让人误判「没建成」而**重复创建**——重复的节律就是重复的外发。建完一律用 `autopilot-list` / `autopilot-get` 独立回读条数、cron、时区、提示词、启用状态和 `next_run_at`,不要相信创建响应。
37
+ `autopilot-create` 的 stdout 是**纯 JSON**(`scheduled: ...` 进度提示走 stderr,`json.load(stdout)` 不会炸)。真正的坑是 **create 非幂等**:重复创建就是重复的节律、重复的外发。建完一律用 `autopilot-list` / `autopilot-get` 独立回读条数、cron、时区、提示词、启用状态和 `next_run_at`,不要相信创建响应。
38
38
 
39
39
  `multica_ext.py` **没有** `autopilot-delete`;误建的先 `autopilot-set-status --status paused` 止血,再按需 `DELETE /api/autopilots/{id}`(需带 `X-Workspace-Id` 头,否则 400)。
40
40
 
@@ -48,7 +48,7 @@ Multica 是钉钉 FDE fork 的托管 Agent 平台:把一个 dingtalk-agent 数
48
48
 
49
49
  1. **dws 自动路径(首选)**:`dws dev app list` 选已有应用或经用户确认 `dws dev app robot submit` 新建 →(异步,`robot result` 轮询到 SUCCESS)→ `dws dev app credentials get --unified-app-id <id>` 取 AppKey/AppSecret → `dingtalk-bind --agent <id> --client-id <AppKey> --client-secret-stdin`。
50
50
  - 机器人 desc 只允许中英文数字和特定标点(冒号/括号会触发 67010 失败)。
51
- - 想让未绑定账号的用户也能直接对话(快速验收/对客服务),绑定时加 `--allow-unbound`(connect-to-customers 模式:以安装者身份服务未绑定发送者);否则未绑定发送者只会收到账号绑定引导。
51
+ - **不加 `--allow-unbound` 的默认行为是拦截**:任何未把钉钉账号绑定到 Multica 的发送者(包括机器人 owner 本人)@ 机器人只会收到账号绑定引导,不产生任何 Run——验收第一句话就会撞上。想让未绑定账号的用户直接对话(快速验收/对客服务),绑定时加 `--allow-unbound`(connect-to-customers 模式:以安装者身份服务未绑定发送者),或先走 `dingtalk-account-begin --binding-mode message` 完成绑定。
52
52
  2. **复用已有安装**:`dingtalk-list` 查看现有机器人安装换绑。
53
53
  3. **扫码兜底**:`dingtalk-begin` 产出扫码链接交用户完成(不要用 `--wait` 阻塞会话)。
54
54
 
@@ -3,7 +3,7 @@ name: dta-deploy-multica
3
3
  description: 当开发者要把 dingtalk-agent Agent Project 部署、更新、检查、恢复或 retire 到 Multica Workspace,或把通过指定 Eval gate 的本地版本晋级、把脱敏反馈转成待评审 Eval candidate 时使用。只编排 dta 的稳定 deploy/promote/observe CLI,不直接调用 Multica 写命令,不创建 Trigger,不热改 Agent 本体或 Skill,也不替用户登录或持有凭据。
4
4
  compatibility: Requires dingtalk-agent and an authenticated Multica CLI profile for live apply/status readback.
5
5
  metadata:
6
- version: "0.4.0"
6
+ version: "0.5.0"
7
7
  ---
8
8
 
9
9
  # 部署 dingtalk-agent 到 Multica
@@ -13,11 +13,12 @@ metadata:
13
13
  ## 工作顺序
14
14
 
15
15
  1. 先运行 `dta workspace doctor <name> --json` 和 `dta workspace inspect <name> --execute --yes --json`。登录、profile 默认 Workspace、显式 Workspace、Runtime、Agent ID 链或 Skill 唯一性不一致时停止;不要用 `--yes` 越过。
16
- 2. 运行 `dta deploy --workspace <name> --dry-run --json`,向用户展示 `planId`、本体/Skill hash、forward/rollback 写预算、将读取和写入的资源类型、删除/retire 范围以及不包含 Trigger。
16
+ 2. 运行 `dta deploy --workspace <name> --dry-run --json`,向用户展示 `planId`、本体/Skill hash、forward/rollback 写预算、将读取和写入的资源类型、删除/retire 范围以及不包含 Trigger。plan 对中间状态敏感:dry-run 之后、apply 之前不要插入 `workspace inspect` 等操作,否则 planId 作废,重跑 dry-run 后立即 apply。
17
17
  3. 只有用户明确确认这份 plan 后,才运行 `dta deploy --workspace <name> --plan-id <id> --yes --json`。旧 planId、当前选择的 Workspace 或显示名都不能代替稳定 ID。
18
- 4. 返回 pending、超时或本地 Receipt 写入失败时,不重复 apply。load-smoke Issue 已创建但没有任何 task 时,CLI 会且只会按稳定 Agent ID 在同一 Issue 留下一条受控 `@` 评论恢复首次入队;已有 queued/running/terminal task 时绝不重复触发。随后运行 `dta deploy --workspace <name> --status --operation-id <id> --execute --yes --json` 做独立回读和 reconcile
19
- 5. 只有远端 Agent/Runtime/Skill tree/assignment 回读一致,System Prompt 与本地人类可读 Definition 原文一致,并且 Basic、Role 的随机 load smoke 轨迹通过,Workspace 才能进入 `ready`。
20
- 6. 退役先 dry-run,再用冻结 plan 执行 `--retire`;只 archive 精确 Agent,保留 SkillIssue、语义存储和 Artifact,不把 retire 当成删除。
18
+ 4. 返回 pending、超时或本地 Receipt 写入失败时,不重复 apply。load-smoke Issue 已创建但没有任何 task 时,CLI 会且只会按稳定 Agent ID 在同一 Issue 留下一条受控 `@` 评论恢复首次入队;已有 queued/running/terminal task 时绝不重复触发。随后运行 `dta deploy --workspace <name> --status --operation-id <id> --execute --yes --json` 做独立回读和 reconcile。注意不带 `--execute` 的 `--status` 返回的是 apply 时刻的**冻结回执**(首次部署 smoke 字段常是空的 pending 快照,输出会标 `receiptFrozen` 并给出刷新命令);smoke 失败若分类为 `skills-not-visible`(冷沙箱尚未物化 Skill),状态收敛为 `verifying` 而非 `failed`,用带 `--execute --yes` 的 status 重试即可。
19
+ 5. runtime 只有两条正路:显式迁移用 `dta deploy --rebind-runtime`(先 dry-run,planId 绑定迁移前后 runtime,Receipt 记录 from/to);退役用 `--retire`(不受 `agent.runtime-drift` 阻挡)。不要原地改 env 里的 runtime id 硬顶普通 apply——那会被 fail-closed 挡下。
20
+ 6. 只有远端 Agent/Runtime/Skill tree/assignment 回读一致,System Prompt 与本地人类可读 Definition 原文一致,并且 BasicRole 的随机 load smoke 轨迹通过,Workspace 才能进入 `ready`。
21
+ 7. 退役先 dry-run,再用冻结 plan 执行 `--retire`;只 archive 精确 Agent,保留 Skill、Issue、语义存储和 Artifact,不把 retire 当成删除。
21
22
 
22
23
  完整命令、状态机与失败恢复见 [multica-deployment-contract.md](references/multica-deployment-contract.md)。
23
24
 
@@ -23,6 +23,10 @@ dta workspace run multica-dev --prompt-file readonly-question.md --execute --yes
23
23
  # 只 archive 精确 Agent;不删除 Skill、Storage、Issue 或 Trigger
24
24
  dta deploy --workspace multica-dev --retire --dry-run --json
25
25
  dta deploy --workspace multica-dev --retire --plan-id <planId> --yes --json
26
+
27
+ # 显式把 Agent 迁到 env 当前指向的 runtime(planId 绑定迁移前后 runtime,Receipt 记 from/to)
28
+ dta deploy --workspace multica-dev --dry-run --rebind-runtime --json
29
+ dta deploy --workspace multica-dev --rebind-runtime --plan-id <planId> --yes --json
26
30
  ```
27
31
 
28
32
  ## 状态机
@@ -33,7 +37,10 @@ planned → applying → verifying → ready
33
37
  ready → retired
34
38
  ```
35
39
 
36
- - 每次 apply 前重新读取 W3 的 profile/auth/Workspace/Runtime/Agent/Skill ID 链;与 plan 不一致时在任何写入前失败。
40
+ - 每次 apply 前重新读取 W3 的 profile/auth/Workspace/Runtime/Agent/Skill ID 链;与 plan 不一致时在任何写入前失败。scope 类失败带结构化 `failureDetails`(expected/actual/hint);provider CLI 自身失败时其 stderr 原文透传到终端,证据文件仍只存 hash。
41
+ - 目标 runtime 的 provider 必须在受支持词汇表内(当前 `opencode`);hermes 等不受支持 provider 在任何远端写入前 fail closed(`scope.runtime-provider-unsupported`)——那类 runtime 上 workspace skill 一律加载不到,部署"成功"也是死局。
42
+ - Agent 的 runtime 归属漂移(`agent.runtime-drift`)只挡普通 apply:显式迁移用 `--rebind-runtime`,退役用 `--retire`,都不被漂移锁死。
43
+ - dta 调用 multica CLI 前会从子进程环境剔除保留变量 `MULTICA_AGENT_ID`(切 Agent 身份鉴权、误报未登录)、`MULTICA_SERVER_URL` 与 `MULTICA_TOKEN`(把 endpoint/身份从具名 profile 脚下换走——shell 里残留的预发 URL 绝不能重定向生产部署);剔除记录在 inspection 的 `envSanitized`。
37
44
  - plan 同时冻结 forward/rollback 最大写预算;最多 32 个受管 Skill、每个最多 128 个 supporting files,CLI 在每个 Provider mutation 前检查预算。
38
45
  - Provider 调用超时属于结果未知:先保存 `reconciling` operation,再只读 status;不盲目重放 create/update。
39
46
  - load-smoke Issue 创建成功但 `issue runs` 为空时,说明 Issue 已持久化而首次入队未落地;apply 只允许按计划中稳定 Agent ID 在同一 Issue 留下一条受控 `mention://agent/<id>` 评论恢复一次。只要已有 task(包括 queued/running/terminal),就不得触发第二次运行。
@@ -48,10 +55,13 @@ ready → retired
48
55
  3. Basic、Role Skill 的完整文件树 hash 一致;
49
56
  4. assignment 精确等于部署计划,不包含隐式平台启动 Skill;
50
57
  5. smoke task completed;
51
- 6. 轨迹中 Basic、每个 Role 都有 Host 原生 Skill load 证据;
52
- 7. 最终 marker/loaded 清单与计划一致;
53
- 8. operation Receipt hash 完整,Workspace state 绑定当前 desired/deployment hash。
58
+ 6. 轨迹中 Basic、每个 Role 都有装载证据——按目标 runtime 的 trace 词汇解读:Host `skill` 工具调用(工具本身就是装载机制),或云 runtime `read_file` **结果**中的 `**Skill loaded**` banner。装载「尝试」(`Loading skill 'X'` 的 tool_use,失败时同样会出现)与模型自述文本都不算证据,banner 也只认可信工具(read_file/skill)的 tool_result——terminal/bash 的输出是模型指定的,echo 伪造不算数;
59
+ 7. 精确 JSON 回执可回读——优先从 Issue comment 本身取证(平台回读,与工具词汇无关),其次是回复文本 / 受限收口文件;
60
+ 8. 轨迹不含 dws(工具名或 shell 命令);
61
+ 9. operation 与 Receipt hash 完整,Workspace state 绑定当前 desired/deployment hash。
54
62
 
55
63
  任一门禁失败都保持 `verifying` 或进入 `failed`,不能用本地 OpenCode 通过、旧 smoke、目录存在或模型自述覆盖。
56
64
 
65
+ **ready 证明什么、不证明什么**:ready 证明每个必需 Skill 有装载证据、精确回执已落盘、无 dws 调用;**不再**证明「轨迹里每个工具都在白名单内」——无害的额外工具/命令不判死(旧的负向白名单在健康云 runtime 上造成过结构性假阴性),词汇表不认识的工具名记录在 `smoke.unrecognizedTools` 供诊断。smoke 失败若分类为 `skills-not-visible`(任务终态但轨迹显示 Skill not found,冷沙箱推送物化竞态),状态收敛为 `verifying`:apply 在预算内做有限次受控 mention 重试,`--status --execute --yes` 也可补一次(该情形如实上报 `remoteWrite`)。重试以「最新 run 的 taskId 变化」为新 run 出现的判据——mention 触发的 run 在平台上异步创建,等不到新 run 只会停手,不会重复 mention;重试评论本身的写入抖动只终止重试并保持 `verifying`,绝不触发对已验证部署的回滚。
66
+
57
67
  `workspace run` 在创建 Issue 前重新核对精确 Workspace、Runtime、Agent 与 assignment。报告记录 Issue/task 状态、实际 Skill trace、回答、轮询次数和脱敏调用;同一状态的重复轮询只保留一条证据。超时后仍没有关联 task 时返回 `task.not-created`,并把 Issue 状态写进 `taskDiagnostic`,不能把 Issue 创建成功算成 Agent 已运行。
@@ -2,7 +2,7 @@
2
2
  name: dta-ops-multica
3
3
  description: External Python wrapper defining the full developer chain for delivering an agent on the Multica (DingTalk-FDE fork) platform — workspace bootstrap, runtime/agent provisioning, skill assembly (push/pull), DingTalk robot & account binding, execution-trace observability, cron scheduling (autopilot), and health checks. Use when provisioning, operating, debugging, or observing a Multica-managed agent from the command line with only an endpoint and a token.
4
4
  metadata:
5
- version: "0.2.0"
5
+ version: "0.3.0"
6
6
  ---
7
7
 
8
8
  # Multica FDE External Skill
@@ -60,8 +60,13 @@ python3 $PY agent-create --workspace fde-team --name "FDE 教练" \
60
60
  merge, never clobber; repo registration needs owner/admin. Slugs:
61
61
  `^[a-z0-9]+(-[a-z0-9]+)*$`, reserved list enforced. An agent needs a runtime:
62
62
  FC-E2B cloud (`runtime-create-fc`, requires server-side FC-E2B enablement) or
63
- a local daemon. `runtime-templates` lists templates; when `--template-id` is
64
- omitted and exactly one exists it is auto-picked.
63
+ a local daemon. `runtime-create-fc` injects `--provider opencode` by default —
64
+ the SERVER default is hermes, where workspace skills structurally never load
65
+ (agents answer "Skill not found" with zero warning); only pass
66
+ `--provider hermes` if that is explicitly what you want. `runtime-templates`
67
+ lists templates; when `--template-id` is omitted the sole READY template is
68
+ auto-picked (waiting templates are excluded; several ready ones → the error
69
+ lists candidates sorted by `created_at`, pick the newest).
65
70
 
66
71
  ### 2 — Define & assemble: where the agent comes from, what skills it has
67
72
 
@@ -94,10 +99,19 @@ python3 $PY dingtalk-bind --agent <uuid> --client-id <AppKey> --client-secret-st
94
99
  python3 $PY dingtalk-begin --agent <uuid> --wait
95
100
  python3 $PY dingtalk-list ; python3 $PY dingtalk-revoke --installation <uuid>
96
101
  # Personal-account binding family (dingtalk_account):
97
- python3 $PY dingtalk-account-begin --agent <uuid> # prints QR URL
98
- python3 $PY dingtalk-account-list ; python3 $PY dingtalk-account-unbind --installation <uuid>
102
+ python3 $PY dingtalk-account-begin --agent <uuid> --binding-mode message # prints QR URL
103
+ python3 $PY dingtalk-account-list
104
+ python3 $PY dingtalk-account-unbind --installation <uuid> --binding-mode message
99
105
  ```
100
106
 
107
+ `binding_mode` is server-enforced (400 `invalid_binding_mode` without it) and
108
+ its wire contract is asymmetric: `begin` takes it in the JSON body, `unbind`
109
+ in the query string. `message` maps the sender's identity for conversation
110
+ routing only; `identity` grants the agent a persistent delegated DingTalk
111
+ identity — never choose it silently on the user's behalf. Without
112
+ `--allow-unbound` on the robot install, senders not account-bound to Multica
113
+ (the robot owner included) only receive a binding prompt.
114
+
101
115
  Bind and unbind always come in pairs: robot `dingtalk-bind`/`dingtalk-begin`
102
116
  ↔ `dingtalk-revoke`; account `dingtalk-account-begin` ↔
103
117
  `dingtalk-account-unbind`. `dingtalk-account-begin` returns the binding link
@@ -142,6 +156,10 @@ that desired state against live:
142
156
 
143
157
  ```bash
144
158
  dta schedule plan --workspace <name> --json # desired state, provider-mapped
159
+ # EXIT 2 = do NOT reconcile. The manifest has schedules that collide at the same
160
+ # instant, that can't be proven not to (cross-timezone / cron extension syntax), or
161
+ # that lack a data-derived completion. Deploying anyway creates one autopilot per
162
+ # entry → the same message goes out twice. Fix the manifest, don't override.
145
163
  python3 $PY autopilot-list # live; match declared↔live by title
146
164
  # create only missing, update drifted, delete extra — autopilot-create is NOT
147
165
  # idempotent, so ALWAYS list first; blind-creating duplicates → duplicate外发.
@@ -175,8 +193,16 @@ python3 $PY agent-check --agent <uuid>
175
193
  Composite JSON report: agent status/archived, runtime health (online /
176
194
  unstable = offline but seen <5 min ago / offline / missing), active tasks +
177
195
  last outcome (with `failure_reason`), recent failure count, DingTalk robot +
178
- account binding state, plus a `checks[]` verdict list; `healthy` = no `fail`
179
- (warns don't fail the check).
196
+ account binding state, plus a `checks[]` verdict list. **BREAKING (0.3.0)**:
197
+ the single `healthy` boolean is removed — it reported green while users could
198
+ not reach the agent at all (no robot / account unbound are only warns).
199
+ Consume the split verdict instead: `infrastructureHealthy` (agent/runtime/
200
+ tasks have no fail) and `userReady` (infrastructure healthy AND robot bound
201
+ AND `dws_identity` bound), with `reasons[]` naming every gap and `nextAction`
202
+ carrying the single next command. `userReadyCaveat` records the one upstream
203
+ blind spot: installations don't read back effective `allow_unbound`, so a
204
+ robot bound with that flag serves unbound senders even when
205
+ `userReady=false`.
180
206
 
181
207
  ### 7 — Basics: issues, comments, chat, members, tokens
182
208
 
@@ -313,15 +313,33 @@ def cmd_runtime_create_fc(ctx: Ctx, args: argparse.Namespace) -> None:
313
313
  # The server resolves template_id against id, template, or name;
314
314
  # id is best-effort and may be absent.
315
315
  ref = lambda t: t.get("id") or t.get("template") or t.get("name") # noqa: E731
316
- if len(templates) > 1:
316
+ status_of = lambda t: str(t.get("status") or "").lower() # noqa: E731
317
+ # waiting 等未就绪状态选了也建不起来;status 字段缺失时保守放行(Observed 字段)。
318
+ usable = [t for t in templates if not status_of(t) or status_of(t) == "ready"]
319
+ if not usable:
317
320
  fail(
318
- "multiple templates available, pass --template-id: "
319
- + ", ".join(f"{ref(t)} ({t.get('name') or t.get('template')})" for t in templates)
321
+ "no ready FC-E2B templates (all "
322
+ + ", ".join(f"{ref(t)}={status_of(t) or 'unknown'}" for t in templates)
323
+ + ")"
320
324
  )
321
- template_id = ref(templates[0])
325
+ if len(usable) > 1:
326
+ newest = max(usable, key=lambda t: str(t.get("created_at") or ""))
327
+ fail(
328
+ "multiple ready templates, pass --template-id: "
329
+ + ", ".join(
330
+ f"{ref(t)} ({t.get('name') or t.get('template')}, "
331
+ f"{status_of(t) or 'unknown'}, created {t.get('created_at') or '?'})"
332
+ for t in sorted(usable, key=lambda t: str(t.get("created_at") or ""), reverse=True)
333
+ )
334
+ + f"; component_versions 通常完全相同,选最新 ready 即可(推荐 {ref(newest)})"
335
+ )
336
+ template_id = ref(usable[0])
322
337
  if not template_id:
323
338
  fail("template list entry has no usable identifier")
324
- note(f"using template {template_id} ({templates[0].get('name') or templates[0].get('template')})")
339
+ note(
340
+ f"using template {template_id} ({usable[0].get('name') or usable[0].get('template')}, "
341
+ f"{status_of(usable[0]) or 'unknown'})"
342
+ )
325
343
  body: Dict[str, Any] = {"template_id": template_id}
326
344
  for flag in ("name", "provider", "visibility"):
327
345
  value = getattr(args, flag, None)
@@ -397,7 +415,15 @@ def cmd_dingtalk_bind(ctx: Ctx, args: argparse.Namespace) -> None:
397
415
  "robot_code": args.robot_code or args.client_id,
398
416
  "allow_unbound": args.allow_unbound,
399
417
  }
400
- out(api(ctx, "POST", f"/api/workspaces/{ws_id}/dingtalk/install/manual", body=body))
418
+ res = api(ctx, "POST", f"/api/workspaces/{ws_id}/dingtalk/install/manual", body=body)
419
+ if not args.allow_unbound:
420
+ note(
421
+ "allow_unbound=false: senders whose DingTalk account is NOT bound to Multica "
422
+ "(including the robot owner) only get a binding prompt when they @ the robot. "
423
+ "For acceptance/customer-facing use, either run dingtalk-account-begin first or "
424
+ "re-bind with --allow-unbound (upsert; installation id unchanged)."
425
+ )
426
+ out(res)
401
427
 
402
428
 
403
429
  def cmd_dingtalk_begin(ctx: Ctx, args: argparse.Namespace) -> None:
@@ -450,7 +476,9 @@ def cmd_dingtalk_account_begin(ctx: Ctx, args: argparse.Namespace) -> None:
450
476
  ctx,
451
477
  "POST",
452
478
  f"/api/workspaces/{ws_id}/dingtalk/account-bindings/begin",
453
- body={"agent_id": args.agent},
479
+ # Server enforces binding_mode (400 invalid_binding_mode without it): message-only
480
+ # identity mapping vs delegated identity. Wire contract: begin reads it from the BODY.
481
+ body={"agent_id": args.agent, "binding_mode": args.binding_mode},
454
482
  )
455
483
  note(f"scan to bind account: {res.get('qr_code_url')}")
456
484
  out(res)
@@ -463,7 +491,14 @@ def cmd_dingtalk_account_list(ctx: Ctx, args: argparse.Namespace) -> None:
463
491
 
464
492
  def cmd_dingtalk_account_unbind(ctx: Ctx, args: argparse.Namespace) -> None:
465
493
  ws_id = resolve_workspace_id(ctx)
466
- api(ctx, "DELETE", f"/api/workspaces/{ws_id}/dingtalk/account-bindings/{args.installation}")
494
+ # Wire contract asymmetry (verified live): unbind reads binding_mode from the QUERY
495
+ # string; putting it in the DELETE body still returns 400 invalid_binding_mode.
496
+ api(
497
+ ctx,
498
+ "DELETE",
499
+ f"/api/workspaces/{ws_id}/dingtalk/account-bindings/{args.installation}"
500
+ f"?binding_mode={urllib.parse.quote(args.binding_mode)}",
501
+ )
467
502
  out({"unbound": True})
468
503
 
469
504
 
@@ -675,6 +710,38 @@ def cmd_agent_check(ctx: Ctx, args: argparse.Namespace) -> None:
675
710
  json.dumps(account.get("dws_identity"), ensure_ascii=False) if account else "no account binding",
676
711
  )
677
712
 
713
+ # 基础设施健康 ≠ 用户可用:机器人未绑/账号 unbound 时用户发消息只会收到绑定引导、
714
+ # 不产生任何 Run,但没有一项是 "fail"。旧的单布尔 `healthy` 正是把这两层混成一团、
715
+ # 让 operator 看着全绿而用户侧完全不工作的祸根——按验收证据(P-01/M-06)拆开并移除。
716
+ infra_checks = {"agent", "runtime", "last_task", "recent_tasks"}
717
+ infrastructure_healthy = all(
718
+ c["status"] != "fail" for c in checks if c["check"] in infra_checks
719
+ )
720
+ robot_ok = bool(robot)
721
+ account_bound = bool(account and (account.get("dws_identity") or {}).get("status") == "bound")
722
+ user_ready = infrastructure_healthy and robot_ok and account_bound
723
+ reasons = [
724
+ f"{c['check']}: {c['detail']}"
725
+ for c in checks
726
+ if (c["check"] in infra_checks and c["status"] == "fail")
727
+ or (c["check"] in ("dingtalk_robot", "dingtalk_account") and c["status"] != "ok")
728
+ ]
729
+ if agent.get("archived_at"):
730
+ next_action = "agent restore(或重新部署)后再检查"
731
+ elif runtime_health in ("missing", "offline"):
732
+ next_action = "runtime-list 核对;必要时 runtime-create-fc --provider opencode 重建并迁移"
733
+ elif not robot_ok:
734
+ next_action = (
735
+ f"python3 multica_ext.py dingtalk-bind --agent {args.agent} "
736
+ "--client-id <AppKey> --client-secret-stdin(或 dingtalk-begin 扫码)"
737
+ )
738
+ elif not account_bound:
739
+ next_action = (
740
+ f"python3 multica_ext.py dingtalk-account-begin --agent {args.agent} "
741
+ "--binding-mode message(或重绑机器人加 --allow-unbound)"
742
+ )
743
+ else:
744
+ next_action = ""
678
745
  out({
679
746
  "agent": {k: agent.get(k) for k in ("id", "name", "status", "model", "runtime_id", "archived_at")},
680
747
  "runtime": {
@@ -683,7 +750,14 @@ def cmd_agent_check(ctx: Ctx, args: argparse.Namespace) -> None:
683
750
  },
684
751
  "tasks": {"active": len(active), "last_outcome_status": (last_outcome or {}).get("status")},
685
752
  "checks": checks,
686
- "healthy": all(c["status"] != "fail" for c in checks),
753
+ "infrastructureHealthy": infrastructure_healthy,
754
+ "userReady": user_ready,
755
+ "reasons": reasons,
756
+ "nextAction": next_action,
757
+ "userReadyCaveat": (
758
+ "安装回读不含 effective allow_unbound(上游缺口 MUL-005):若绑定时开启过 "
759
+ "allow_unbound,未绑定账号的发送者实际也能对话"
760
+ ),
687
761
  "warnings": sum(1 for c in checks if c["status"] == "warn"),
688
762
  })
689
763
 
@@ -936,11 +1010,19 @@ def build_parser() -> argparse.ArgumentParser:
936
1010
  p = cmd(
937
1011
  "runtime-create-fc",
938
1012
  cmd_runtime_create_fc,
939
- "Create an FC-E2B cloud runtime; picks the sole available template when --template-id is omitted",
1013
+ "Create an FC-E2B cloud runtime (provider defaults to opencode); "
1014
+ "picks the sole READY template when --template-id is omitted",
940
1015
  )
941
1016
  p.add_argument("--template-id", dest="template_id")
942
1017
  p.add_argument("--name")
943
- p.add_argument("--provider")
1018
+ p.add_argument(
1019
+ "--provider",
1020
+ default="opencode",
1021
+ help="runtime provider; defaults to opencode — the only provider dta's skill-loading "
1022
+ "contract supports. The SERVER default is hermes, where workspace skills never load "
1023
+ "(agents report 'Skill not found' with zero warning); pass --provider hermes only if "
1024
+ "you explicitly want that",
1025
+ )
944
1026
  p.add_argument("--visibility", choices=["private", "public"])
945
1027
 
946
1028
  p = cmd("agent-list", cmd_agent_list, "List agents in the workspace")
@@ -984,7 +1066,14 @@ def build_parser() -> argparse.ArgumentParser:
984
1066
  p.add_argument("--client-secret", dest="client_secret", help="DingTalk AppSecret (prefer stdin/env)")
985
1067
  p.add_argument("--client-secret-stdin", dest="client_secret_stdin", action="store_true")
986
1068
  p.add_argument("--client-secret-env", dest="client_secret_env", metavar="VAR")
987
- p.add_argument("--allow-unbound", dest="allow_unbound", action="store_true")
1069
+ p.add_argument(
1070
+ "--allow-unbound",
1071
+ dest="allow_unbound",
1072
+ action="store_true",
1073
+ help="serve senders whose DingTalk account is not bound to Multica "
1074
+ "(connect-to-customers). WITHOUT this flag such senders — the robot owner "
1075
+ "included — only receive a binding prompt",
1076
+ )
988
1077
 
989
1078
  p = cmd(
990
1079
  "dingtalk-begin",
@@ -1000,6 +1089,15 @@ def build_parser() -> argparse.ArgumentParser:
1000
1089
 
1001
1090
  p = cmd("dingtalk-account-begin", cmd_dingtalk_account_begin, "Start binding a personal DingTalk account to an agent")
1002
1091
  p.add_argument("--agent", required=True, help="agent uuid")
1092
+ p.add_argument(
1093
+ "--binding-mode",
1094
+ dest="binding_mode",
1095
+ choices=("message", "identity"),
1096
+ default="message",
1097
+ help="binding capability: message-only identity mapping (default), or delegated "
1098
+ "DingTalk identity — identity grants a persistent execution identity, never pick "
1099
+ "it silently for the user",
1100
+ )
1003
1101
 
1004
1102
  cmd("dingtalk-account-list", cmd_dingtalk_account_list, "List personal DingTalk account bindings")
1005
1103
 
@@ -1009,6 +1107,14 @@ def build_parser() -> argparse.ArgumentParser:
1009
1107
  "Remove a personal DingTalk account binding (row kept as revoked for audit)",
1010
1108
  )
1011
1109
  p.add_argument("--installation", required=True, help="installation uuid")
1110
+ p.add_argument(
1111
+ "--binding-mode",
1112
+ dest="binding_mode",
1113
+ choices=("message", "identity"),
1114
+ default="message",
1115
+ help="binding capability to remove: message-only identity mapping (default) or "
1116
+ "delegated DingTalk identity",
1117
+ )
1012
1118
 
1013
1119
  p = cmd("agent-tasks", cmd_agent_tasks, "List an agent's task run history")
1014
1120
  p.add_argument("--agent", required=True, help="agent uuid")