凭证到位后 `w05` 五案例连跑六轮,逼出八条**浏览器走查逮不到**的缺陷 —— 脚本每张卡都先空手交一次、每个岔路口都按序代答,人手点的时候永远不会去试 「空着交上去会怎样」。末轮:跑到的四例全部 released,三例被拒 0 (第五例停在大模型账户余额不足,不是代码问题)。 平台侧(六条): - L 数据工单的提交闸没牙 —— 卡没绑表就回落到必交付物,而数据工单要人补的是 `parts.*` 主数据,不在交付物名单里:闸盯着一堆无关的表、一格不缺,空手交卷一路放行。 契约新增 `ExecutionWorkOrder.gateLedgers`(这张卡该拦哪几张表,发卡的人说了算), `gateAddressesOf` 变三级取一:绑的表 > 卡自报 > 交付物。 - M `finish` 撞 doneWhen 为假的拒回只谈「行与列」,而缺的是一次**工具调用**; 现在摆出判据原文 + 那几个名字此刻的读数,`tool_*_calls` 读到 0 的单独点名(`unmetWhy`)。 - N 人点了决策卡,模型每拍再发一张,连拒五拍熔断。把「他选的是 approve」写进拒回**没用** —— 下一拍它又从头翻一次会滚走的窗口。改结构:`answeredCardFact` 把 「这张卡已答过 → 他选的是什么」做成每拍都在的事实行。 - O 模型传 `requirement: null`,工具 `null.trim()` 当场崩(边界上一律经 `text()` 归一)。 - P 拒回里的 `count=(没写过)` 是 S-FEEL 函数名被当成台账名 —— 假线索模型会去追。 - Q/S 零行 = 零缺,闸与哨兵一起放过空手交卷的数据工单(`emptyGate`:卡自报的表一行都没有 = 没完成);「早收」头一次只落观察、第二次起才算被拒(与「重发已答过的卡」同一把尺)。 活动侧(1.2.7 → 1.2.9):三张执行工单各自声明 `gateLedgers`;说明书③补「发卡前骨架必须先有行」、 ⑤补这张卡的前提(一颗 absent 都不剩);哨兵 `parts_profile_rows >= created` 逐颗对账; S0_bind 写明发录入卡那一拍别抢写需求行。 e2e 脚本加 `JCAI_E2E_BEARER` 直供旁路(服务账号 client_credentials 仍是正路)。 门禁 typecheck / lint / depcruise / test(2091)全绿;api:check 的差异仍只在 org/* 与 api/* (工作树里先前那批鉴权改动),card.d.ts 是本轮的纯新增字段。 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FJcX8A6TWp4GRs41TYryEh
42 KiB
活动开发规范(任务类活动)
2026-08-02 第三版。基准:
activities/quote-intake(A1 询价受理,@42,用户验收通过 —— 三条形状不同的案例端到端released、行动被拒 0)。目标读者:开发活动的 AI(任务活动 开发 agent)或人 —— 读完这一份,你应当能独立把一个 SOP 节点做成能released的 案例活动,不必读apps/api的源码。本版按四条轴重组:解耦机制(§2)· Harness 契约(§3)· Loop(§4)· 活动怎么写(§5)。报告模版单独成册:
docs/HTML报告生成规范.md(与本文互为姊妹篇, 报告与表单的关系在那份里立成硬规矩)。本文与代码冲突时以代码为准,发现即改本文。相关:
docs/编码规范.md§8、 各模块CONTRACT.md、docs/debt.md(欠账)。历史语义源已归档docs/archive/。
1. 世界观:一个活动是怎么跑起来的
活动 = 任务的定义。 编排引用一个活动,运行时长出一个任务(恰一名负责人)。 活动在系统外开发、经安装通道入库,版本恒新增、同版重装 no-op、在途任务按启动时版本跑完。
活动没有代码逻辑。 行为全部写在清单里:案例模型(CMMN)+ 逐步剧本(steps)+
说明书(narrative)+ 判定表(DMN)+ 技能 + 台账声明 + 报告模版。
「系统不执行代码即知包为何物」。src/index.ts 只是被静态审扫描的产物文本,
defineActivity({ name }) 即可。
推进由 Loop 逐拍驱动(一拍 = 一次模型调用):
语义引擎发布局面 ──> Agent 决断一串行动 ──> 引擎逐个执行 ──> 留痕 ──> 下一拍
· 当下可为(Enabled,标「须你启动」) (1-6 个) 被拒停在谁那儿
· 进行中(Active) tool 后截断
· 当前这一步的剧本(guide 摊开,其余步各留一行) 挂起型收拍
· 目标缺口 + 轨迹 + 工具清单 + 台账字典 + 基座读数
拍是按模型调用计费的 —— 一切设计围着「少一拍是一拍」:一拍连做、批量工具入参、 规则写死不留给模型判。A1 的账:六步的活,常路 9-11 拍收口(第一版 21 拍 —— 差距全在设计,不在模型)。
人只在责任点与放行点出现:确定交系统(判定表/基座读数),临场交 AI(编排、拆解、 落表),落地交人(审定、放行)。整套机制的反复主题只有一句: 该由人亲手落下的,不许引擎或 AI 悄悄替它发生;能由数据算出来的,不许活动自报。
1.1 四条铁律(违者拒装或被引擎拒)
| # | 铁律 | 机制承载 |
|---|---|---|
| ① | 只读世界:活动只能读台账、调读类工具、问模型、发卡问人 | 产物文本静态审(process.env/fetch/node:*/eval 命中即拒装);depcruise 双向隔离 |
| ② | 写只有草稿与意向:写台账 = 草稿层(不抛事件);外部动作 = 登记动作意向(入参冻结 + 幂等键)。登记那一刻世界上什么都没变 | write 只落草稿;写类工具自动转意向 |
| ③ | 终局唯放行:任务完成的唯一出口是人验收通过 —— 那一刻才草稿合并、意向执行、输出口发射 | submit = 发验收卡挂起;放行落地见 §3.7 |
| ④ | 活动不自转:推进由引擎逐拍驱动;预算与熔断由 Harness 环外强制 | 迁移拓扑把 Completed/Terminated 设为死胡同 |
2. 解耦机制:活动与系统之间只有一条通道
2.1 安装通道(唯一入口)
activities/<id>/src/ ──(pnpm activities:build)──> dist/{manifest.json,index.js}
──(pnpm activities:install)──> 库(经安装通道读文件,不经 import)
- 新包在
tools/build-activities.ts的 PACKAGES 与apps/api/src/app/seed-activities.ts的REAL_PACKAGES两处登记。 - 别用
db:seed装活动:它重写整套治理台配置。 - 版本纪律:改了任何内容必须升
identity.packageVersion—— 同版重装静默丢失(踩过)。 卸载只置状态不删行;同名旧槽位要退役卸载(库里两个同名活动会骗人)。 - 撤掉一张台账声明不会 drop 物理表(物化只增不破坏)—— 历史行还在库里,要清得单独迁移。
2.2 import 白名单与静态审
活动包只准 import @sdk / @contracts / @sfeel;depcruise 双向隔离
(activities 不进 apps,apps 不 import activities)。产物文本黑名单:
process.env、require(、node:fs/net/child_process、fetch(、XMLHttpRequest、
eval(、new Function(。视图模版含 <script 或外链即拒。
2.3 声明即授权(三个授权面)
| 声明 | 授权了什么 | 越界的下场 |
|---|---|---|
declarations.ledger |
运行时只准写这里声明的地址与列名;人从件上改回来的值也走同一道门(§3.1) | 编出来的列当场拒;未声明地址的保存一格不落 |
declarations.tools |
Agent 可调的工具名单 | 未白名单授权报错(越权不转人) |
assets.viewTemplates |
工作区能推的画面 | 模版名对不上,「换画面失败」留痕、屏上不动 |
静态审还核:谓词里的名字必须是声明过的台账列或基座读数(域_对象_rows /
_missing / _gaps / _<列>_<值> 这些基座读数在审的白名单里)。
2.4 边界纪律:别替下游产出
随令牌流向下一个节点的,只有工作成果验收里的内容(出口载荷 = 已落地的事实)。 下游活动拿到之后按自己的口径归一需要的数据。
实测账(2026-08-02,用户当场定):A1 从前逐条开「替代评估请求 / 询价请求」交给 A2/A3 —— 那是替下游把它的输入先摆好,而下游本来就要归一一遍:同一份事实两处各存一份, 改一处就分叉,而分叉之后没人看得出以哪份为准。两张表连同派单判据、报告明细表一并退休。
跨活动共享事实只有两条路:输出口载荷,或共享域(shared/customer/market 等
主数据域)。活动自持域用活动名(quote_intake)。
3. Harness 契约:基座替你做什么
这一章列的是你能依赖、也必须依赖的基座行为 —— 活动侧不要重做它们, 更不要绕开它们。
3.1 台账:三写一撤 + 归型 + 字段四标记
- 写三分:
systemWrite(世界已发生的事实,即时抛事件)/draftWrite(任务草稿层, 读己之写,不抛事件)/releaseWrite(放行合并,此刻抛事件)。三个方法不是一个带 flag 的方法。 draftDrop:撤掉本单草稿层的一行 —— 不是第四种写(正式版碰不到、不抛事件)。 它存在的理由:门拦住一件办错的事却不给改正的手段,那扇门就只是个死胡同。- 归型前移:
draftWrite按声明列型归(空值 = 不写这一列、数值/布尔字符串转真类型、 归不了当场拒)。人手改的值与 AI 写的值过同一道门。 - 字段五标记(
fieldDeclarationSchema):
| 标记 | 语义 | 基座据此产出 |
|---|---|---|
required |
这一列缺了就是活没干完(验收标准;不建 NOT NULL) | 域_对象_missing / _missing_keys;执行卡提交闸;件上缺项红带 |
expect |
缺了不拦单,但那一问不能省(客户该给而没给的) | 域_对象_gaps / _gaps_keys;L4「选填缺口」谓词读它 |
tally: ['A','B','C'] |
分类列,给出取值域 | 域_对象_<列>_<值> 按值现数(没出现的值也铺 0) |
auto |
内部流水号,不问人、不让模型编 | 写入那一刻按「同组其余主键值-两位序号」生成(CU-9001-01);录入卡上那一格不出现,另摆一句「由系统按顺序生成」;件上「加一行」同规(2026-08-24):其余主键都填了、缺的恰好是 auto 列 → 基座续号落行,拒回也不点名人填不了的 auto 列 |
distinct |
这一列要数「有几个不同的值」(2026-08-24 加,E04 首用) | 域_对象_distinct_<列>(空值不算一种;零行铺 0)。覆盖闭合判据的落点:「每家候选客户都进了计划」= 计划_distinct_客户码 = 候选_rows —— 行数 ≠ 客户数,没有这个数,「盖没盖住每一家」就只能靠模型自觉(实测靠不住:人在确认卡上加的客户被从计划到收口无视了一整单) |
注意 expect 的边界:我方写的注(如备注列)不算 —— 标了它 _gaps 恒大于 0,
那条判据永远绿不了,门就废了。
- 一行还可以是一份资料文件(对象声明的
file段,2026-08-23 加,I01 首用):file: { titleColumn, bodyColumn, formatColumn }—— 正文一律 Markdown 原文 落在bodyColumn,格式列写md/html/docx/xlsx。基座按它现造真字节 (docx / xlsx 是自己打的 OOXML 包,Word / Excel 打得开),件上给资料名挂data-file-of="主键列"人就点得开(详见《HTML报告生成规范》§4)。 两条纪律: · 不另存一份文件 —— 资料的正身是台账那一行,另存即同一份事实两处各存一份; · 这一段同时是授权面,没声明的表点不开(基座 403),与「写看台账声明」同款。 配套把bodyColumn/formatColumn都标required:少一样,那一行就不是文件、 只是一行标题 —— 而_missing数得出来,收步判据与放行门自然拦得住。
3.2 基座读数(机检的唯一合法数源)
任务视图顶层永远铺着这些数,全部由基座现算,模型碰不着:
| 键 | 含义 | 备注 |
|---|---|---|
域_对象_rows |
本单在这张表写了几行 | 声明过的表铺 0(没写过 ≠ 写了 0 行 —— S-FEEL 未绑定名字参与比较为假,踩过) |
域_对象_keys |
本单写的行的主键名单(JSON) | 封顶 12 行;拒回时摆出来,「多的是哪一行」一比便知 |
域_对象_missing / _missing_keys |
缺必填几行 / 缺在哪几行哪一列 | 名单用中文列名 |
域_对象_gaps / _gaps_keys |
缺选填几行 / 哪几行 | 同上 |
域_对象_<列>_<值> |
该分类值现有几行 | 取值域声明给,数得全 |
机检纪律(最重要的一条,已被违反三次):
L1/L2 判据、收步判据、放行门哨兵,一律只许读基座数得出来的数。
活动自报的汇总列(line_total 这类)只能出现在等式的一边,或只做提示。
三次违反史,每次都是同一个死法 ——「那个数要模型自己重算,漏算/改小一次,门就废了」:
① missing_key_total 自报 → 人补了值门不开,十轮里三轮熔断;
② missing_minor_total 自报 → 门只接受「是」,模型把 1 改回 0,台账从此写着假数;
③ untyped_total 自报 → 模型跳过打标写个数就想过门(判据现读
_type_tag_a + _b + _c = _rows,两边都是基座出的数)。
3.3 门与闸(从起单到放行,一路的拦截点)
| 门 | 在哪 | 规则 |
|---|---|---|
| 起单宽进 | process.start |
不按 Schema 拒缺项。三活动 input.required 一律空(宽进严出),起单补全卡那条老路实际不再触发(彻底移除记 debt) |
| 执行卡提交闸 | 卡面 gate + 服务端 |
必填缺着按不下提交({ready,why,blanks},与放行门 L1 同一个数同一套算法);服务端在收下回传之前也把一遍(前端灰按钮只是给人看的)。看哪几张表三级取一(gateAddressesOf,有测钉着):①卡上绑了 blocks 看绑的;②卡自己声明的 gateLedgers(零输入卡该给的就是它 —— 只有发卡的人知道这一张要人补哪几张表);③回落到必交付物声明的 ledgers/ledger。三级全空才不给闸 —— 没有依据的时候不拦人。主数据类的数据工单必须给 gateLedgers:它要补的表(parts.*)不在必交付物名单里,不给就回落到一堆无关的表、一格不缺,件上红带点着缺项而提交钮照样绿(2026-08-27 W05 e2e 实测) |
提交门(submit 预检) |
engines | 机检(L1/L2)必须为真;裁量(L3/L4)裁过即可(裁成「不满足」照样能提交,裁断连理由随卡交给人 —— 一条只接受「是」的判据是一句教模型说谎的提示);L4 带谓词时谓词说了算(谓词真 = 已达标,模型那句相反的理由一并丢掉;谓词假才轮到裁量);必交付物按 ledger 地址数本单草稿行 |
| 放行门入场哨兵 | CMMN sentry | 必须有 ifPart,且读基座数、认「办完了没有」 —— 只有 onPart 的哨兵等于「有人写过一笔就开门」,模型写个 false 就推开了(实测:跳过三步直达放行门) |
| 熔断 | Harness | 连续 5 拍行动被拒 → 停问模型,发熔断卡把拒绝理由摆给人(比对削掉串位前缀、另开 60 条窗) |
拒回话术纪律(拒回的话是模型下一拍的行动指南,指错方向比不给指引更糟):
- 机检差数,先分清数不对(改汇总列)还是行不对(照主键名单 drop/补写);
- 判据点名的表这一单一行都没写(
_rows= 0)→ 直说「那一步还没办,回去办了再来」—— 这时数与行都是对的,指「改数/改行」是指向两扇打不开的门(实测五拍撞死); - 已收掉的项按状态分开说(「挂到当下这一步」+ 列出可为的项),别说「先 start」—— start 一个 Completed 项同样被拒,两堵墙对着撞。
3.4 人类工具:四类卡(补全卡已退役)
| 类 | 行动 | 形态与硬规矩 |
|---|---|---|
| 执行 | human + workOrder:'execution' |
卡 = 查看工单钮(四字粗体,与验收卡同形)+ ask + 作业指导书(steps+standard,与判据同一套版面、不画对勾)+ 依据 + 提交闸。零输入(2026-08-02 用户定):卡上没有任何填写 —— 不摆 fields、不绑 blocks,填报全在工作区的件上(人按「保存」基座直写草稿层,别再 write 一遍);空表单是常态,基座照发(从前的「表单不得为空」拒发已删)。指导书写「哪几格必须有、哪几格值得追」,不写「怎么核表」。steps/standard 是纯文本 —— 别写 ** 这类记号,它们不会变粗体,只会原样摆在人眼前 |
| 验收 | submit |
判据表(读时现算 —— 人在件上补一格判据跟着变;L5 不上卡:那张卡本身就是那道关,列进去等于摆一条恒红的项)+ 落地动作 + 可修订值 + 出口(算得出摆 routedPort 告知,算不出退 portChoice 让人选)。办结之后换成落地结果(landing):按交付物点名落了几行、转出哪一口、带过去哪些字段;一个口都没发直说「到此为止」 |
ask |
已全局退役(2026-08-02 用户定:「执行卡完全就是包含补全语义的人类工具」)—— 缺信息发零输入执行工单:指导书写清缺什么,人在工作区的件上补(保存即落草稿层)。旧输出里的 ask 由基座原地转成执行工单(归一不拒);引擎侧直发 ask 拒并指路 |
|
| 决策 | decide |
选择题(≥2 个选项,少了拒发);人点哪个进轨迹 |
| 录入 | entry |
人按下确认那一刻基座直接写库,AI 不经手 —— 建档这类主数据用它。可多段(also):建客户 = 公司档 + 联系人档,一张卡两段、确认一次落两行(值按 段号.列名 隔开)。卡面摆表的人读名(不摆 customer.profile);auto 列不进表单、另摆一句说明 |
通用规矩:
- 一单同一张卡只发一次。人交回来之后不论还缺什么都不许再发 —— 必填有提交闸兜着(卡回来时必填一定齐);人留空交回来 = 对方给不了,那也是一个答案: 如实记账、缺口照实留着、把这一步收掉往下走。
- 发卡即挂起,回传即续跑;在途验收先于一切。值怎么落:件上保存的、录入卡确认的, 基座直写;执行卡零输入(空答复 = 人照原样确认,痕上写明);决策卡选项进轨迹 —— 该记账的记账,别当没答过再发一张。
- 调人预算(
humanCallBudget)环外扣账。
3.5 视图渲染(概要,详见《HTML报告生成规范》)
- 模版活动持有,一单一模版贯穿始终(硬规矩);剧本每步的
view定推哪张。 - 渲染 = 纯取值绑定(
data-slot/data-rows/data-blank/data-class-of/data-col); 模版不算不判,基座算好的可以摆。 - 待处理那一屏报告即表单:可改格子渲孪生
<input>,默认锁定、按「编辑」才开, 能增删行;保存按地址落草稿层(授权面 = 台账声明),基座随即重渲(repush)—— 类徽、分布条、去向这些算出来的东西跟着变。 - 报告与卡的分工、同源读数、保存链路 —— 全在姊妹篇 §2,那是这套里最容易做错的一章。
3.6 进度条、日志、对话区
- phases 是给人看的刻度(引擎一眼不看):
{name, items[]}[], 除放行外每个计划项恰属一个阶段(漏一项,人办那件事时进度条已经走过去了 —— 实测:S3 漏在名单外,人对着审定卡而条上写着「判定去向」)。收口那一格由基座统一加 (「放行收口」),总格数 = 声明数 + 1。 - 日志三色:负责人绿 / 智能体蓝 / 系统灰;日志与卡面不摆系统内英文名 (状态名、表名、列名、口名一律换人读名)。
- 对话区一轮一句:
say只在交出控制权那一拍(ask/human/decide/entry/listen/submit) 才摆到人面前,别的拍进工作日志。到了要找人的那一拍,say 写「这一轮办完了什么、 为什么现在需要他」。顺利收尾对话区不再出一条(落地结果那一屏已说全); 没顺利收尾的出一条人话(「这一单受阻待人」)。
3.7 放行落地(人按「通过」之后发生什么)
草稿合并进正式版(逐表计数留结构化痕)→ 动作意向执行(幂等键;失败发落地失败卡,
任务 blocked)→ 输出口发射:载荷 = 已落地的事实(landedFacts,放行后草稿层已清,
不从作用域猜)→ released。selective 出口按各口 when 谓词序求值,首个命中即出 ——
去向是算出来的,不是人选的;人签的字确认的是算出来的那一口。
4. Loop:一拍怎么跑(Agent 视角)
4.1 局面里有什么
当前这一步的剧本(guide 整段摊开,其余步各留一行)+ 说明书(整单的事)+
活动记忆选摘 + 最近轨迹(含人答的值与工具出参)+ 当下可为(标「须你启动」/「裁量项」)+
进行中(有开着的活就不算断点)+ 目标缺口 + 待裁量前提 + 工具清单(带入参键名)+
可写台账字典(地址/主键/列/列型)+ 本单草稿(mine:*)+ 基座读数 + 任务数据
(摊平字段在前、整表殿后截断 —— 但剧本 reads 声明的表置前不截断)。
4.2 行动十四型(一拍一串,封顶 6)
| 行动 | 形状 | 语义 |
|---|---|---|
start |
{itemId} |
显式启动一项(可省:2026-08-02 起动它就算启动);已 Active / 已收掉再按 = 无事发生 |
plan |
{itemId} |
裁量项入场 |
disable |
{itemId, reason} |
此单不走这条 |
judge |
{criterion, satisfied, rationale} |
裁语义性前提 / L3-L4 判据;criterion 照判据名整句原样抄 |
write |
{itemId, address, rows:[{key,patch}]} |
写草稿。授权面是台账声明;itemId 只是记账,挂不上照写不拦;patch 只带有值的列;空转写(一行没变)即拒 |
drop |
{itemId, address, keys:[{主键}], reason} |
撤本单草稿层的行;主键与写进去时一字不差 |
tool |
{itemId, tool, input} |
读类即调;写类转意向;未命中转人挂起 |
ask |
基座原地转成零输入执行工单(字段名并进指导书);别再用 | |
entry |
{itemId, ask, ledger, entryKey, fields, also?} |
数据录入卡,挂起;确认那一下基座直写 |
decide |
{itemId, ask, options(≥2), basis} |
决策卡,挂起 |
human |
{itemId, workOrder, card{title,ask,steps?,standard?,basis}} |
执行/验收工单,挂起。执行卡零输入(2026-08-02 用户定):不摆 fields 不绑 blocks,填写全在工作区的件上(editGate + 孪生件),人按「保存」基座直写草稿层;必填由提交闸看住 —— 卡上加 gateLedgers:["域.对象"] 告诉闸这一张要拦哪几张表,不给才回落到必交付物声明 |
listen |
{itemId, eventType, keyFilter, deadline} |
立监听,挂起;时限必配 |
finish |
{itemId, rationale} |
宣告一项完成;非人工激活的项就地过 Active 再收 |
submit |
{rationale, fields?} |
提交放行 → 预检 → 发验收卡挂起 |
串的纪律(引擎强制):按序执行,谁被拒停在谁那儿(前缀已生效不回滚,理由喂回下一拍);
tool 之后截断(出参没看见,后面是闭着眼计划的);挂起型收拍;超封顶丢弃留痕。
拍级附带 say{text, tag, view?},不占行动位。
4.3 人工激活:动它就算启动(start 墙已拆)
标了 manualActivationRule 的项,模型显式动它(调工具/发卡/finish)就算启动,
不必单发 start(2026-08-02 拆墙)。它的分量在引擎那一侧,原样保留:
引擎不因哨兵满足替它启动(哨兵只推到 Available/Enabled),
也不给 doneWhen 替人收(自动收步跳过人工激活项)—— 收它只能是模型的 finish。
实测账(两轮,同一堵墙):先是「话与门同宽」—— 提示词写「先 start 再调工具」只说了
调工具,而门拦 tool/ask/human/decide/entry/finish 一整片,模型发 human 照字面漏了
start 白烧一拍;措辞写全之后仍然熔断(A1 一单连拒五拍,五次全是「要先 start」,
提示词与拒回都写着,它就是学不稳)。这才看清:那道墙保护不了任何东西 ——
start 是模型自己的动作,同一串连发即过,拦下只是逼它多演一步礼仪,
而学不稳的礼仪就是死法。状态机的礼仪不该长成墙(与 finish 就地过 Active、
start 已收项无事发生同族)。剧本里也别再教 start —— 教它白烧 token。
4.4 自动收步(doneWhen)
引擎每拍开头对任务视图求每一步的 doneWhen,真了就自己收那一项,不问模型
(与模型同拍撞车时:引擎收过了,模型这拍的 finish 当已完成,不抛非法迁移)。
人工激活的项不给 doneWhen —— 给了等于给引擎一把替人签字的钥匙。
5. 活动包怎么写
5.1 包结构
activities/<activity-id>/
├── package.json # @activities/<id>,private,type:module
├── src/
│ ├── index.ts # export default defineActivity({ name })
│ ├── manifest.ts # 清单六区 + 导出 DMN 常量
│ ├── case-model.ts # CMMN_XML + NARRATIVE + INTAKE_STEPS + INTAKE_PHASES
│ ├── skills.ts # Skill[]
│ └── views/{report.ts, prd-style.ts} # 报告模版(见姊妹篇)
├── __tests__/ # 自测;只准 import @sdk/@contracts/@sfeel
└── dist/ # pnpm activities:build 产出
5.2 清单六区要点(全量 schema 见 packages/contracts/src/manifest.ts)
identity:activityId / name / packageVersion(改内容必升)/ category / sdkVersion(精确相等)/ checksum。
contract 七件:
| 件 | 硬规矩 |
|---|---|
| ① input | 起单宽进;properties[k].title 写中文(补全卡上给人看的);inboundComm 声明哪几个入参字段合起来是那封来件 —— 基座起单那一刻归档进沟通记录 |
| ② output | mandatory 必发;selective 每组 ≥2 口、每口写 when 谓词(读台账列/基座读数)—— 放行时按序求值首个命中即出,算不出才退人选。载荷 = 已落地事实,别在出口另编载荷 |
| ③ requiredDeliverables | ledger 必写(预检数行);一份成果由几张表承载就写 ledgers(A1 的受理记录 = record + type_table + verdict 三张表、一份成果 —— 从前各占一条,人在成果面上看到三样东西);editableAtRelease 列出验收页上人可就地改的列(AI 判的结论列也要在其列,如 type_tag —— 人改它就是在推翻一次判定,那是他的职权) |
| ④ acceptanceCriteria | L1/L2 必带 predicate 且只读基座读数(§3.2);L4 可带谓词(谓词说了算,§3.3);措辞写全(「客户已确认:发件邮箱认出为老客户,或已为新客户建好档」——「(认出或已建档)」这种缩写人读不懂);内部一致性不上卡(打标依据对不对、两张表行数齐不齐 —— 该由收步判据看住,放行卡是给签字的人看的,一屏九条真要他过目的反而淹了);判据得能指向一个可做的动作(「去向合乎常理」这种让模型给自己没做的决定盖章的,删) |
| ⑤ revisableAtRelease | 放行时人可当场改的整单级值;拟值基座从台账现读,不让模型重述 |
| ⑥ prohibitions / authorization | 禁区短而硬;humanCallBudget 调人预算 |
| ⑦ resources | budget 按实测留一倍余量(A1:steps 60 / tokens 400k / 60min / 调人 4;实测常路 9-11 拍 8-13 万 token) |
declarations:台账(字段四标记,§3.1)、tools、models、listeners(时限必配)、harness。
assets:businessModel{cmmnXml, narrative, dmn, steps, phases} + skills + viewTemplates +
cardSchemas + distillHooks。steps 与 phases 都在这(见 §5.4、§3.6)。
samples ≥1 条(常路 + 最长路径都给上;input 键与 contract.input 对齐 ——
A1 是 customerEmail / subject / body / attachments)。
5.3 案例模型(CMMN)建模纪律 —— 每条都是实测账
- 图上有几步就是几项,不多建一件。 A1 第一版 6 步建成 12 项(自加阶段/里程碑/ 裁量项),模型全程跟 harness 较劲,一单二三十万 token 收不了口;扁平 7 项后零被拒。
- 案卷项 id 就是任务视图键名(入参与台账列摊平后的名字)。对不上 = 前提永不满足 = 第一个项都不 Enabled,而每一层单看都「正常」。
manualActivationRule只标触达世界与关键交付的项,且不给 doneWhen。 A1 标两项:S3_confirm(要打断真人)、T_release(签下去触达世界)。- 人的门要建成一项,不要挂成某一步收步条件里的半句。 「缺东西才叫人」的写法,
一封齐整的询价进来那半句当场为真,引擎自己收步,人一眼没看过就被叫去验收
(用户原话:「我都没确认需求 它就让我验收了」)。做法:独立一项 + 人工激活 +
不给 doneWhen + 下一步的入场前提取只有人的回复才产生的计数列(
confirmed_total)。 - 哨兵必须有 ifPart,且读基座数、认「办完了没有」(§3.3 放行门那行的教训)。
repetitionRule标常来回的项;判断菱形 = 两条互斥前提的 sentry,不是一个计划项。autoComplete="true";里程碑前提达成即达成;ifPart前缀semantic:交 judge。- 每个计划项(除放行)恰属一个 phase;放行收口那格基座加,活动别自己起名。
5.4 剧本(steps)—— 每步一条,引擎按它组织上下文
{ item: 'S2_normalize', // CMMN 计划项 id,对不上安装拒
intent: '一句话:这一步交出什么',
guide: `这一步的做法(只在轮到它时进上下文)`,
writes: ['quote_intake.record'], // 可写哪几张表
reads: ['customer.contact'], // 要读哪几张表 —— 声明了置前不截断(不声明会被盲截,
// 实测:客户联系人表落在刀口外,老客户被当成新客户)
tools: ['crm.history-price-read'],
doneWhen: 'line_total > 0 and quote_intake_record_rows = line_total',
view: 'intake-overview', // 这一步人看哪张件 —— 写清单时定死,不归模型临场挑
}
doneWhen 三坑:
X != null—— S-FEEL 未绑定名字!= null为 true,那一步开局即收。判「写过没有」 用正向式(X >= 0)或计数列;- 布尔列不当判据 —— 合法值含 false,分不清「没写」与「写了 false」。
取同步显式写下的计数列(
route_decided_total); - 收步判据与验收判据同源 —— 都读基座读数,比同一件事。松出来的那一截就是返工 (收了步却过不了验收,回头补写一张已收掉的表);同源还把发现点提前 (「多写一行」从第 10 拍提前到第 2 拍)。
5.5 说明书与 guide 写作纪律
说明书(narrative)只讲整单的事(开案、成品、SLA、口径);逐步做法归剧本 guide (两处各写一份会分叉出模型里不存在的项名,有测试钉死)。guide 写作十条:
- 规则写死,不留给模型判(隐藏思考已关换十倍速度,边界必须写到不可能读歪: 「关键字段只有 PN/EAU/交期」)。
- 写哪张表、哪些列,点名写死;人在哪一步答的东西谁负责落表,写清。
- 卡的骨架照抄给它(整段 JSON 例子,含
steps/standard/also/绑表的blocks)—— 散文描述不管用;系统提示词写死的形状盖过剧本,骨架里少一个键那个键就不存在。 - 一拍连做指引 + 常路拍数预期;工具批量入参,一次传完。
- 一单同一张卡只发一次;人留空 = 给不了(§3.4)。别写「还缺就再发一张卡」—— 提交闸上线后那条路不存在,模型照它办每次都撞「刚问过,人也答过了」(实测连烧四拍)。
- 绑台账的表别自己抄行;人提交后别再 write 一遍(值没变会被「写了等于没写」拒)。
- 回填按主键对行 —— 主键换个写法就是新开一行(实测同一颗料成了两行); 发现主键抄错交给人,别用新主键再写一行。
- 别替下游产出(§2.4);去向不用你选也不用人选,你只负责把判据数写对。
- 汇总列是你的活的,写明重算时机(人补完值你要把数改对);机检读数你改不动, 别为过判据去改自报数。
- 卡面是纯文本:
steps/standard/say 里别写**星号记号。
5.6 判定表(DMN)—— 能算的不问 LLM
单元格谓词 = S-FEEL('-' 任意、'"A","B"' 析取、'<= 90'、'[0..100]');
FIRST 的行序就是业务优先级(C 类 EOL 必须排最前);任一输入缺 → incomplete;
命中留痕、同输入必同结论、零 token。推荐做成 pure: true 的私有功能工具
(quote-intake.classify),模型只取齐判据、调工具拿结论;
出参键名与台账列同名(拿回直接 write)、连 counts 一起给(供照抄);
「没有」也当判据回(无历史 → historyDays: 9999),别回错误。
5.7 工具
声明式(名字进 declarations.tools,系统没有的运行时转人 —— 执行卡代办,
人填的值就是出参);批量入参;读类即调、写类转意向、未授权报错。
系统内化零声明即得:write/drop/ask/entry/decide/human/listen。
5.8 报告模版
见《HTML报告生成规范.md》。 那份是硬规矩:一单一模版贯穿始终、三个区各放什么、 报告与表单的关系、模版语言、编辑态、PRD 目标件平移法、陷阱表。
5.9 技能与记忆
技能 {id, label, applicability, method[], version}:常驻只有目录,正文按需加载,
载方法不载权力。方法里写锚点、纪律、批量惯例,不写产品文案。剧本改了口径,
技能里同一句也要跟着改(A1 定去向那条技能与 S6 同步删过开单指引)。
任务记忆只增;终态封卷后 distillHooks 蒸馏进活动记忆。
6. 自测与验收口径
6.1 包内自测(照 activities/quote-intake/__tests__/)
- 清单合规:过 schema;监听时限;L1/L2 有谓词且谓词名都在台账列或基座读数里; 必交付物都有 ledger;
- 案例模型:双表示同在;触达世界的项标人工激活且无 doneWhen; 每个计划项恰属一个 phase(除放行)、一项不进两个阶段; 剧本 item 与 CMMN 对得上;doneWhen 开局不为真(空局面求值必须 false);
- 判定表逐行求值;
- 对齐线:deliverable.ledger 在声明里;出口载荷标量有列承载; 剧本 view 都在 viewTemplates 里;
- 模版纪律(见姊妹篇 §9)。
6.2 五道门禁与安装两审
pnpm gates(typecheck / lint / depcruise / api:check / test),以退出码验。
安装静态审 error 即拒(黑名单见 §2.2;acceptance-level-misreported = L1/L2 没谓词)。
6.3 试跑口径与验收标准
- 试跑一律用 AI 账号(密码
jcai-dev-2026),绝不派到用户工作台; 测完pnpm --filter @app/api reset-runs。 - 三条常备案例(形状真不同,不是同一封信改个号): ① 老客户 · BOM 附件齐整 · 全 A 类直通;② 老客户 · 正文点名一条本轮不报(测人手撤行); ③ 新客户 · 中文正文列料 · 无附件 · 缺必填(测建档 + 人补格子)。
- 验收标准:三案例全部
released、行动被拒 0。 每一条「行动被拒」都是设计欠账 —— 修清单或剧本,不怪模型; 被拒理由指向死路(模型改不动的那种)是 harness 欠账,报上去。
6.5 设计律(2026-08-02/03 一整轮实测沉淀 —— 每条都付过学费)
排障表治的是「这一处坏了」;这几条治的是「为什么总在坏」。改 Harness、写活动、 做报告之前先过一遍:
- 墙要配路:任何一道门拦住一件事时,必须存在一个模型(或人)做得出的动作 把事办对 —— 没有出路的门只是死胡同(drop 之于「多写一行」、撤行之于「替代未果」)。
- 学不稳的礼仪就是死法:纯礼仪的墙一律拆(start 墙、空表单拒发)。 模型十次里学会九次不算学会 —— 第十次熔断的是用户的单。幂等优于拒回: 按钮按两下不该报错(start / finish / 重发已答卡,同族三例)。
- 拒回喂对抗,观察喂事实:拒回的话是模型下一拍的行动指南,指错方向比不给更糟; 头几次犯浑先转成 observation 指路(卡已答过→接着办交回后的事),连犯才算撞墙进熔断。
- 「机检只读基座数」管的是一整片:验收判据、收步判据、放行哨兵、入场哨兵、 提交闸、红带 —— 任何一处读模型自报的数,那道门就等一个可能永远不来的记账 (第四次违反藏在 s_parsed 哨兵里:行落了门却不开,模型被「依赖断点」推去发卡绕路)。
- 提示词分层:全局提示词只说通用真理,活动级细节归说明书 —— 一句全局禁令 (「别再 write 一遍」)能把所有活动的既有约定顶翻,且症状出在别的活动身上, 查起来像闹鬼。改全局面 = 改所有活动,回归也要按「所有活动 × 全部常备案例」跑。
- 措辞治不了的要结构治:模型「不查工具直接编结论」这类顽疾,说明书写死到头 仍概率性犯 —— 根治靠机制(规则进判定表由引擎求值 / 工具出参直接带结论只让照抄), 措辞只能降频。一条纪律连续两轮被同一形状踩穿,就该停止改话、开始改结构。
- 改共享面必须全量回归:提示词、turn-runner、解析器、容器 —— 动一处, 三活动的全部常备案例都要重跑(A1 新客户路的熔断就是「只回归了老客户路」漏出来的)。 「稳定的活动出错」比新功能不 work 伤得多。
- 用户重复提的要求,说明上一轮没打中病根:往下再挖一层,别在同一层再修一遍 (「工作成果 1:1」挖了三层才到「办结那一刻存快照」这个真解)。
7. 排障速查表(症状 → 根因 → 处置;全部实测账)
| 症状 | 根因 | 处置 |
|---|---|---|
| 起单后第一个项都不 Enabled | 案卷项 id 不是任务视图键名 | §5.3-2 |
| 连续「迁移非法/项不存在」熔断 | 计划项建多了/嵌套了 | §5.3-1 扁平化 |
| 一单 20+ 拍才收口 | 没写一拍连做/拍数预期;或项过多 | §5.5-4 |
| 机检判据全绿而交付物是错的 | 等式两边都由活动自报 | 一边改读基座读数(§3.2) |
| 一条 L1/L2 永远绿不了而数据合规 | 读了自报汇总列 | 标 required,读 _missing(§3.2 违反史①) |
| 台账里某个数被改小、判据变绿 | 门只接受「是」,模型去改自报数 | 机检只读基座数;数不出的降 L3/L4(违反史②) |
| 没打标就进了放行门 | 哨兵只有 onPart(写过一笔就开门) | ifPart 读基座数认「办完没有」(§3.3) |
| 同一条理由连拒到熔断而数与行都对 | 拒回把「活没干完」说成「数/行不对」 | 已修:_rows=0 直说那一步没办 |
| 行数对不上,模型只会改数字 | 拒回没摆主键名单 | 已修:_keys 名单;照名单 drop |
| 「要先 start」而 start 也被拒 | 拒回没按状态分开说 | 已修;start 已收的项无事发生 |
| 「要先 start」连拒五拍熔断(措辞已写全) | start 墙本身:学不稳的礼仪就是死法 | 已拆墙(2026-08-02):动它就算启动(§4.3) |
| 零输入执行卡被「表单不得为空」拒发到熔断 | 基座旧机检把表单当交付通道 | 已删该拒发:空表单是常态(卡族表「执行」行) |
| 整拍六个动作全对却退化成「需要你判断下一步」 | 模型多吐一个闭括号,单数花括号的配平切出半截 | 已修:括号栈 + 丢不配对闭括号(parseJsonLoose,有测) |
| 「需要你判断下一步」卡面摆着一坨 JSON 原话 | 整拍输出没成形,旧兜底一次就发卡还把原话上卡 | 已修(2026-08-03):就地重问一次、原话归工作日志、卡面只说人话(agent-runner,有测) |
| 模型逐字复读一句被明令禁止的话 | 反例把原话引进了提示词(「别说『…』」)—— 关思考的模型不读否定框,只认框里现成的句子 | 反例只描述、不引原话;要引原话就引正例(想听哪句就示范哪句)。A1「照历史价估」逮过(2026-08-03) |
| 执行卡上作业指导书整个是空的(basis 里是「当下可为(空)/目标缺口」这类家具) | 卡骨架写在那一步的剧本里,而模型在「干活+收步+派卡」压成的一拍里发了它 —— 那一拍该步剧本没摊开,模型手上没有骨架,凭记忆拼出裸卡,基座兜底 basis 又摆上局面家具 | 凡要模型照抄的卡骨架,放说明书不放步骤剧本(说明书每拍都在;A6 决策卡先立此法,A1 审定卡 2026-08-03 同坑)。另:steps/standard 已补进出参 schema —— 从前能到卡面纯靠厂商 JSON 模式宽松 |
| 人答完后同一条拒回重复五遍熔断(「这一步已办完,不能再往它上面挂活」) | 重发判断走在站上场之后 —— 人答完那一步常已收掉,readyToWork 先抛,于是「重发已答过的卡 = 无事发生」这条活路在最需要时够不着 |
已修(2026-08-03):先判重发再站上场;重发一张已答过的卡本就不需要那一项在场(墙要配路,有测) |
同一个岔路口连发四张决策卡,调人预算烧光落 exhausted |
决策卡没有幂等兜底(执行工单一直有) | 已修:resendVerdict 两族共用 —— 头两次无事发生、第三次起算被拒 |
| 收上一步时连拒「写了等于没写」 | 值已在台账(件上保存/上一拍写过),模型照抄重写;或提前干了下一步的活(如在 S2 查行情) | 拒回已带指南「该办下一件事」;剧本明写这一拍只有哪几件事 |
| 人答了卡,模型又发一张一样的 | 剧本残留「还缺就再发一张卡」死路 | §5.5-5:一单一卡;必填有闸兜着 |
| 人在卡上填了值,台账还是空的 | doneWhen 写成「问过就算完」 | 等「答案落了表」的计数(answered_total) |
| 该经过人的一步整个没发生 | 「经过人」写成了收步条件里的半句 | §5.3-4:独立一项+人工激活 |
| 人改了表,分布条/去向不动 | 汇总数没人重算;或保存没落库 | 分类列标 tally 由基座数;保存走件上按地址那条路(姊妹篇 §6) |
| 人在件上保存,一格都没落 | 保存借道当下那张卡的 blocks(熔断卡没有 blocks) | 已修:按 data-edit 地址收,授权=台账声明 |
| L4 判错(人填了它说没填) | 整条只读裁断,模型凭记忆写理由 | L4 带谓词,谓词说了算(§3.3);expect 别标我方写的注 |
| 判据卡面与提交门说的不一样 | 两处各算各的 | 已修:读时现算,同一段 criterionCheck |
| 进度条在人办事时已走过那一格 | phases 漏了那一项 | §3.6:每项恰属一格,自测钉死 |
| 收了步却过不了验收 | 收步判据比验收松 | §5.4:同源 |
| 全 0 判据过不去连撞熔断 | 没写过 ≠ 写了 0 行 | 已修:声明过的表铺 0 |
| 补全卡答了数据没进台账 | 系统不替 Agent 写 | §5.5:答案要自己 write(绑表除外) |
| 同一颗料成果表里两行 | 主键换写法 = 新开一行 | §5.5-7 |
| 数值列被写成 0 表示「没有」 | 模型拿 0 当空 | 写死:没有给 ""(归型不写这一列) |
| 制表符 BOM 拆错列 | 连续 \t 是中间格空 |
附件 rows 有值照它拆,别自己数制表符 |
| 工作区画面不换 / 推错张 | view 名不在模版里 / 剧本没写 view | §5.4 view 定死 |
| 归档件被冲成别的屏 | 收口后按固定名推退到第一张 | 已修:按剧本推,活步收完不推 |
| 消息被「无主入站待认领」刷屏 | 台账 data 事件进了兜底 | 已修:只有 source.kind==='channel' 才兜 |
| 下游网关取不到值 | 出口标量没有台账列承载 | §5.2-② |
| 改了包重装没生效 | 同版重装 no-op | 升 packageVersion |
| 收口前一两拍预算耗尽 | budget 给太紧 | §5.2-⑦ 留一倍余量 |
| 代答脚本交执行卡被拒「必填字段未回填:结果」 | 卡没声明 fields 时基座兜底加必填「结果」回执字段 |
交卷 values.result 带一句 |
| 验收 pass 后落地判「选择组未选出口」 | routedPort 只在前端回传里顶上(已修:服务端发事件前算有效口) | 升级基座;临时:回传自带 selectedPorts |
8. 待实现与已知不一致
| # | 事项 | 现状 | 目标 |
|---|---|---|---|
| ④ | 受理分支误吞验收裁决 | deliver 的受理分支判 awaiting-human && human 不看 cardId |
按补全卡 cardId 匹配;开发侧规避:带缺口开跑的单尽早补齐 |
| ⑤ | 治理台白名单授权口 | 只有种子常量表 | 治理台按活动配白名单 |
| ⑦ | dist 真编译 | index.js 是源码文本拷贝(债 M5-3) |
build-activities 真编译 |
| ⑧ | 单拍工具调用缝 | 调用经 ToolInvoker 未走总线编译 | 开环转正或接总线 |
| ⑨ | preauthorizedActions 死代码 |
无调用方 | manifest 里不要写 |
| ⑩ | SchemaValidator 双死依赖 |
声明了不再调 | 清理 |
| ⑪ | 评价卡(rate) | schema 在、链路未接 | 用户定:暂不做 |
| ⑫ | judge 还没裁各花一拍 |
待裁量判据不在单拍局面里,撞拒回才知道 | 把「还没裁的判据」进局面(结构改动,未做) |
小不一致(不阻塞):declarations.tools[].version 无消费者;contract.resources.tools
双份声明;replay 跳过白名单;工具 retries 不重试;转人答案不经出参 Schema 校验(债 M6-1)。