dingclaw 0.4.1__tar.gz
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.
- dingclaw-0.4.1/CHANGELOG.md +845 -0
- dingclaw-0.4.1/LICENSE +21 -0
- dingclaw-0.4.1/MANIFEST.in +4 -0
- dingclaw-0.4.1/PKG-INFO +521 -0
- dingclaw-0.4.1/README.md +497 -0
- dingclaw-0.4.1/docs//344/272/244/344/273/230/346/211/213/345/206/214.md +502 -0
- dingclaw-0.4.1/docs//351/205/215/347/275/256/345/217/202/350/200/203.md +385 -0
- dingclaw-0.4.1/pyproject.toml +66 -0
- dingclaw-0.4.1/setup.cfg +4 -0
- dingclaw-0.4.1/src/dingclaw/__init__.py +17 -0
- dingclaw-0.4.1/src/dingclaw/agent.py +665 -0
- dingclaw-0.4.1/src/dingclaw/agent_skill/SKILL.md +67 -0
- dingclaw-0.4.1/src/dingclaw/agent_skill/agents/openai.yaml +7 -0
- dingclaw-0.4.1/src/dingclaw/agent_templates.py +244 -0
- dingclaw-0.4.1/src/dingclaw/artifact_check.py +560 -0
- dingclaw-0.4.1/src/dingclaw/batching.py +143 -0
- dingclaw-0.4.1/src/dingclaw/blacklist.py +135 -0
- dingclaw-0.4.1/src/dingclaw/bundle_reader.py +109 -0
- dingclaw-0.4.1/src/dingclaw/chat_read.py +463 -0
- dingclaw-0.4.1/src/dingclaw/chat_write.py +399 -0
- dingclaw-0.4.1/src/dingclaw/cli.py +4008 -0
- dingclaw-0.4.1/src/dingclaw/compensation.py +486 -0
- dingclaw-0.4.1/src/dingclaw/compensation_models.py +116 -0
- dingclaw-0.4.1/src/dingclaw/compensation_state.py +683 -0
- dingclaw-0.4.1/src/dingclaw/config.py +1545 -0
- dingclaw-0.4.1/src/dingclaw/config_edit.py +263 -0
- dingclaw-0.4.1/src/dingclaw/context.py +153 -0
- dingclaw-0.4.1/src/dingclaw/context_store.py +769 -0
- dingclaw-0.4.1/src/dingclaw/contract_probe.py +573 -0
- dingclaw-0.4.1/src/dingclaw/ding_inbox.py +302 -0
- dingclaw-0.4.1/src/dingclaw/ding_notifier.py +548 -0
- dingclaw-0.4.1/src/dingclaw/ding_verdict.schema.json +21 -0
- dingclaw-0.4.1/src/dingclaw/dws_client.py +712 -0
- dingclaw-0.4.1/src/dingclaw/dws_parser.py +496 -0
- dingclaw-0.4.1/src/dingclaw/dws_prewarm.py +135 -0
- dingclaw-0.4.1/src/dingclaw/event_listener.py +584 -0
- dingclaw-0.4.1/src/dingclaw/event_rearm.py +640 -0
- dingclaw-0.4.1/src/dingclaw/home_template/AGENTS.md +47 -0
- dingclaw-0.4.1/src/dingclaw/home_template/IDENTITY.md +27 -0
- dingclaw-0.4.1/src/dingclaw/home_template/README.md +43 -0
- dingclaw-0.4.1/src/dingclaw/home_template/SOUL.md +34 -0
- dingclaw-0.4.1/src/dingclaw/home_template/bin/dingclaw +140 -0
- dingclaw-0.4.1/src/dingclaw/home_template/knowledge/people.md +40 -0
- dingclaw-0.4.1/src/dingclaw/home_template/skill/SKILL.md +32 -0
- dingclaw-0.4.1/src/dingclaw/home_template/skill/references//350/203/275/345/212/233/347/224/273/345/203/217.md +24 -0
- dingclaw-0.4.1/src/dingclaw/home_template/skill/references//350/257/201/346/215/256/344/270/216/346/235/203/351/231/220/351/227/250/347/246/201.md +29 -0
- dingclaw-0.4.1/src/dingclaw/inbox_skill/SKILL.md +103 -0
- dingclaw-0.4.1/src/dingclaw/inbox_skill/agents/openai.yaml +7 -0
- dingclaw-0.4.1/src/dingclaw/inbox_view.py +451 -0
- dingclaw-0.4.1/src/dingclaw/legacy_delivery_state.py +101 -0
- dingclaw-0.4.1/src/dingclaw/live_events.py +517 -0
- dingclaw-0.4.1/src/dingclaw/media.py +584 -0
- dingclaw-0.4.1/src/dingclaw/message_render.py +306 -0
- dingclaw-0.4.1/src/dingclaw/models.py +442 -0
- dingclaw-0.4.1/src/dingclaw/onboarding.py +665 -0
- dingclaw-0.4.1/src/dingclaw/owner_summon_probe.py +258 -0
- dingclaw-0.4.1/src/dingclaw/panel_contacts.py +634 -0
- dingclaw-0.4.1/src/dingclaw/panel_groups.py +397 -0
- dingclaw-0.4.1/src/dingclaw/panel_write.py +271 -0
- dingclaw-0.4.1/src/dingclaw/policy.py +187 -0
- dingclaw-0.4.1/src/dingclaw/preflight.py +356 -0
- dingclaw-0.4.1/src/dingclaw/presence.py +667 -0
- dingclaw-0.4.1/src/dingclaw/presence_ack.py +737 -0
- dingclaw-0.4.1/src/dingclaw/prompt_composer.py +253 -0
- dingclaw-0.4.1/src/dingclaw/readiness_check.py +211 -0
- dingclaw-0.4.1/src/dingclaw/reply.schema.json +13 -0
- dingclaw-0.4.1/src/dingclaw/reply_executor.py +303 -0
- dingclaw-0.4.1/src/dingclaw/reply_recovery.py +121 -0
- dingclaw-0.4.1/src/dingclaw/rpc_methods.py +1536 -0
- dingclaw-0.4.1/src/dingclaw/rpc_schema.py +274 -0
- dingclaw-0.4.1/src/dingclaw/rpc_server.py +450 -0
- dingclaw-0.4.1/src/dingclaw/runtime.py +81 -0
- dingclaw-0.4.1/src/dingclaw/self_command.py +330 -0
- dingclaw-0.4.1/src/dingclaw/sender.py +784 -0
- dingclaw-0.4.1/src/dingclaw/service.py +3053 -0
- dingclaw-0.4.1/src/dingclaw/skill_install.py +285 -0
- dingclaw-0.4.1/src/dingclaw/state.py +2115 -0
- dingclaw-0.4.1/src/dingclaw/state_codec.py +171 -0
- dingclaw-0.4.1/src/dingclaw/state_coordination.py +180 -0
- dingclaw-0.4.1/src/dingclaw/state_errors.py +25 -0
- dingclaw-0.4.1/src/dingclaw/state_schema.py +515 -0
- dingclaw-0.4.1/src/dingclaw/summarize.py +267 -0
- dingclaw-0.4.1/src/dingclaw/summary.schema.json +21 -0
- dingclaw-0.4.1/src/dingclaw/tui.py +496 -0
- dingclaw-0.4.1/src/dingclaw/tui_app/THIRD_PARTY_LICENSES.txt +672 -0
- dingclaw-0.4.1/src/dingclaw/tui_app/tui.mjs +19625 -0
- dingclaw-0.4.1/src/dingclaw/typewriter.py +96 -0
- dingclaw-0.4.1/src/dingclaw/unread_gate.py +275 -0
- dingclaw-0.4.1/src/dingclaw/warm_agent_pool.py +423 -0
- dingclaw-0.4.1/src/dingclaw.egg-info/PKG-INFO +521 -0
- dingclaw-0.4.1/src/dingclaw.egg-info/SOURCES.txt +93 -0
- dingclaw-0.4.1/src/dingclaw.egg-info/dependency_links.txt +1 -0
- dingclaw-0.4.1/src/dingclaw.egg-info/entry_points.txt +2 -0
- dingclaw-0.4.1/src/dingclaw.egg-info/requires.txt +7 -0
- dingclaw-0.4.1/src/dingclaw.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,845 @@
|
|
|
1
|
+
# 变更记录
|
|
2
|
+
|
|
3
|
+
本文件记录 dingclaw 每个交付版本的变化。格式参考 Keep a Changelog,版本号遵循语义化版本。
|
|
4
|
+
|
|
5
|
+
## [未发布]
|
|
6
|
+
|
|
7
|
+
## [0.4.1] - 2026-08-25
|
|
8
|
+
|
|
9
|
+
### 发布
|
|
10
|
+
|
|
11
|
+
- 项目许可证改为 MIT,并把许可证文件、SPDX 元数据、公开 PyPI 安装说明和发行分类写入
|
|
12
|
+
Python 包元数据;TUI 构建同时生成并随 wheel 携带运行时依赖的第三方许可证汇总。
|
|
13
|
+
- DWS 获取方式改为引用钉钉开放平台官网及 `DingTalk-Real-AI/dingtalk-workspace-cli`
|
|
14
|
+
官方仓库,补充国内 Gitee 与 npmmirror 安装路径。
|
|
15
|
+
- 公开发行版本统一为 `0.4.1`;同一组 wheel / sdist 可上传 PyPI 并同步到云效 Packages,
|
|
16
|
+
云效继续作为需要成员凭据的企业镜像,不宣称提供匿名公网下载。
|
|
17
|
+
- 制品纯净度门禁新增 MIT 元数据、许可证文件、访问令牌、私钥、带密码 URL 和阿里内部域名
|
|
18
|
+
检查;源码包同时携带正式文档、许可证与变更记录。
|
|
19
|
+
- TUI 第三方许可证清单改由 esbuild 的真实 bundle 输入生成,嵌套安装的同名多版本依赖
|
|
20
|
+
不再被按包名错误去重;正式打包新增 `--release`,强制干净工作区、精确版本标签和净室安装。
|
|
21
|
+
|
|
22
|
+
## [0.4.0] - 2026-08-25
|
|
23
|
+
|
|
24
|
+
三条近同时的召唤把两个问题一起暴露出来:第三条排队 75 秒才有回音(三层串行),
|
|
25
|
+
而同事单聊里的召唤,回复落进了主人自己的自聊(大小写归一化不对称)。这一版把
|
|
26
|
+
回复管线并行化、把 claude 冷启动预热掉、把那次错发修死。
|
|
27
|
+
|
|
28
|
+
### 发布
|
|
29
|
+
|
|
30
|
+
- 版本统一为 `0.4.0`,新增 `dingclawd --version`,TUI 启动时同时校验协议版本和
|
|
31
|
+
应用版本,避免旧面板与新内核混装。
|
|
32
|
+
- 发布脚本新增 Python/TUI 覆盖率、类型检查、依赖审计、包元数据、纯净度与干净安装
|
|
33
|
+
门禁;生成 `SHA256SUMS`,并标准化 wheel、sdist 和交付包元数据以支持可复现构建。
|
|
34
|
+
- 前端测试与构建依赖升级到无已知审计漏洞的版本;Proprietary 制品按私有 PyPI
|
|
35
|
+
分发,明确禁止上传公开 PyPI/TestPyPI。
|
|
36
|
+
|
|
37
|
+
### 修复
|
|
38
|
+
|
|
39
|
+
- **主人自聊的即时回执不再把可靠性押在单条 Stream 订阅上。**新增缺省关闭的
|
|
40
|
+
`presence_owner_summon_probe_interval_seconds`(0 或 2–60):仅重叠读取主人自聊、
|
|
41
|
+
仅接受 `@me <任务>`,命中后复用原即时回执队列并唤醒权威轮次,不从历史载荷执行
|
|
42
|
+
任务。ACK 日志新增 `source/event_id/conversation_id/message_id`;轮次把主人单聊的
|
|
43
|
+
fresh mark 单独计数,一次漏推只重建 owner-o2o consumer,其他订阅的流量不再掩盖它,
|
|
44
|
+
也不连带重启全部 consumer。探针读取单次 5 秒止损且不做 `--verbose` 原地重试;失败
|
|
45
|
+
后由下一间隔的新进程重读,避免一个 CLI 启动抖动把无回执窗口从 5 秒放大成 10 秒。
|
|
46
|
+
- **launchd 任务不再以 Background 类型安装。**`ProcessType=Background` 会让守护进程连同它
|
|
47
|
+
派生的一切——14 个 dws consumer、它们 fork 出来的事件 bus、每一次 add-text-emotion 子进程
|
|
48
|
+
——都跑在 darwin-bg 节流档(`ps` 里 PRI=4,而普通 shell 是 31)。2026-08-24 深夜机器被终端
|
|
49
|
+
安全扫描压到负载 50-100 时实测:节流档的 bus 把服务端 `deliveredAt` 之后 55-111 秒才把事件
|
|
50
|
+
递给 consumer(同一条事件带着不同 event_id 重投 3 次,说明 ack 也慢到触发服务端重试),
|
|
51
|
+
守护进程自己的 `dws --version` 超时 30 秒而前台 shell 只要 0.7 秒,轮询侧连 `auth status`
|
|
52
|
+
预检都失败、整轮按未登录跳过;而主人手打 `@自己 现在几点` 的 ⌛ 用了 9.9 秒——其中 8.4 秒
|
|
53
|
+
花在事件抵达守护进程之前,dws 调用本身只占 1.5 秒。先改为 `Standard`,实测只拿到
|
|
54
|
+
PRI 20(utility 档,与终端安全扫描器的工作进程同档),负载 150 下 ⌛ 的 dws 调用仍
|
|
55
|
+
撑不过预算,最终定为 **`Interactive`**——与主人自己的前台应用同档;agent 子进程同样
|
|
56
|
+
继承。**已安装的 plist 不会自动改**:升级后需重新生成或手改 `ProcessType`
|
|
57
|
+
并 bootout/bootstrap 一次。
|
|
58
|
+
- **⌛ 秒贴不再因为 10 秒硬上限而整条消失。**引擎给 add-text-emotion 的预算原来写死
|
|
59
|
+
10s("超过就算输了"),但它真正的对手是负载下 36–62s 的轮次,而 dws 进程在负载 150
|
|
60
|
+
时光初始化就要十几秒(2026-08-25 实测两条 App 手打召唤全部 `add-text-emotion
|
|
61
|
+
declined`,dws.log 里连 command_start 都没有)。预算改为可配
|
|
62
|
+
`presence_ack_timeout_seconds`(缺省 30,范围 5–120);失败行 `presence_ack_failed`
|
|
63
|
+
现在带 `reason`(`timeout` / `unavailable` / `cancelled` / `exit:<退出码>:<输出摘要>` /
|
|
64
|
+
`error:…` / `declined[:服务端错误码]`)、同成功行的 `push_ms / queue_ms / add_ms /
|
|
65
|
+
attempts` 四段、`row`(登记行的下场:`released` / `retained` / `taken_over` /
|
|
66
|
+
`gone`)和 `taken_over`(轮次已接管该行并自己贴上);引擎自身异常是另一种形状
|
|
67
|
+
`reason=engine_error`,只带 `error` 文本,没有四段与 `row`,每分钟至多一行、带
|
|
68
|
+
累计 `total`。
|
|
69
|
+
- **轮次不再"收养"一枚并不存在的贴纸。**`presence_acks` 新增 `landed` 位(旧库打开时
|
|
70
|
+
自动加列,历史行按已落地处理):引擎写前登记时为 0,dws 调用成功才置 1。轮次
|
|
71
|
+
reserve 遇到 0 不再当作贴纸已在,而是自己贴并接管该行(新轮结果键
|
|
72
|
+
`presence_acks_taken_over`,与 `presence_acks_adopted` 分开计;接管不算
|
|
73
|
+
`presence_marks_fresh`——事件明明到了,只是秒贴输了竞速);引擎失败只在服务端明确
|
|
74
|
+
拒绝时释放行——超时/进程死亡说不清贴纸有没有先落地,行保留为 0 交给轮次接管,回复
|
|
75
|
+
落地时两枚一并撤掉,若行已被撤回/清扫(`row=gone`)则顺手撤一次以防贴纸其实落了
|
|
76
|
+
地;被接管的行留给轮次收尾(失败行上 `row=taken_over`);成功后发现
|
|
77
|
+
行已被撤回/清扫则把刚贴的贴纸再撤掉(`presence_ack_taken_down reason=row_gone`)。
|
|
78
|
+
轮次自己贴失败时只删自己插入的行,接管来的行保留(引擎的调用可能还会落上去;
|
|
79
|
+
登记表读不了时也不删)。引擎撤回失败(`presence_ack_taken_down removed=false`)时
|
|
80
|
+
把行按已落地态写回,交给 TTL 清扫兜底;接管时 `acked_at` 刷新为当下,TTL 从轮次
|
|
81
|
+
自己的贴纸起算;GENERATING 碰撞既不撤贴纸也不动行(对方的终态会连同本批次的行
|
|
82
|
+
一起收尾)。未落地行在兄弟撤回与 TTL 清扫里不再同步调 dws——负载下每枚多半不存在
|
|
83
|
+
的贴纸都要再吃一个超时,正好堵在 ⌛ 的关键路径上——而是删行后交给修复队列异步撤
|
|
84
|
+
(每轮限量),同步撤回失败也入同一队列。此前引擎一超时,这条消息直到 ✅ 之前都
|
|
85
|
+
没有任何 ⌛。
|
|
86
|
+
- **贴纸 add 允许对瞬时错误有界重试。**实测(2026-08-25)同一描述符对同一消息连贴两次
|
|
87
|
+
只留一枚、一次 remove 即清——服务端按(消息 × 用户 × 表情)去重,"重试会叠贴"的旧
|
|
88
|
+
假设不成立。`DwsEmotionClient` 新增 `try_add`(带原因与次数)与 `add_retries`;
|
|
89
|
+
`presence_react_retries` 现在接到轮次侧所有 client 的 remove 上(此前该键根本没接到
|
|
90
|
+
轮次侧,一直跑构造默认值 1),add 的瞬时失败(`timeout` / `exit:…`)只在轮次
|
|
91
|
+
annotator 的贴纸(⌛ 与 ✅)上重试——AI 披露角标在回复的关键路径上、探针与清扫器
|
|
92
|
+
本就一次性,都不重试;
|
|
93
|
+
服务端明确拒绝、二进制起不来不重试;引擎侧 add 与 remove 一律不重试(单 worker,
|
|
94
|
+
一次重试会把后面每个事件的 ⌛ 都拖住一个预算,失败由轮次兜底)。
|
|
95
|
+
- **秒回引擎打开状态库时执行与轮次相同的权限检查。**引擎可能是新库的第一个写入者
|
|
96
|
+
(轮次首轮之前就来了事件);现在同样以 0600 创建、拒绝非属主独占的库,初始化失败
|
|
97
|
+
不缓存半成品连接,下一条事件从头再来。
|
|
98
|
+
- **`presence_ack` 日志行带上耗时拆分。**除既有 `latency_ms`(消息时间→贴纸落地)外新增
|
|
99
|
+
`push_ms`(消息时间→事件行离开 consumer 管道,即守护进程之外的部分)、`queue_ms`
|
|
100
|
+
(到达→引擎开始处理)、`add_ms`(add-text-emotion 子进程本身)。此前一条 9.9 秒的贴纸
|
|
101
|
+
只能靠翻 dws 的 debug 日志做减法才知道钱花在哪;这三个数让下一次报告不再需要取证。
|
|
102
|
+
- **事件订阅"登记在案却不投递"的静默失聪现在会自愈。**2026-08-24 实测:重启后在
|
|
103
|
+
温 socket 上重建的 14 条订阅 sublist 全绿、consumer 全部健在,却 18 分钟一条不投,
|
|
104
|
+
既有的 cold_socket / source_reconnect 两条重弹规则都看不见这种形状,秒贴 ⌛ 全程
|
|
105
|
+
退化到轮询路径(约 10 秒)。新增第三条**投递活性**规则:轮次不得不自己首贴 ⌛
|
|
106
|
+
(新轮结果键 `presence_marks_fresh`)而 listener 累计投递数纹丝不动,记一次证据;
|
|
107
|
+
证据跨静默轮保持、任何投递清零,攒满 2 次弹跳全部 consumer 重建订阅
|
|
108
|
+
(`event_listener_rearm` 新 `reason=delivery_liveness`),冷却 15 分钟且冷却内
|
|
109
|
+
证据作废。只在秒回引擎武装时生效——引擎不在场时轮次首贴是常态而非证据;重试
|
|
110
|
+
复活的补贴纸(adopted 再标)不计证据,模型侧故障不会被翻译成事件通道弹跳。
|
|
111
|
+
- **同事单聊里的召唤,回复回到召唤发生的会话。**根因一行:`latest_counterpart_open_id`
|
|
112
|
+
拿数据库里原始大小写的 open id 与 policy casefold 过的排除集合直接比对,主人自己
|
|
113
|
+
永远不被排除,被当成"对端"返回,回复按人寻址发给了主人自己——自聊,恰好是召唤
|
|
114
|
+
没有发生的那个会话。比对两侧统一 casefold(返回值保持原始大小写供寻址),回归
|
|
115
|
+
测试用混合大小写 id 钉死。2026-08-24 上午的错发即此。
|
|
116
|
+
- **回复能正确报时。**prompt 从不含当前墙钟,模型只能从上下文时间戳猜"现在",实测
|
|
117
|
+
偏差 13 分钟。请求构建时注入 `当前时间`(东八区 ISO8601)到可信指令帧;request
|
|
118
|
+
hash 与 job 幂等不含该字段,重试仍归并为同一任务。
|
|
119
|
+
|
|
120
|
+
### 新增
|
|
121
|
+
|
|
122
|
+
- **秒回引擎的丢弃可观测。**引擎十二个静默闸门(过期、去重、召唤无窗口、登记竞败等)
|
|
123
|
+
此前在日志里与"没收到事件"不可区分,上一次排障只能靠 bus 侧计数反推。现在每个
|
|
124
|
+
闸门按 reason 计数并限频上报 `presence_ack_skipped`(引擎级汇总:同一 reason 每
|
|
125
|
+
分钟至多一行,带累计 total,差值即静默期内的次数)。
|
|
126
|
+
- **dws 预热。**每次 add-text-emotion 都是新起一个 dws 子进程,实测冷启 ~4.7s、
|
|
127
|
+
温启 ~1s——冷启罚金恰好落在每次部署后的第一张贴纸上。daemon 启动即跑一次
|
|
128
|
+
`dws --version` 并每 20 分钟重复(不碰网络与认证态、剥离发送侧 secret),日志
|
|
129
|
+
一行 `dws_prewarm`(duration_ms 兼作 dws 劣化的前置信号);失败或非零退出记
|
|
130
|
+
`dws_prewarm_failed`,预热失效不影响任何正确性。
|
|
131
|
+
- **跨会话并行回复(`reply_parallel_enabled`,缺省开)。**轮循环只做发现与调度,
|
|
132
|
+
生成+投递交给进程级 worker 池:同会话严格 FIFO 保序,跨会话并行(`reply_workers`
|
|
133
|
+
缺省 3),召唤不再在选择循环里就地执行。防重三层:执行器按 dedup id 拒重、
|
|
134
|
+
messages/job 两把既有 CAS、以及新的 `reply_admissions` 心跳表——排队中的工作在库
|
|
135
|
+
里可见,三个恢复入口(滞留复活、弃置任务回收、SENDING 清扫)一律绕开心跳新鲜的
|
|
136
|
+
admission,跨进程的一次性 poll 也看得见;进程崩溃心跳停摆三分钟后恢复照常接管。
|
|
137
|
+
SENDING 清扫从零判据改为按 `updated_on` 加宽限(随 send_timeout 推导),分段发送
|
|
138
|
+
逐段重盖时间戳。行为可见变化:轮日志的 `select` 不再包含 agent/deliver 时长
|
|
139
|
+
(骤降属预期),新增 `submitted` 计数与逐任务 `reply_job_done` 日志行
|
|
140
|
+
(worker/agent/deliver 毫秒、outcome);轮结果里的 `agent_calls`/`failed`/
|
|
141
|
+
`send_attempts`/`accepted_sends`/`unknown_sends` 在并行模式下归零——这些事实
|
|
142
|
+
迁移到了 per-job 行,基于轮计数做的告警要跟着搬。近距双消息由"合并一条回复"
|
|
143
|
+
变为"各自一条回复"。关闭开关即回到完全内联的旧路径。
|
|
144
|
+
- **claude 预热池(`agent_warm_pool_enabled`,缺省开)。**实测冷启动 8.5-11.5s 中
|
|
145
|
+
~2.7s 是进程初始化;池子按模板预热一个 `--input-format stream-json` 进程,任务
|
|
146
|
+
到达即用(实测 send→result 5.9s),一进程一任务用完即杀,会话隔离与冷路径等价。
|
|
147
|
+
半死进程 15s 握手超时判死;快速故障(无热进程/spawn 失败/管道断/握手不落地)
|
|
148
|
+
一律静默降级冷路径。**唯一不降级的是跑满超时**——warm 与 cold 是同一次模型调用,
|
|
149
|
+
实测撞上 API 529 过载时 claude 会指数退避重试到超时,此时再跑一遍冷路径只是把
|
|
150
|
+
同一个等不到的答案等两遍;这一种按冷超时同样的 transient 错误上报,交给既有重试
|
|
151
|
+
机制。命令改写集中在 `warm_command()` 并用测试锁定与内置模板的对应关系;输出上限、
|
|
152
|
+
prompt 字节上限、`start_new_session` 与冷路径逐项对齐。
|
|
153
|
+
- **关机不再遗留付费孤儿进程。**agent 子进程全部登记在案,SIGTERM 时 executor 短暂
|
|
154
|
+
排空(5s)后统一杀进程组(含预热池已领用的进程);事件监听未启用的部署此前完全
|
|
155
|
+
没有关机钩子,现在两个分支同样覆盖。发送就绪检查的 dws 预检子进程加 10s 缓存,
|
|
156
|
+
并行 worker 不再各付 1.3-1.7s。
|
|
157
|
+
|
|
158
|
+
一次投递失败把三个沉默故障同时暴露出来:卡片永远转圈、两个状态表情并存、分身把自己
|
|
159
|
+
的回复读成主人说的话。追根到底是同一件事——**失败的返回值被丢掉了**。这一版把那些
|
|
160
|
+
返回值捡回来,并给这个服务装上它一直缺的那只表:分段耗时和日志时间戳。
|
|
161
|
+
|
|
162
|
+
### 新增
|
|
163
|
+
|
|
164
|
+
- **即时回执(`presence_instant_ack`,缺省关):事件到达数秒内先贴 ⌛ 再干活。**
|
|
165
|
+
事件载荷在 presence 层换一张「回复中」贴纸,消息→⌛ 实测 3-7 秒;门禁、游标、
|
|
166
|
+
生成、投递零改动。判定门 fail-closed(无文本/陈旧/身份不明/deny/robot/mute/
|
|
167
|
+
SUMMON 无窗都不贴;deny 每 60s 热读),写前登记 `presence_acks` 表,轮次收养或
|
|
168
|
+
撤回(TTL 清扫兜底崩溃残留,关开关后残留照扫)。观测:`presence_ack` 行 +
|
|
169
|
+
`presence_acks_adopted`/`presence_acks_withdrawn` 轮次计数。开启要求
|
|
170
|
+
presence_markers_enabled 与 event_listener_enabled 同时为真。
|
|
171
|
+
- **轮次侧 ⌛ 前移到请求构建之前。**原先贴在 agent 调用前、请求构建(含每张图一次
|
|
172
|
+
下载+视觉模型调用)之后,带图消息的 ⌛ 要等图片理解付完费才出现。现在任务选中
|
|
173
|
+
即贴;构建/建任务抛错的路径显式撤回,GENERATING 碰撞不叠贴。所有贴纸动作统一
|
|
174
|
+
走登记表去重,不依赖服务端对重复 add 的幂等性。
|
|
175
|
+
|
|
176
|
+
### 修复
|
|
177
|
+
|
|
178
|
+
- **事件通道不再在重启后随机失活:加入 rearm monitor。**根因不是订阅条数、不是
|
|
179
|
+
`--ephemeral`、也不是 stdin——实测服务端只把"长连接已建立时发出的订阅 create"
|
|
180
|
+
绑定到当前连接推送,而冷启动的 create 全部贴着 socket 出生,于是"创建成功、
|
|
181
|
+
`status=1`、永不投递"。叠加 dws 的归属清理(无 `--subscribe-id` 的 consumer 退出
|
|
182
|
+
必注销自己创建的订阅),每次重启都在重掷这颗骰子。新的 `RearmMonitor` 探测 bus
|
|
183
|
+
出生时间,把"贴着 socket spawn"的 consumer 在 socket 温热后弹跳一次,重建的订阅
|
|
184
|
+
落在温 socket 上稳定武装;`bus.log` 出现原地重连告警时同样重弹。每次重弹产生一行
|
|
185
|
+
`event_listener_rearm`(`reason=cold_socket|source_reconnect`),deliberate 弹跳
|
|
186
|
+
不计入失败退避。监视器整体是 best-effort:它失效只是少一次助推,轮询兜底不动。
|
|
187
|
+
|
|
188
|
+
- **卡片收口失败不再让卡片永远转圈。**`_abandon` 里那次收口 `update-card` 的返回值
|
|
189
|
+
以前直接丢弃;它一失败,卡片就停在 `flow_status=2`,前端"…"转到天荒地老。生产上
|
|
190
|
+
真实发生过:一条 512 字的回复分两段,第二段失败、兜底收口也失败,十四分钟后那张卡
|
|
191
|
+
还在转。现在收口结果会被判断,失败则把收口正文和 `bizId` 一起带出来交给修复队列。
|
|
192
|
+
只有 FINISH 帧会重试(`card_update_retries`,缺省 1 次)——中间帧丢了本来就等价于
|
|
193
|
+
提前收口,重试只是晚一个超时到达同一个结局;FINISH 帧丢了才是不可逆的。重试之间
|
|
194
|
+
不 sleep:被重试的失败本身就是一次 30 秒超时,早等够了。
|
|
195
|
+
- **`bizId` 落库,卡死的卡片第一次变得可修。**以前只存 `openTaskId`,而收口要的是
|
|
196
|
+
`bizId`——于是一张卡死的卡片,程序修不了,人也修不了。现在每次卡片发送都把 `bizId`
|
|
197
|
+
写进 `reply_jobs.card_biz_id`(这一列是给人排障用的),失败的那些另外进
|
|
198
|
+
`delivery_repairs` 队列,下一轮自动收口,超过预算则标 `ABANDONED` 不再重试。
|
|
199
|
+
**这不是重发**:修复只对同一张已存在的卡片再发一次 `update-card`,不新建消息、
|
|
200
|
+
不改内容——`SEND_UNKNOWN 不自动重发`这条没有松动。
|
|
201
|
+
- **⌛ 和 ✅ 不再并存。**`mark_replied` 以前不看 `remove` 的结果,清理失败照贴"已回复",
|
|
202
|
+
于是同一条消息上挂着两个互相矛盾的状态。`remove` 现在有界重试
|
|
203
|
+
(`presence_react_retries`,缺省 1 次),失败会计进 `presence_errors` 并进修复队列
|
|
204
|
+
下一轮再清。**"已回复"仍然照贴**——那是正确信息,不能因为清理失败就不告诉对方回复
|
|
205
|
+
到了;这里要修的是"失败不可见、也不会被清理",不是把 ✅ 拿掉。
|
|
206
|
+
- **`@me <任务>` 的回复回到召唤所在的会话。**控制批次的 sender 是主人本人,而单聊
|
|
207
|
+
投递一直是「回给 sender」——于是在同事单聊里打 `@me`,回复落进了主人的自聊:召唤
|
|
208
|
+
唯一没发生的那个会话。这个错还连带把记账撕成两半(回复记在同事会话、真实回声在
|
|
209
|
+
自聊被当成主人手打)。现在批次携带显式的 `reply_to_open_id`(从会话历史解析对端,
|
|
210
|
+
随 job 持久化——只存内存的话,跨轮重发的恰恰会回退到旧行为),三种形态各归其位:
|
|
211
|
+
自聊回自己、单聊回对方、群聊进群。对端解析不到(对方从没说过话)退回自聊,答案
|
|
212
|
+
宁可回错地方也不消失。回执类输出不变,始终只发给本人。
|
|
213
|
+
- **事件成为主处理链路,轮询降级为兜底。**新开关 `event_subscribe_standard_tier`
|
|
214
|
+
为标准档白名单里的每个 userId 自动派生一条单聊事件订阅——工号形如 `551400` 或
|
|
215
|
+
`WB02018153` 都算数,`--user` 唯一拒收的形状是 openDingTalkId(此前"必须纯数字"
|
|
216
|
+
的判据会把带字母前缀的同事悄悄留在轮询路径上,面板的实时订阅同源修正)。订阅上限
|
|
217
|
+
32,超出先丢派生、永不丢显式配置;派生满足不了 `event_listener_enabled requires
|
|
218
|
+
event_subscriptions` 这道门禁——它只认显式列表,名册编辑不该能让实例起不来。
|
|
219
|
+
每一轮日志新增 `wake`(event/interval/immediate/startup)与 `events_seen`:事件
|
|
220
|
+
通道死没死从此是日志里的事实——墓碑事故能潜伏,正是因为这两个字段从未存在。
|
|
221
|
+
跑超间隔的轮次若有事件在等,如实记 `event` 而不是 `immediate`(实测活跃窗里约四分
|
|
222
|
+
之一的轮次走这条分支)。监听器停机的宽限从"每消费者一份"改为**全体共享一份**:
|
|
223
|
+
launchd 在 SIGTERM 后约 20 秒就 SIGKILL,十几个消费者按旧算法要排队等两分钟,
|
|
224
|
+
等来的只会是被击杀在退订中途。轮询间隔推荐提到 300 秒(缺省不变,实例自配);
|
|
225
|
+
代价与边界写在 README 常驻模式一节。
|
|
226
|
+
- **事件唤醒不再在每次重启后无声变哑。**DWS 给同一条规则的所有消费者发的是**同一个**
|
|
227
|
+
服务端订阅 id(实测),而 `--ephemeral` 让任何一个消费者退出时把这份共享订阅注销掉;
|
|
228
|
+
之后的重建"成功"返回的只是墓碑,不再投递。于是每次 daemon 重启(旧进程退出注销、
|
|
229
|
+
新进程创建撞墓碑)都会把事件加速器掐死,轮询兜底又把这件事藏得干干净净——直到显式
|
|
230
|
+
`dws event stop` 清掉墓碑才能复活。常驻订阅改为不带 `--ephemeral`(幂等 subId 保证
|
|
231
|
+
每条规则全局只有一份,当初担心的"孤儿累积"在实测行为下不存在);面板的临时会话订阅
|
|
232
|
+
保持 ephemeral,但**与常驻规则同名时改为共享消费**——否则关一次面板就顺手拆了 daemon
|
|
233
|
+
的唤醒通道。同时实测确认:o2o 键用本人 userId 订阅,自聊消息也会实时推送(约 1.3 秒),
|
|
234
|
+
自聊从此不再只有轮询一条路。升级已套死的实例需要一次性
|
|
235
|
+
`dws event stop <subId> --yes` 清墓碑再重启。
|
|
236
|
+
- **分身不再把自己的回复读成主人的指示。**钉钉在回程上会重写空白(发出去的 `\n\n`
|
|
237
|
+
回来变成 ` \n `),而回声认领做的是逐字比对,于是恰恰是那些需要认领的回复认领失败,
|
|
238
|
+
被当成主人手打的一轮追加进 transcript。现在比对在空白归一化后进行。子串匹配加了
|
|
239
|
+
8 字下界:短回声("好的")撞进别人那一轮,会把主人真正说的话吃掉;完全相等则不设
|
|
240
|
+
下界。代价是七字以内的分段回声会多出一条冗余轮次——宁可多一条冗余,也不能吞掉一轮。
|
|
241
|
+
|
|
242
|
+
### 新增
|
|
243
|
+
|
|
244
|
+
- **`agent_config_home`:给分身一份自己的 CLI 配置。**不配这一项,分身用的就是你本人
|
|
245
|
+
那份——你的模型、你的思考档位、你装的每个 skill 和 MCP server。`claude:privileged`
|
|
246
|
+
模板本来就声明了 `isolation="config_home"`,只是在此之前没有任何东西去设那个环境变量。
|
|
247
|
+
本机实测(同一条 prompt、真实模板命令、各 6 次):继承本人配置时墙钟中位 23.0s
|
|
248
|
+
(20.0–29.5s),换成隔离目录后 12.5s(10.4–14.2s),两个分布不重叠。凭据按目录分账
|
|
249
|
+
存在 Keychain 里,所以新目录要自己登录一次;**本仓库不提供任何搬运凭据的代码路径**。
|
|
250
|
+
`agent_path` 一并加入,用来补齐 launchd 那份极窄的 `PATH`。
|
|
251
|
+
两者都**只接受字面绝对路径,不支持 `~`**——与 `dws_binary` 同规则,猜不如拒。
|
|
252
|
+
- **`agent_timeout_seconds_by_tier`:超时按档位分开。**一个统一的 120 秒对 restricted
|
|
253
|
+
富余、对 privileged 不够:超时白烧 120 秒、退避 30 秒、下一轮从头再来,实测端到端
|
|
254
|
+
被拉到 212.9s 和 349.8s。`lease_ttl` 跟着取各档最大值,遗弃判定的那个写死的十分钟
|
|
255
|
+
也改成跟 `lease_ttl` 走——否则超时一调大,还在跑的 job 会被误判成遗弃。
|
|
256
|
+
- **轮次分段耗时与日志时间戳。**在此之前 `launchd.out.log` 五千多行**没有一个时间戳**,
|
|
257
|
+
全是计数器;那次卡片故障只能靠反查 SQLite 才量出投递段花了 89.9 秒。现在每行输出带
|
|
258
|
+
`at`,每轮结果带 `duration_ms` 和分段 `phases`(`repair`/`recover`/`maintenance`/
|
|
259
|
+
`unread_snapshot`/`fetch`/`select`/`agent`/`deliver`)。
|
|
260
|
+
- **`card_open_before_generation`(缺省关闭):卡片先落地,再往里写。**开启后空卡在调
|
|
261
|
+
模型**之前**就发出去,读的人一轮之内看到卡片而不是等整个模型调用。风险是多出一个
|
|
262
|
+
"卡片已存在但回复还不存在"的状态,所以登记修复在生成**之前**——进程被 kill 也不会
|
|
263
|
+
留一张永远转圈的空卡。缺省关闭。
|
|
264
|
+
|
|
265
|
+
### 变更
|
|
266
|
+
|
|
267
|
+
- **Agent 子进程可以指定工作目录**,回复路径与 `verify-template` 探针都指向
|
|
268
|
+
`agent_workspace`。以前 `Popen` 不传 `cwd`,子进程继承 daemon 的工作目录,而
|
|
269
|
+
privileged 模板带 `--permission-mode bypassPermissions`——等于每一轮代答都在那个目录
|
|
270
|
+
的全部工具面上跑。模板明明声明了 `--add-dir {agent_workspace}`,继承是意外不是设计。
|
|
271
|
+
这是安全改动,不是性能改动。
|
|
272
|
+
- **`verify-template` 探针改用与回复路径完全相同的环境覆盖和工作目录。**不改这条,
|
|
273
|
+
`agent_templates_verified` 里那条人工背书验证的是运行时根本不会跑的命令。
|
|
274
|
+
- **`_notify_ding` 复用轮内已取的未读快照**,不再为同一个问题多起一次 `dws` 子进程;
|
|
275
|
+
`list-all` 与 `list-mentions` 改为并行取。`dws` 是个 shell wrapper,光 `--version`
|
|
276
|
+
就要 1.3–1.7 秒,一次回复原本要付十几次这样的冷启动。
|
|
277
|
+
- **`dws auth status` 明确不缓存。**省时诱人,但"每一次真实发送前重新复核 DWS 组织和
|
|
278
|
+
本人 ID"是写在文档里的承诺,这一版没有为了几秒钟去动它。
|
|
279
|
+
- `init` 生成的 launchd `PATH` 不再只有四个系统目录——privileged 那一轮里 agent 想调
|
|
280
|
+
`node`、`uv` 会直接找不到,失败重试白烧时间。
|
|
281
|
+
- `active_poll_interval_seconds` 下界从 15 放宽到 10,低于 15 会给一条 warning:
|
|
282
|
+
一轮自己的 DWS 调用就要好几秒,间隔压太低只是把占空比拉满。
|
|
283
|
+
|
|
284
|
+
## [0.3.0] - 2026-08-23
|
|
285
|
+
|
|
286
|
+
会话列表分档、会话流按 vim 翻、以及一条通往会话列表够不到的人的路。
|
|
287
|
+
|
|
288
|
+
三个功能都不小,但这一版最要紧的东西不在功能里:为了做它们而读透的那几条路径,
|
|
289
|
+
交出了四个一直在骗人的缺陷——其中三个每天都在影响真实消息。
|
|
290
|
+
|
|
291
|
+
### 修复(这四条是这一版的真正理由)
|
|
292
|
+
|
|
293
|
+
- **你亲手打的每一条消息,都带着「AI 辅助回复」发给了对方。**
|
|
294
|
+
`--title "AI 辅助回复"` 是写死的,钉钉把它显示在**对方的会话列表预览**里;
|
|
295
|
+
`--ai-tag` 取自全局配置而不是这条消息的来源,所以原生 AI 徽标也一样贴。
|
|
296
|
+
而撰写区一直在向你承诺「这条以你本人身份发出,不带 AI 标识」。
|
|
297
|
+
**正文层的披露一直是对的**(`compose_text` 只给模型写的内容加前缀),
|
|
298
|
+
错的是徽标层和标题层——`chat_write` 顶上「badge 由来源决定」那句声明,
|
|
299
|
+
实现只兑现了三分之一。现在 provenance 跟着消息走:`MessageBatch.ai_originated`
|
|
300
|
+
默认 `True`(漏改一个构造点的后果是多打标识,反过来是 AI 冒充人),
|
|
301
|
+
只有面板那一个构造点传 `False`,分身路径**代码零改动**。
|
|
302
|
+
卡片通道是第三层:它的徽标是贴上去的表情反应,既没有 `--ai-tag` 也没有 `--title` 可关,
|
|
303
|
+
现在按来源决定贴不贴,并且面板在卡片实例上改走独立的 personal 通道——
|
|
304
|
+
否则它连引用回复和转发都拿不到。
|
|
305
|
+
- **左边预览面板显示的是最旧的八条,不是最新的。**
|
|
306
|
+
`list --direction older` 返回降序,而取尾巴的代码假设升序。会话内因为
|
|
307
|
+
`limit` 恰好等于单页大小而全量取回,所以这个 bug 只在预览里暴露、三个月没被发现。
|
|
308
|
+
**AI 起草喂给模型的十二条同样是最旧的十二条**,连「对方是谁」都取自最旧那条的发件人——
|
|
309
|
+
「模型看到的和人看到的是同一片」这句注释属实,只是两边错得一样。
|
|
310
|
+
排序现在是 `ConversationHistoryPage` 这个类型自己的保证,不是调用方的假设。
|
|
311
|
+
- **按 `v` 下载附件永远说「下载完成」,而磁盘上什么都没有。**
|
|
312
|
+
`download-media` 要五个必填参数,代码传了两个不是合法 flag 的名字;
|
|
313
|
+
dws 返回错误对象却**退出码为零**,于是被当成正常响应,`path` 取空,
|
|
314
|
+
面板照着空路径报成功。根因不止这一处:dws 的业务错误是返回值不是异常,
|
|
315
|
+
同一个形状在清红点那条路上也中过一枪。
|
|
316
|
+
- **按 `m` 让分身闭嘴,列表上什么都不会变。**
|
|
317
|
+
写的是本地静音,读的是钉钉服务端免打扰,两件事共用一个 `muted` 字段。
|
|
318
|
+
反过来也错:你在手机上设了免打扰的会话会被折进面板的免打扰区,
|
|
319
|
+
而分身在那里照样说话。现在拆成 `avatarMuted` 和 `notificationsOff` 两个字段。
|
|
320
|
+
**这条修复是可见的**:免打扰折叠区从「钉钉里设了免打扰的」变成「你按 `m` 让分身闭嘴的」,
|
|
321
|
+
所以从没按过 `m` 的人会看到这条折叠区**整个消失**,原本藏在里面的会话回到未读档。
|
|
322
|
+
本机快照里那是 33 个群。它们一直被藏错了地方。
|
|
323
|
+
|
|
324
|
+
### 新增
|
|
325
|
+
|
|
326
|
+
- **会话列表分四档:置顶 / 单聊 / 群聊 / 其他。**
|
|
327
|
+
分档判定全在 Python 侧,面板只做一次相等比较。
|
|
328
|
+
**置顶区不是主列表的一个标志位**——实测十四个置顶会话里有九个(全是群)
|
|
329
|
+
压根不在 `list-all-conversations` 里,所以它从自己的 feed 独立渲染,
|
|
330
|
+
拉失败只损失一个分区标题和一句提示,绝不损失列表本身。
|
|
331
|
+
第四档叫「其他」而不是「功能」,因为 dws 没有「功能号」这一位数据:
|
|
332
|
+
`channel` 字段一百行全是 false。按标题正则猜会把真人群「云效公有云下单通知」
|
|
333
|
+
错判成功能号——**分类错误比分类未知更糟,读者会相信标签**。
|
|
334
|
+
残差还自带体检:一百行样本里只有一行落进「其他」,哪天它跳到二十,
|
|
335
|
+
说明钉钉加了新的群类型而白名单该更新了。
|
|
336
|
+
- **`t` 置顶 / 取消置顶。**写钉钉服务端,手机同步。不给确认对话框:
|
|
337
|
+
它自作用域、同一个键撤销、按下立刻可见,和清红点同一类。
|
|
338
|
+
对话框守的是代价和影响谁,不是「它是不是服务端写操作」。
|
|
339
|
+
- **会话流按 vim 翻页,光标不再跑到屏幕外。**
|
|
340
|
+
`^D`/`^U` 半屏、`PageUp`/`PageDown` 整屏——都按**行**不按条,
|
|
341
|
+
一条消息可能占一行也可能占四十行,按条翻的「一页」不是任何人认识的一页。
|
|
342
|
+
`zz`/`zt`/`zb` 定位。原来的实现只画末尾一屏、没有跟随光标的视口,
|
|
343
|
+
所以按几下 `k` 之后选中标记就消失在屏幕上方,而 `v`/`q`/数字键仍然作用在那条看不见的消息上。
|
|
344
|
+
- **`u` 真的能载入更早了。**原来 `before` 恒为当前时间、`limit` 只用来截断已取回的内容,
|
|
345
|
+
所以每次 `u` 都重新取同一批最新五十条,而「载入更早」那行永远挂着——
|
|
346
|
+
一个按不掉的假承诺。现在走真游标,并且**空游标是「到头了」的唯一含义**:
|
|
347
|
+
短页不代表到底,服务端扫固定条数原始记录再丢掉不渲染的,请求五条回三条是常态。
|
|
348
|
+
- **`q` 走钉钉原生引用回复**(`chat message reply`),不再是把引用文本拼进正文。
|
|
349
|
+
代码里「dws 不支持引用」那句注释是错的——`send` 没有这个参数不等于网关做不到。
|
|
350
|
+
- **多选转发。**`空格` 标记并下移一条,`V` 连选,`f` 转发。
|
|
351
|
+
多选不是一个模式,是「标记集合非空」这个状态——需要显式进出的模式会让
|
|
352
|
+
「我按 `j` 是移动还是扩选」变成一件要想的事。
|
|
353
|
+
多条走合并转发,**语义是打包成一条不是逐条原样转发**,这件事在选目标那一屏就说,
|
|
354
|
+
不留给确认框——人以为自己在做 A,确认框问「你确定要做 A 吗」,他会说确定。
|
|
355
|
+
确认屏画目标全名、**群人数**、和对方将看到的前三条。
|
|
356
|
+
多目标 dws 不支持,只能挨个发,所以结果**逐目标呈现**,
|
|
357
|
+
而且不提供重试:三个转发子命令的 schema 都声明 `idempotency: unknown`。
|
|
358
|
+
- **搜人开聊。**命令面板 `^K › 搜人开聊`。会话列表是一个不透明的一百行窗口,
|
|
359
|
+
实测四分之一真正在联系的人落在窗口外、今天完全够不到。
|
|
360
|
+
**结果不自动选中任何一行**:Enter 是搜索键,而「没看到反应就再按一次 Enter」
|
|
361
|
+
是终端里最常见的手势,默认停在第一行等于自动取第一个——
|
|
362
|
+
而实测搜「天彤」会同时命中两个人,发错人撤不回来。
|
|
363
|
+
每一项给部门而不是职位(职位在这个组织里三个数据源全空),
|
|
364
|
+
取不到时写「没有更多可区分的信息」而不是留空行——空行会被读成「和上一行一样」。
|
|
365
|
+
还会检查**这一屏里有没有两行渲染成完全一样的串**并说出来。
|
|
366
|
+
没聊过的人在选择时就说明原因,不等点进去才解释。
|
|
367
|
+
|
|
368
|
+
### 变更
|
|
369
|
+
|
|
370
|
+
- 协议 `1.0` → `1.1`。全部是新增字段,`muted` 保留一版且含义不变。
|
|
371
|
+
- 会话焦点的 `⏎` 从「进撰写区」改绑为「展开引用 / 合并转发」,`i` 不变。
|
|
372
|
+
- `Home`/`End` 明确不做:Ink 不上报这两个键,自己解析要引入第二条输入通路,
|
|
373
|
+
而「哪个焦点吃哪个键」从一张表变成两张表比缺这个键更糟。`gg`/`G` 是唯一路径。
|
|
374
|
+
|
|
375
|
+
## [0.2.4] - 2026-08-23
|
|
376
|
+
|
|
377
|
+
分身之外的两个人开始能用它:一个键直达常联系的人,以及一个只会读的 agent 通道。
|
|
378
|
+
外加一条把「写进去了但没人读」这类沉默故障关掉的配置门禁。
|
|
379
|
+
|
|
380
|
+
### 新增
|
|
381
|
+
|
|
382
|
+
- **速拨键 `g1`–`g9`,一个键直达一个会话。**前缀 `g` 本来就有(`gg` 回顶),
|
|
383
|
+
而 `gg` 是「去最上面」、`g1` 是「去第一个人」,是同一句话;裸数字保留原意
|
|
384
|
+
(列表里切镜头、会话里开链接),那两个用得太频繁,加前缀是便宜的那一半。
|
|
385
|
+
九个槽位不是字母表:真实需求是三到八个,vim 的 mark 语法帮不了不用 vim 的人。
|
|
386
|
+
**绑定走命令面板的对话框,不给键**——这个动作很少做、需要选槽位、且需要看见
|
|
387
|
+
已有绑定才能选,静默覆盖是「我依赖的速拨消失了而我从没看见它发生」的来源。
|
|
388
|
+
槽位不推荐、不自动重排:速拨的全部价值是肌肉记忆,会自己重排的速拨比没有更糟。
|
|
389
|
+
绑定时就解析对端身份,单聊解析不出来直接拒绝并给出路(「先在那边发一条」),
|
|
390
|
+
而不是先存着——速拨是「这个键不用想就到」的承诺,只是通常有效的那种会
|
|
391
|
+
恰好在主人需要快的那一刻失效。事后失效的槽位会在键上说出来并标进帮助表。
|
|
392
|
+
绑定是「读整张表、改一条、写回去」,而那次读发生在 `apply_config_updates`
|
|
393
|
+
的锁之外:调度循环是多线程的、dedup 只拒同槽位,所以两个不同槽位同时绑定会
|
|
394
|
+
各自基于同一份快照计算,后者抹掉前者。现在在进程内串行化——面板是这个 key
|
|
395
|
+
的唯一写者,这是正确的作用域。**没有那把锁,对应测试就红。**
|
|
396
|
+
0.2.2 加的穷尽 action switch 这次赚回来了:两个新 action 在引入它们的那一行
|
|
397
|
+
就被编译期拒绝,直到被处理。
|
|
398
|
+
|
|
399
|
+
- **第三份 skill `dingclaw-inbox`,只读。**收件箱、会话列表、单会话、时间线、跨会话搜索,
|
|
400
|
+
五条一次性 CLI(`dingclawd inbox / conversations / messages / timeline / search`),
|
|
401
|
+
输出单行 JSON。此前这五个视图**只存在于面板**:CLI 一条都没有,所以想让 agent 回答
|
|
402
|
+
「我错过了什么」,唯一的路是把 NDJSON socket 交出去——而那条连接上同时挂着 `messages.send`。
|
|
403
|
+
- **写的那一半不是加门禁,是没有子命令。**发送、起草、清红点、静音、切 summon/auto、
|
|
404
|
+
下载附件、触发轮次,一个都没有对应命令,也没有任何 flag 能把这五条变成能发消息的东西。
|
|
405
|
+
内核侧用一张显式 allowlist 把 `build_methods` 和 CLI 隔开:往调度表里加方法**不会**
|
|
406
|
+
自动变成 agent 够得到的能力,有测试钉住。
|
|
407
|
+
- **读不清红点。**这与面板本身不同:面板里打开会话会调 `clear-red-point`(除非按了 `^R`)。
|
|
408
|
+
`unread_gate_enabled` 读的是服务端未读状态,清红点等于让分身以后不再处理那个会话——
|
|
409
|
+
这个副作用该由看着屏幕的人决定,不该由一次「帮我看看」产生。五个视图逐一断言了
|
|
410
|
+
命令里不出现 `clear-red-point` / `mark-read` / `send`。
|
|
411
|
+
- **`skill install` 支持按 skill 选择**:`--skill dingclaw-inbox` 只装读的那一份。
|
|
412
|
+
分开是因为它们是两种授权:想让 agent 看收件箱的人,不必同时交出「以我的身份说话」。
|
|
413
|
+
缺省 `--skill all` 两份都装。失败报告改为 `atomicity=per_agent_skill`,
|
|
414
|
+
多出 `failed_skill` 和逐条的 `pending`;`pending_agents` 仍然只列**完全没动过**的
|
|
415
|
+
Agent——把刚失败的那个也列进去会读成「重试一下」,而重试会重做刚失败的那一步。
|
|
416
|
+
|
|
417
|
+
### 修复
|
|
418
|
+
|
|
419
|
+
- **配置写入不再接受 loader 永远不会读的 key。**`load_config` 不遍历 payload,
|
|
420
|
+
它按名字逐个取自己认识的 key——所以没人读的 key 不是错误,是**沉默**。
|
|
421
|
+
`apply_config_updates` 把收到的东西合并进去、靠加载一遍验证、然后报告成功。
|
|
422
|
+
拼错因此躲过了每一道检查:`send_enabld` 被接受、写进磁盘、永远被忽略,
|
|
423
|
+
而 `send_enabled` 保持旧值,调用方被告知写入成功。**是在真实配置上实测的,
|
|
424
|
+
不是设想**:全程 `auto_send_ready` 都是 true。
|
|
425
|
+
已知集合从 dataclass 推导而不是手写清单——手写清单就是一个版本之后的同一个故障。
|
|
426
|
+
只检查本次更新带来的 key:配置里已经存在的陌生 key 保留,拒绝它们会把
|
|
427
|
+
某人留下的过期行变成起不来的实例,那比一行被忽略更糟,也不是这个函数该做的决定。
|
|
428
|
+
**真实配置里就找到过这种行。**
|
|
429
|
+
第一版留了一个洞,宽度正好一个 key,而且是最不该开的那个:`config_path_secure`
|
|
430
|
+
是 `ServiceConfig` 的字段但不是配置 key(`load_config` 从文件权限位算出它,
|
|
431
|
+
从不从 payload 读),所以它通过了推导出的白名单然后被忽略——正是要堵的那个故障。
|
|
432
|
+
它也是任何人想绕过权限检查时最可能伸手去写的名字,因为它读起来像一个可以关掉的
|
|
433
|
+
安全开关,而不是一个已经得出的结论。减掉这一个名字只修好今天:下一个计算字段
|
|
434
|
+
会用新名字把洞重新打开,且无人察觉。所以耐久的那一半是一条断言——可写 key 集合
|
|
435
|
+
与全新安装写出的 key 集合必须**完全相等**。现在是 95 对 95,一旦不等即失败,
|
|
436
|
+
已通过加一个字段看着它变红验证过。
|
|
437
|
+
|
|
438
|
+
- **发布门禁此前不检查两份 skill 是否真的进了包。**`agent_skill/` 只靠 pyproject 的
|
|
439
|
+
package-data 通配符,而 `skill install` 是从 package data 里读的——glob 掉了的话
|
|
440
|
+
editable 安装照常工作,只在接收方机器上炸。这正是 tui_app 那条 `Required` 当初
|
|
441
|
+
要防的形状,现在两份 skill 各有一条,且各自独立断言。
|
|
442
|
+
|
|
443
|
+
## [0.2.3] - 2026-08-23
|
|
444
|
+
|
|
445
|
+
分诊列表第一次能被清空。三个分区此前根本没有「处理掉」这个动作——`R` 只清红点,
|
|
446
|
+
而红点只有「未读」有。
|
|
447
|
+
|
|
448
|
+
### 新增
|
|
449
|
+
|
|
450
|
+
- **`R` 现在能清掉全部四种行**。关键、钉我、@我 在自己的状态库里记一道本地水位,
|
|
451
|
+
未读仍然调 `chat clear-red-point`。**一个意图一个键**:哪些种类有服务端 ack
|
|
452
|
+
是实现细节,不该漏到人的手指上——「R 清红点」和「d 标已处理」从主人的位置看
|
|
453
|
+
是同一句话「把这条从我的列表里拿掉」。
|
|
454
|
+
钉钉侧确实没有这个原语,实测:`ding message list --type UNREAD` 对一个
|
|
455
|
+
`ALL` 有五十条的信箱恒返回 0;子命令只有 send / recall / receiver-status,
|
|
456
|
+
而 `receiver-status` 报 `senderUid not match`,只能查自己发出去的那些。
|
|
457
|
+
客户端里 DING 归在待办 tab 下是**呈现方式不是数据模型**:一条 DING 记录只有
|
|
458
|
+
`dingContent` / `dingSourceOpenMessageId` / `openDingId` / `sendTime` / `senderNick`
|
|
459
|
+
五个字段,没有状态也没有指向待办的 id,**50 条 DING 对 20 条待办内容零重合**。
|
|
460
|
+
- **水位存的是时刻,不是布尔**。处理掉一个会话,settle 的是当时在里面的东西,
|
|
461
|
+
不构成对之后任何消息的表态——所以新消息来了这一行自己浮回来。
|
|
462
|
+
永久静音是另一句话,`m` 是它的键。水位还按种类分:处理掉一条 DING 不会遮住
|
|
463
|
+
同一会话里到达的未读。
|
|
464
|
+
- **已处理的行折进折叠组而不是消失**,复用免打扰那条 `fold` 行。
|
|
465
|
+
直接消失会让人怀疑自己按错了键,而且没有任何复原线索。
|
|
466
|
+
展开后在行上**再按一次 `R` 就是撤销**——同一个键,不需要抢时间窗,
|
|
467
|
+
也不需要先意识到自己错了。
|
|
468
|
+
- **`dingclaw tui`**。`dingclawd` 不在 PATH 上,shell 入口此前没有任何通往面板的路,
|
|
469
|
+
所以「怎么打开」的答案是一条指向 virtualenv 的绝对路径。用 `exec` 不起子进程,
|
|
470
|
+
否则 Ctrl-C 和窗口尺寸到不了面板。
|
|
471
|
+
|
|
472
|
+
### 修复
|
|
473
|
+
|
|
474
|
+
- **主人自己的 @ 不再当作待办**。`build_rows` 的文档一直写着 `self_names` 和
|
|
475
|
+
`self_open_ids` 会滤掉主人自己的流量,而**只有 DING 那一半真的做了**:
|
|
476
|
+
`_mention_rows` 压根没接这两个参数。实测主人收件箱里四条 @我 有三条是他自己的召唤。
|
|
477
|
+
@ 提及**不带 senderId**,所以 open id 是唯一的身份依据;显示名只在没有 id 时兜底、
|
|
478
|
+
永远不覆盖 id——名字不是身份,而误杀一个同名同事的消息是更糟的那种失败,
|
|
479
|
+
为此专门加了反向断言。
|
|
480
|
+
- **`buildRows` 的 `filter` 选项从 0.2.1 起就没有任何调用点传过**。面板改用它,
|
|
481
|
+
删掉上一版在 App 层的重复实现。
|
|
482
|
+
|
|
483
|
+
### 变更
|
|
484
|
+
|
|
485
|
+
- 头部计数只数还在等的东西,已处理的跟着列表一起从计数里退出。
|
|
486
|
+
头部写 3 而列表只有 1,读者会信头部然后去找另外两条。
|
|
487
|
+
- 未读那一路**绝不同时写本地水位**。这是正确性不是偏好:清红点失败却写了水位,
|
|
488
|
+
行会在面板里消失而分身仍视为未读,屏幕上看不出两边已经分叉。
|
|
489
|
+
|
|
490
|
+
### 已知边界
|
|
491
|
+
|
|
492
|
+
- **「已处理」只在这台机器上**。换一台机器、或者清掉 state 库,标记就没了。
|
|
493
|
+
钉钉不知道这件事,同事也看不见——所以它只能表达「我看过了」,
|
|
494
|
+
不能表达「我认领了」。
|
|
495
|
+
- **同一个形状在这个仓库出现了三次**:curses 面板的 `titles={}` 参数预留好、
|
|
496
|
+
永远传空;0.2.2 那四个键绑定存在、永远不触发;`buildRows` 的 `filter`
|
|
497
|
+
选项存在、永远不传。共同点是**默认值让死的东西看起来是活的**——不报错、
|
|
498
|
+
不告警、测试照过。键那一类已经用编译期穷尽检查堵住了;
|
|
499
|
+
参数这一类**试过三轮自动扫描,每一轮都以不同方式算错**
|
|
500
|
+
(正则减法、构造器按类名调用、按位置传参),
|
|
501
|
+
第三轮收敛到 26 条之后仍然条条需要人工判断(别名、legacy 路径、
|
|
502
|
+
故意不用的过滤器),**误报率高到会训练人忽略它**,因此没有做成检查工具。
|
|
503
|
+
下一个人请把「有没有调用点真的传过它」当成一个独立问题主动去问。
|
|
504
|
+
|
|
505
|
+
## [0.2.2] - 2026-08-23
|
|
506
|
+
|
|
507
|
+
把 0.2.1 留下的半成品做完,不加新表面。
|
|
508
|
+
|
|
509
|
+
### 新增
|
|
510
|
+
|
|
511
|
+
- **`^F` 跨会话搜索**,并且说明搜的是哪个时间窗。只说「没有结果」会让人以为
|
|
512
|
+
东西不存在,而真实答案几乎总是「不在这个窗口里」。
|
|
513
|
+
- **`h` / `←` 返回**。刻意没有 Ctrl 形式:`^H` **就是** ASCII 退格字节,
|
|
514
|
+
绑成「左」会让任何发送 0x08 的终端失去退格键;`^L` 在所有 shell 里都是清屏,
|
|
515
|
+
所以它在这里是刷新。readline 的左右本来就是 `^B` / `^F`,撰写区两个都认。
|
|
516
|
+
- **`^R` 未读保持开关**、**`/` 当前镜头筛选**、**`q` 引用**。
|
|
517
|
+
引用**不是钉钉原生引用**且不能声称是:`chat message send` 没有 reply-to 参数
|
|
518
|
+
(对着完整 flag 表查过),网关做不出那个引用气泡。它把原文带进正文并署名,
|
|
519
|
+
跟 threading 出现之前的邮件一样。
|
|
520
|
+
- **附件选择器**。此前 `v` 只读 `attachments[0]`,一条消息带两个文件时
|
|
521
|
+
第二个用面板里任何键都够不到。
|
|
522
|
+
|
|
523
|
+
### 修复
|
|
524
|
+
|
|
525
|
+
- **超长消息在面板里截断而不是渲染成空行**。`_bounded_text` 把超过四千字的正文
|
|
526
|
+
清空,这对回复门禁是对的(不能把十四 KB 的卡片喂给模型),在屏幕上却是
|
|
527
|
+
把一条有内容的消息画成空白,读者会以为消息本身是空的。
|
|
528
|
+
截断是**显式**的:静默只显示前四千字会让人以为自己读完了。
|
|
529
|
+
- **四个键按下去什么都不发生**,其中两个还印在帮助表里(`^R` 未读保持、
|
|
530
|
+
`o` 在钉钉里打开)。它们能活下来是因为 switch 结尾是一个运行时兜底——
|
|
531
|
+
测试、构建、类型检查没有一个会失败。现在那个分支把 action 赋给 `never`,
|
|
532
|
+
新增一个键忘了接线会**停在那一行编译不过**。
|
|
533
|
+
`o` 直接删了没有假装做:dws 不暴露任何深链,日历那唯一一处 detail URL 实测是 null。
|
|
534
|
+
|
|
535
|
+
### 变更
|
|
536
|
+
|
|
537
|
+
- **被艾特和被召唤的消息不再等三十秒静默窗口**。实测端到端 101 秒降到 46 秒,
|
|
538
|
+
发现那一半从 41 秒降到 10 秒。静默窗口在单聊里 26% 的批次真的等来了第二条消息,
|
|
539
|
+
在群里只有 9%——而群正好是所有 @ 记录所在的地方,因为没人会在一对一里 @ 对方。
|
|
540
|
+
- **每轮 job 上限 10 改 3**。轮次内 job 串行、而一轮只在开头发现一次消息,
|
|
541
|
+
所以这个数字同时是「轮询可以瞎多久」:实测一轮曾经占住循环十七分钟。
|
|
542
|
+
补偿链路**另给了自己的上限**,否则「重处理最近十分钟」会悄悄变成「重处理三条」。
|
|
543
|
+
- **过期的 access token 先尝试非交互续期再关门**。实测该账号出现过十九个中断窗口,
|
|
544
|
+
最长连续九十八轮。**续期是否真的有效至今未验证**——它一次都没触发过:
|
|
545
|
+
实际观察到的失败态里 `refresh_token_valid` 是 false,
|
|
546
|
+
而这个分支要求它为 true。见 0.2.3 之后待办。
|
|
547
|
+
|
|
548
|
+
## [0.2.1] - 2026-08-22
|
|
549
|
+
|
|
550
|
+
分身开始「有状态、看得见、能被人接管」。四件事:回复状态用钉钉原生文字表情表达、
|
|
551
|
+
只处理未读变成一个可关的开关、图片能被真正读懂、以及分身关掉时有一个 TUI 待办面板。
|
|
552
|
+
|
|
553
|
+
### 新增
|
|
554
|
+
|
|
555
|
+
- **文字表情状态标识**(`presence_markers_enabled`,默认关)。分身开始生成时,在
|
|
556
|
+
**对方那条消息**上贴「⌛分身回复中」;回复真的发出去之后换成「✅分身已回复」。
|
|
557
|
+
这是唯一能表达「进行中」的通道——等到有正文可发的时候,已经不在「回复中」了。
|
|
558
|
+
锚点是批次里**最新**的那条消息(回复在视觉上答的就是它),不落库:批次是冻结在
|
|
559
|
+
reply job 里的,终态时重算出的锚点必然和当初打标记的那条一致。
|
|
560
|
+
- **AI 标识从正文搬到表情**(`ai_disclosure_mode=emotion`,默认仍是 `text`)。
|
|
561
|
+
出站消息贴「🤖智能分身代发」,正文不再带 `[AI 分身代发]` 前缀。
|
|
562
|
+
**只有流式卡片路径允许这么做**,因为只有它存在「消息已建、正文还空」的一瞬:
|
|
563
|
+
`send-card` 建卡 → `query-send-status` 拿到 openMessageId → 贴徽标 → 再
|
|
564
|
+
`update-card` 填正文。徽标没贴上就把正文标识加回去再填内容,**合规不变量一步没松**。
|
|
565
|
+
准确的说法是「**正文永远不会裸奔**」而不是「窗口为零」:空卡片本身在贴徽标之前
|
|
566
|
+
已经可见,对方会先看到一个属于主人的空气泡,只是它里面还没有任何内容。
|
|
567
|
+
纯文本与机器人路径连这个都没有,保留正文标识,这是 readiness 硬约束。
|
|
568
|
+
- **`dingclawd create-emotions`**。一次性创建三个文字表情模板并写回配置。
|
|
569
|
+
刻意做成手动、且重复运行会被拒绝:`create-text-emotion` **没有幂等键**
|
|
570
|
+
(实测同名同文两次调用返回不同 emotionId),而 DWS **没有任何列出或删除文字表情的命令**,
|
|
571
|
+
所以放进轮询里只会在主人的表情面板里堆出清不掉的重复模板。
|
|
572
|
+
`--force` 留给「描述符丢了、且接受留下旧的」这一种情况。
|
|
573
|
+
- **`dingclawd tui`**。分身关着的时候用的待办面板(纯 stdlib curses,无第三方依赖):
|
|
574
|
+
未读会话、@我、钉我、以及服务自己判定值得打断的提醒,**一个会话只出一行**,
|
|
575
|
+
按紧急度倒排。方向键/jk 选中,`Enter` 一键触发一次回复,`p` 先输提示词再触发。
|
|
576
|
+
触发链路是「先写 `manual_triggers` 行,再起一个真的 `dingclawd poll` 子进程」——
|
|
577
|
+
常驻服务没跑时这一轮就是面板干的活,常驻服务在跑时抢不到租约、由它消费,
|
|
578
|
+
**两种模式走同一套代码**。退出不杀正在跑的那一轮。
|
|
579
|
+
- **DING 可以反查会话**。「消息转 DING」那一类带 `dingSourceOpenMessageId`,
|
|
580
|
+
经 `chat message list-by-ids` 能拿回 conversationId 与真实发送者身份。
|
|
581
|
+
所以 `ding_notifier.py` 里那句「DING 只能当出口」需要改写成更准确的说法:
|
|
582
|
+
**DING 永远不提供身份,但它可以指向一条提供身份的消息**——黑名单、self-id、
|
|
583
|
+
未读、时效、tier 全部照旧作用在那条真实消息上,没有引入任何新的信任。
|
|
584
|
+
拿不到源消息的 DING(自由文本 DING、转发类 DING)**只展示、永不可触发**,
|
|
585
|
+
行尾直接写明原因。
|
|
586
|
+
- **图片理解**(`image_understanding_enabled`,默认关)。DWS 已经把图片消息拍平成
|
|
587
|
+
`[图片消息](mediaId=@xxx)` 加一段服务端 caption,而那段 caption 实测就是
|
|
588
|
+
`screenshot` 一个词。现在改成:`download-media` 下载到 `<state_path>/../media`,
|
|
589
|
+
交给一个**新的 `vision` tier** 读图,把描述替换回正文再交给回复模型。
|
|
590
|
+
回复模型的隔离一点没动——它拿到的仍然只是文本,`--tools ""` 不变;
|
|
591
|
+
全产品唯一允许读文件的进程是这个独立的视觉步骤,它的命令同样是逐字白名单模板,
|
|
592
|
+
唯一可读目录就是那个媒体目录。
|
|
593
|
+
**成本是所有限制的由来**:实测经 Claude Code CLI 读一张图,Haiku 约 0.30 美元 / 14 秒,
|
|
594
|
+
Opus 约 1.22 美元 / 32 秒,绝大部分开销是 CLI 自己的系统提示词而不是图。所以
|
|
595
|
+
**按 mediaId 缓存**(同一个告警横幅在几十条告警里是同一个 id,只付一次),
|
|
596
|
+
并同时有每条消息上限(默认 2 张)和每日上限(默认 20 次)。
|
|
597
|
+
- **手动触发可以带提示词**。`manual_triggers` 增加 `note` 列,经
|
|
598
|
+
`AgentRequest.owner_note` 进入 prompt 的**可信半区**——它确实是主人本机输入,
|
|
599
|
+
不是消息 JSON 里的不可信内容。但它**只影响回复内容,不授予任何权限**:
|
|
600
|
+
tier、工具、发送门禁一律不变。
|
|
601
|
+
|
|
602
|
+
### 变更
|
|
603
|
+
|
|
604
|
+
- **`unread_gate_enabled` 默认从 false 翻成 true。** 这是全产品唯一一个「默认开」的开关,
|
|
605
|
+
因为打开它是**收窄**分身的作用范围。关掉它才是「什么都回」,那是更宽的行为,
|
|
606
|
+
必须由人明确选择。
|
|
607
|
+
- **关掉未读门禁时,四个防「反悔」的装置从可选变成必需**(readiness 错误):
|
|
608
|
+
`burst_quiet_seconds >= 15`、`round_merge_enabled`、`presend_recheck_enabled`、
|
|
609
|
+
`max_supersede_per_job >= 1`。理由是门禁开着时,主人自己「读过了」本身就是一道兜底;
|
|
610
|
+
关掉之后这道兜底没有了,剩下的就只有这几个,没有一个还能是可选的。
|
|
611
|
+
最后那条是复查时才发现的:防饿死计数为 0 会在 recheck 之前短路返回,
|
|
612
|
+
**让整段 presend recheck 变成永远不执行的死代码**——一道开着却从来没跑过的防线。
|
|
613
|
+
- **关掉未读门禁时,presend recheck 不再 fail-open。** 原来的 fail-open 理由
|
|
614
|
+
(「fail closed 会让每次重试都哑掉」)成立的前提是未读门禁还是主防线;
|
|
615
|
+
门禁关掉之后 recheck 就是主防线,查不出来时改为**保留这一轮**(job 仍是 GENERATED,
|
|
616
|
+
下一轮重判),而不是当作「对方没说新的」照发。持续查不出来最终仍会走
|
|
617
|
+
`stale_message` 正常作废,不会无限挂着。
|
|
618
|
+
- **prompt 增加一条覆盖规则**:同一批消息里后面的话覆盖前面的话,对方改口以最后一次为准;
|
|
619
|
+
前后矛盾且无法判断时,不照任何一条执行或承诺,改为一句话问回去确认。
|
|
620
|
+
这条针对的是「让删某个文件、随后又说别删」这类连续发言场景。
|
|
621
|
+
- `DeliveryReceipt` 增加 `sent_text`:徽标路径下正文可能带也可能不带标识前缀,
|
|
622
|
+
流水账必须记**实际发出的那一份**,否则下一轮的自有消息认领必然落空。
|
|
623
|
+
- agents 条目的 `tier` 现在允许 `vision`。
|
|
624
|
+
|
|
625
|
+
### 修复
|
|
626
|
+
|
|
627
|
+
- **`verify-contracts` 过去只能成功跑一次,而文档要求的流程恰恰是跑两次。**
|
|
628
|
+
影响不止于「跑不通」:**任何按文档做过 attest 的人,签下的都是一次失败的探针**,
|
|
629
|
+
于是整个 attest 门禁在上一个版本里名义上成立、实际上从未验证过。
|
|
630
|
+
探针的批次 id 与文本都是常量,而个人发送的幂等 uuid 由这两者派生,所以第二次运行
|
|
631
|
+
必然复用第一次的 uuid,被钉钉以 `Request is repeated with uuid` 拒绝——
|
|
632
|
+
也就是说「先跑一次看证据,再加 `--attest` 跑第二次」里**签字的那一次一直是失败的那一次**。
|
|
633
|
+
探针 id 现在带时间戳。这是 0.2.0 就存在的缺陷。
|
|
634
|
+
- **卡片首段就失败时,发出去的那句占位符没有任何 AI 标识。** `_abandon` 发的是
|
|
635
|
+
`closing` 而不是带标识的正文,首段失败时 `closing` 就是一句裸的「本条回复未能发出」。
|
|
636
|
+
现在 `_abandon` 自己补标识。同样是 0.2.0 就存在的缺陷。
|
|
637
|
+
- **`_abandon` 回传的 `sent_text` 说了谎。** 它回传的是**预期的完整回复**,
|
|
638
|
+
而实际发布的是那句占位符。后果不只是 echo claim 对不上:下一轮 agent 会以为
|
|
639
|
+
自己已经说过整段话、对方其实根本没收到,于是不再重复。现在回传实际发布的文本。
|
|
640
|
+
- **贴徽标那两次 DWS 往返抛非预期异常时,空卡片会永远停在「生成中」。**
|
|
641
|
+
它在 update 循环之外,不在任何能收尾卡片的 try 里。现在兜住并走 `_abandon` 收尾。
|
|
642
|
+
- **`_safe_media_name` 会把不同的图映射到同一个文件名。** 被清洗掉的
|
|
643
|
+
`@` `+` `/` `=` 正好是钉钉 mediaId 用的 base64 字母表,于是两张只差一个字符的图
|
|
644
|
+
共用一个文件名,「已下载」检查直接返回**别人的图**。现在文件名带完整 id 的摘要后缀。
|
|
645
|
+
|
|
646
|
+
### 安全
|
|
647
|
+
|
|
648
|
+
- **「回复中」原来有十条永久泄漏路径。** 只有一处打标记,但退出 GENERATING 的终态有十条,
|
|
649
|
+
其中 `read_after_generation`(主人自己看到并手工回了)是**每天都会发生的正常路径**——
|
|
650
|
+
对方消息上会永远挂着「分身回复中」,而主人其实已经在跟他真人对话。
|
|
651
|
+
现在三个退役动作(cancel_generated / cancel_retry / supersede)统一收口成三个
|
|
652
|
+
同时撤标记的 helper,十个调用点没有一个能绕过;崩溃恢复里尝试次数用尽、
|
|
653
|
+
再也不会重跑的 job 也会撤标记。
|
|
654
|
+
- **图片描述在进入下一个模型的 prompt 前会被规范化。** 它是模型输出,而
|
|
655
|
+
`text_in_image` 的设计目的就是逐字转写图里的文字——所以「在截图里写一段指令」
|
|
656
|
+
是一等公民注入通道,视觉模型如实转写恰恰是在正确工作。现在去控制字符与换行、
|
|
657
|
+
中和方括号、中和块自身的标签,一段读数没法伪造自己块的结束或者伪造第二个块。
|
|
658
|
+
- **图片替换改成对原文一次遍历。** 原来逐个 id 替换会重新扫描已经含有描述的文本,
|
|
659
|
+
于是一段描述里写上另一个 mediaId 就能把那张图的读数吞进自己的块里。
|
|
660
|
+
- **对方打字打不出预算消耗了。** 裸 `mediaId=` 正则现在要求以 `@` 开头
|
|
661
|
+
(真实 mediaId 都是),否则随手打一行字就能换走一次下载子进程和一格当日视觉额度。
|
|
662
|
+
- **两个新子进程沿用 agent 子进程的全套加固**:环境变量白名单、`RLIMIT_FSIZE`
|
|
663
|
+
输出上限、超时杀**进程组**(原来 `subprocess.run` 的超时只杀直接子进程,
|
|
664
|
+
`claude` 的孙进程会活下来)。
|
|
665
|
+
- **表情反应按对外动作对待。** 它带着主人的名字出现在别人的聊天里,所以
|
|
666
|
+
`add-text-emotion` / `remove-text-emotion` 是两条必须验证的新 DWS 契约,
|
|
667
|
+
`verify-contracts` 会在**主人自己的单聊**上真贴一次再撤掉,出具证据。
|
|
668
|
+
`remove` 与 `add` 一起验:能贴上却撤不掉,比不贴更糟——那是一句撤不回的
|
|
669
|
+
「回复还在路上」。
|
|
670
|
+
- **任何非发送成功的终态都撤标记,而不是换成「已回复」。** 模型判 skip、
|
|
671
|
+
policy 过滤、presend 被新消息取代、发送失败,全部走 clear。
|
|
672
|
+
- **表情描述符纳入发布纯净性门禁。** emotionId 是在某一个组织里创建出来的,
|
|
673
|
+
而 DWS 既不能列出也不能删除模板,所以它既是个人数据、又对接收方永远是错的。
|
|
674
|
+
`disclosure_emotion` / `presence_composing_emotion` / `presence_replied_emotion`
|
|
675
|
+
必须发空。
|
|
676
|
+
- **`download-media` 是一条独立契约。** 把同事的附件写到本地磁盘,和读一条消息
|
|
677
|
+
是不同性质的动作,值得人看一眼。下载写临时名、成功才改名,被杀的下载不会留下
|
|
678
|
+
一个看起来像缓存命中的半截文件;媒体 id 只用于生成文件名前会被清洗,它来自钉钉,
|
|
679
|
+
从来不当路径成分用。
|
|
680
|
+
- **图片描述是模型输出,会进入下一个模型的 prompt。** 它以
|
|
681
|
+
`[图片内容(自动识别):…]` 明确标注,回复模型不得把它当成对方原话复述。
|
|
682
|
+
|
|
683
|
+
### 已知边界
|
|
684
|
+
|
|
685
|
+
- 文字表情的文案长度有一个**未公开的上限**,实测:8 个中文可以、9 个不行;
|
|
686
|
+
10 个 ASCII 可以、12 个不行;emoji 占用明显更重。
|
|
687
|
+
所以「zhuoyue智能分身代发」这类字符串建不出来,交付默认用不带名字的
|
|
688
|
+
「🤖智能分身代发 / ⌛分身回复中 / ✅分身已回复」。`create-emotions --avatar-name`
|
|
689
|
+
会先试带名字的写法,被拒绝时回退到通用写法并**在输出里明说用了哪一个**。
|
|
690
|
+
- 进程被强杀且恰好卡在「已贴回复中、还没贴已回复」之间时,会在对方消息上留下一个
|
|
691
|
+
过期的「回复中」。所有正常路径(含崩溃恢复后判定不再重跑的 job)都会撤,这一种不会。
|
|
692
|
+
另有一种:`mark_replied` 里 remove 失败但 add 成功时,两个标记会同时挂着,
|
|
693
|
+
没有对账机制去修它。
|
|
694
|
+
- **未读会话数达到 100(钉钉这个 API 的上限)时,默认的 `skip_round` 会让分身整体静默**,
|
|
695
|
+
且除了 `doctor` / `status` 的 `warnings` 之外没有别的信号。这个默认值是在
|
|
696
|
+
未读门禁还是「默认关」的时候定的;门禁现在默认开了,所以至少要说出来。
|
|
697
|
+
收件箱重的人建议把 `unread_gate_on_overflow` 改成 `bypass`。
|
|
698
|
+
- TUI 的未读行拿不到群名(`list-unread-conversations` 只返回 conversationId),
|
|
699
|
+
退化成「群聊 cidXXXX…」。
|
|
700
|
+
- TUI 的 DING 段按时间窗过滤(`--hours`,默认 24)。用 `--type UNREAD` 看着更对但实际不行:
|
|
701
|
+
主人一打开钉钉 DING 就被标记已读,那个列表空的时候远多于待办列表空的时候。
|
|
702
|
+
- 视觉模型的 `readable` 字段过去把「我打开并看到了这张图」和「图里有值得说的内容」
|
|
703
|
+
混成一件事,于是一张白墙照片会被渲染成「无法识别内容」。提示词已经把两者拆开。
|
|
704
|
+
|
|
705
|
+
## [0.2.0] - 2026-08-22
|
|
706
|
+
|
|
707
|
+
首个可交付版本。0.1.0 只在打包者本机运行,0.2.0 开始面向「交给别人装在他自己的 Mac 上」。
|
|
708
|
+
|
|
709
|
+
### 新增
|
|
710
|
+
|
|
711
|
+
- **安装 skill** `SKILL.md`,位于交付包**根目录**。接收方把它交给自己的 agent 即可
|
|
712
|
+
完成安装。它是包里的一个普通文件而不是 `dingclawd skill install` 的产物——后者装的是
|
|
713
|
+
运维 skill,而安装的时候还没有 `dingclawd` 可用。
|
|
714
|
+
- **三条带证据的门禁命令**,取代手改 0600 配置:`verify-contracts`(逐项真实调用契约、
|
|
715
|
+
出具证据,全通过才写 `verified_contracts` 与 `contract_verified`)、`verify-template`
|
|
716
|
+
(探针通过才写 `agent_templates_verified`)、`enable-sending`(开 `send_enabled`)。
|
|
717
|
+
三条都要求显式 `--yes`,且都是 fail-closed:任何一项 `failed` 或 `skipped` 就拒绝签字,
|
|
718
|
+
`enable-sending` 在写入的那一刻重算 readiness,不信任上一次 `doctor` 的结论。
|
|
719
|
+
- **安装器** `dingclawd init`。渲染完整配置、`~/.dingclaw` 骨架与 launchd plist,
|
|
720
|
+
全部门禁写成关闭状态。plist 在安装时按接收方路径生成,不再由仓库提供静态文件。
|
|
721
|
+
- **身份自查** `dingclawd whoami`。两步只读调用拿到本人 userId 与 openDingTalkId。
|
|
722
|
+
同名会返回多条,按 `dws auth status` 的 `user_id` 匹配,不取首条。
|
|
723
|
+
- **多 agent 支持**。claude / codex / qoder 三种 CLI,交付默认 qoder。qoder 无
|
|
724
|
+
`--safe-mode` 与 `--json-schema`,restricted 档靠 `--tools "" --strict-mcp-config
|
|
725
|
+
--no-session-persistence` 隔离,JSON 结构无 schema 强制——这是它与 claude 的真实
|
|
726
|
+
能力差。qoder 模板属实验档,接收方须自行 `verify-template` 后写入
|
|
727
|
+
`agent_templates_verified`。
|
|
728
|
+
- **发布纯净性门禁** `scripts/check_artifact.py`。递归展开交付包内的 wheel 与 sdist,
|
|
729
|
+
拦截打包者的个人数据:本机 home 路径、他人的 openDingTalkId、打包者所属组织的
|
|
730
|
+
corpId、手机号、邮箱、非空的身份名单、混入的运行时状态文件。断言**逐包独立**,
|
|
731
|
+
避免完好的 sdist 替有问题的 wheel 背书。
|
|
732
|
+
- **打包脚本** `scripts/package.sh`。构建、组装交付包、过门禁、跑净室安装,
|
|
733
|
+
通过后输出 sha256。任一步失败即删除交付包,不留下可发出去的东西。
|
|
734
|
+
- **净室安装验证** `scripts/cleanroom_install.sh`。在临时 HOME 与 venv 中安装 wheel,
|
|
735
|
+
执行 `init` 与 `doctor`,断言就绪错误与预期完全一致,并核对真实 HOME 未被写入。
|
|
736
|
+
|
|
737
|
+
### 移除
|
|
738
|
+
|
|
739
|
+
- **删除代码级强制黑名单**。原先有一个写死在代码里的人员条目,随包发给每一个使用者。
|
|
740
|
+
黑名单属于运行实例的人,不属于打包的人。现在 `deny_user_ids` 就是全部,任何条目
|
|
741
|
+
都能加能删,`init` 产出空黑名单。**升级前必读下方「从 0.1.0 升级」第 1 步。**
|
|
742
|
+
|
|
743
|
+
### 变更
|
|
744
|
+
|
|
745
|
+
- **人格不再硬编码**。新配置项 `owner_display_name` 决定 agent 自述身份。
|
|
746
|
+
- sdist 不再包含 `tests/`(`MANIFEST.in` 的 `prune tests`)。接收方的验证路径是
|
|
747
|
+
`doctor` 与 `verify-template`,不需要打包者的单元测试;而测试夹具正是真实标识符
|
|
748
|
+
最容易沉淀的地方。
|
|
749
|
+
- 删除 `deploy/` 下的静态 plist,改由 `init` 运行时渲染。
|
|
750
|
+
|
|
751
|
+
### 安全
|
|
752
|
+
|
|
753
|
+
- **新增 advisory 警告通道**。`ServiceConfig.warnings()`,在 `doctor` 与
|
|
754
|
+
`blacklist list` 的输出里以 `warnings` 字段呈现。它报告的第一类问题是:
|
|
755
|
+
**写成工号的黑名单条目匹配不到任何人**——入站消息的 `sender_id` 恒为空,
|
|
756
|
+
只有 `sender_open_id` 有值,所以按工号写的屏蔽规则看起来生效、实际从未命中。
|
|
757
|
+
同时也报告空的 `owner_display_name`。
|
|
758
|
+
这些是**警告不是就绪错误**:条目本身无害只是不生效,而就绪错误会让运行中的实例整体停摆。
|
|
759
|
+
- **`init` 报告孤儿 plist**。离线安装时 label 走摘要,登录后 `--force` 重跑会换成
|
|
760
|
+
userId,旧 plist 留在原地;若它已 bootstrap 过,就是两个常驻实例共用一个状态库。
|
|
761
|
+
`init` 现在扫描 `~/Library/LaunchAgents/com.dingclaw.*.plist`,把不是本次写的列在
|
|
762
|
+
`other_dingclaw_labels` 与 `next_steps` 里并给出 bootout 命令。安装器只报告,不代为卸载。
|
|
763
|
+
- **所有出站消息带 AI 标识**。新配置项 `ai_disclosure_text`(默认 `[AI 分身代发]`)
|
|
764
|
+
注入于唯一投递收口,覆盖文本、流式卡片、机器人三条路径;置空是就绪错误。
|
|
765
|
+
另有 `ai_tag_enabled`(默认 true)显式传递 dws 的 `--ai-tag`,不再依赖上游默认值——
|
|
766
|
+
上游若把默认翻成 false,角标会无声消失。
|
|
767
|
+
记流水账时记的是**实际发出的那份文本**,否则下一轮的自有消息认领必然落空。
|
|
768
|
+
- **修复 sdist 泄露打包者 openDingTalkId**。0.1.0 的 sdist 包含
|
|
769
|
+
`tests/test_ding_notifier.py`,其中写有实例所有者本人的 openDingTalkId。
|
|
770
|
+
- **强制黑名单双向断言**。怀虎的 userId 与 openDingTalkId 既豁免扫描,又必须存在于
|
|
771
|
+
交付包中;任一缺失即拒绝发布。这防的是交付时连同个人数据一起把黑名单清掉。
|
|
772
|
+
- **门禁不硬编码任何人名**。冒名规则的姓名由 `--owner-name` 传入(或环境变量
|
|
773
|
+
`DINGCLAW_OWNER_NAME`)。把姓名写进检查器等于让它携带自己要查的数据,且只对一个
|
|
774
|
+
打包人有效。未提供姓名时拒绝构建,而不是跳过该规则。
|
|
775
|
+
|
|
776
|
+
### 从 0.1.0 升级
|
|
777
|
+
|
|
778
|
+
0.1.0 从未对外交付,不存在需要迁移的第三方安装。但**打包者本机的既有实例需要人工迁移**,
|
|
779
|
+
下面三步都不能省。
|
|
780
|
+
|
|
781
|
+
#### 1. 先补黑名单,再升级(唯一一个安全方向的破坏性变更)
|
|
782
|
+
|
|
783
|
+
0.1.0 里有一个写死在代码里的强制屏蔽条目,0.2.0 删掉了。**升级后它不再生效。**
|
|
784
|
+
|
|
785
|
+
这里有个陷阱:那个人在 0.1.0 的 `config.json` 里也有一条对应记录,但**那条记录从来没有
|
|
786
|
+
命中过任何消息**——入站消息的 `sender_id` 恒为空,只有 `sender_open_id` 有值,
|
|
787
|
+
所以按工号写的条目匹配不到发送者。真正拦住他的一直是代码里那个硬编码的
|
|
788
|
+
openDingTalkId。删掉硬编码之后,**光靠配置里那条工号记录挡不住任何人,而且不会有任何报错**。
|
|
789
|
+
|
|
790
|
+
所以升级前必须做:把每一个要屏蔽的人的 **openDingTalkId**(不是工号)加进
|
|
791
|
+
`deny_user_ids`。取值与写入:
|
|
792
|
+
|
|
793
|
+
```
|
|
794
|
+
dws contact user search --query "<姓名>" --profile <corpId> --format json
|
|
795
|
+
dingclawd blacklist add --user-id <openDingTalkId>
|
|
796
|
+
```
|
|
797
|
+
|
|
798
|
+
用 `blacklist add` 而不是手改 JSON:配置是 0600,且写入要过锁与校验,
|
|
799
|
+
手改容易和运行中的实例互相覆盖。
|
|
800
|
+
|
|
801
|
+
升级后用 `dingclawd doctor` 确认,`warnings` 里不应再出现
|
|
802
|
+
`these deny_user_ids are not openDingTalkIds and cannot match an inbound sender`。
|
|
803
|
+
出现了就说明还有条目写的是工号,那些人此刻是可以收到分身回复的。
|
|
804
|
+
|
|
805
|
+
#### 2. 补三个新配置项
|
|
806
|
+
|
|
807
|
+
| 配置项 | 默认值 | 不补的后果 |
|
|
808
|
+
|---|---|---|
|
|
809
|
+
| `owner_display_name` | `本人` | agent 自述降级为「本人的工作数字分身」,不会冒用他人姓名 |
|
|
810
|
+
| `ai_disclosure_text` | `[AI 分身代发]` | 使用默认文案,行为正确;置为空串则是就绪错误,不会静默放行 |
|
|
811
|
+
| `ai_tag_enabled` | `true` | 无需补 |
|
|
812
|
+
|
|
813
|
+
其余配置项语义未变。`config.example.json` 现由 `onboarding.render_config` 生成,
|
|
814
|
+
全量键加默认值,与 `init` 的渲染表同源,可直接作为配置参考。
|
|
815
|
+
|
|
816
|
+
#### 3. 先卸载旧 launchd 任务,再加载新的
|
|
817
|
+
|
|
818
|
+
**这一步漏掉会导致两个实例同时跑,并争抢同一个 `state_path`。**
|
|
819
|
+
|
|
820
|
+
`deploy/` 下的静态 plist 已删除,plist 改由 `init` 在安装时渲染,label 也随之改变:
|
|
821
|
+
|
|
822
|
+
| | 旧 | 新 |
|
|
823
|
+
|---|---|---|
|
|
824
|
+
| label | `com.zhuoyue.gxd.dingclaw` | `com.dingclaw.` 加一段随使用者变化的后缀 |
|
|
825
|
+
| 来源 | 仓库里的静态文件 | `init` 运行时用 `plistlib` 渲染 |
|
|
826
|
+
|
|
827
|
+
**不要自己推算新 label**:后缀有三级回退(花名的 ASCII slug、owner 的 userId、名字的
|
|
828
|
+
8 位摘要),中文名走的是后两级。实际值以 `dingclawd init` 输出里的 `launchd_label`
|
|
829
|
+
字段为准,plist 文件名与它一致。
|
|
830
|
+
|
|
831
|
+
label 变了,所以 `launchctl bootstrap` 新任务**不会**替换旧任务,旧的也**不会**自动卸载。
|
|
832
|
+
必须按顺序执行(`<新 label>` 替换成 `launchd_label` 的实际值):
|
|
833
|
+
|
|
834
|
+
```
|
|
835
|
+
launchctl bootout gui/$(id -u)/com.zhuoyue.gxd.dingclaw
|
|
836
|
+
rm ~/Library/LaunchAgents/com.zhuoyue.gxd.dingclaw.plist
|
|
837
|
+
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/<新 label>.plist
|
|
838
|
+
```
|
|
839
|
+
|
|
840
|
+
确认只剩一个实例:`launchctl list | grep dingclaw` 应只有一行。
|
|
841
|
+
|
|
842
|
+
## [0.1.0]
|
|
843
|
+
|
|
844
|
+
本机单实例运行时。轮询、门禁分层、上下文、DING 提醒、流式卡片、事件订阅。
|
|
845
|
+
未做交付参数化,仅在打包者本机可用。
|