v3.8.0 发布说明
本次 minor 版本带来一项新能力:定向表单收集 —— dingtalk_ask_user_question 现在可以把表单投递给指定用户或指定群成员,并在收齐或超时后把结果回传到原发起会话。
同时,本版本完成以 ClawHub 审计为驱动的第二轮安全收口,共四项默认暴露面收紧:Gateway RPC 能力面默认关闭、学习回路成为真正的总开关并引入规则生命周期、出站媒体在未配置边界时拒绝直读主机文件、插件源码不再直接访问进程环境;随包发布依赖的已知漏洞也从 47 条清零。
Important
本版本包含三项破坏性默认值变更(Gateway RPC、学习回路、出站媒体主机读取)。升级前请对照下方「破坏性变更」与「升级检查清单」完成配置迁移。最低宿主版本保持 OpenClaw 2026.8.1 不变,无需连带升级宿主。
最新版本入口:latest.md
⚠️ 破坏性变更
1. Gateway RPC 能力面默认关闭
v3.7.0 引入的 gatewayCapabilities 在 v3.8.0 由默认开启改为默认关闭。
PR #617(by @soimy)把gatewayCapabilities.tools.docs与gatewayCapabilities.tools.proactiveSend的默认值由true改为false,且要求显式true才生效(=== true判定)。- 拒绝信息会直接点名需要开启的配置键,便于定位,例如:
dingtalk docs Gateway RPC is disabled (gatewayCapabilities.tools.docs defaults to false); set gatewayCapabilities.tools.docs = true to enable it dingtalk-connector.status与dingtalk-connector.probe不受影响,无需额外配置。- 白名单语义不变:
gatewayCapabilities.docs.allowedSpaceIds与gatewayCapabilities.send.allowedTargets未配置时不限制;配置后至少一项,[]会被 schema 直接拒绝,运行时按 fail-closed 全部拒绝。 - 多账号仍按子键(
tools/docs/send)与渠道级合并,账号级同子键优先。
迁移:升级后如需继续使用 docs 或主动发送 RPC,请显式开启对应开关,并按需配置 allowedSpaceIds / allowedTargets。
文档更新:docs/user/reference/gateway-rpc.md、docs/user/reference/security-policies.md(含默认值矩阵、最小权限示例与升级提示)、README.md。
2. 学习回路:learningEnabled 成为真正的总开关,并引入规则生命周期
此前 learningEnabled 只拦截 prompt 注入,/learn 写入与强制回复(forcedReply)短路仍然照常运行。v3.8.0 把它收敛为真正的 kill switch,并给持久化规则加上生命周期与作用域边界。
- 总开关生效范围(
PR #617,by @soimy):- 关闭时(默认)
/learn的写入命令被拒绝(global/session/here/target/targets/target-set-*),且已持久化的规则不再命中强制回复,消息回落到正常 agent 流程。 - 只读与清理命令保持可用,运维仍可在关闭状态下检查与删除历史规则。
- 关闭时(默认)
- 规则有效期
learningRuleTtlMs:默认30 天,规则自最后更新起超过该窗口即停止命中(注入与强制回复两条路径同时失效);设为0关闭过期。 - 账号级规则需显式 opt-in
learningAllowManualGlobalRules:默认false。- 关闭时
/learn global的写入被直接拒绝(而不是"写入成功但不生效",避免给出误导性成功回执),并提示会话级替代命令/learn here、/learn target、/learn targets、/learn target-set apply。 - 已存在的账号级规则不再注入 prompt、也不再强制回复。
- 关闭时
- 命中可审计:命中强制回复时输出
[DingTalk][Learning][ForcedReply] ruleId=… scope=… target=…(不记录回复正文)。 - 判定入口唯一化:新增
resolveLearnedRulePolicy()/resolveLearnedRuleState(),prompt 注入、强制回复、/learn list三条路径共用同一判定,规则不会在三处漂移;/learn list会把不生效的规则渲染为enabled, not applied (<reason>),原因取disabled/learning-disabled/expired/global-rules-disabled。 - 文档与权限表述修正:明确只有
commands.ownerAllowFrom中的 senderId 才能写入规则;openclaw.plugin.json的learningEnabled描述与 WebUI hint 说明写入会被拒绝、已存规则不会命中,以及 owner 规则可强制精确回复、绕过模型。 - prompt 卫生调整:注入块不再要求模型对用户隐瞒"规则来自存储",只保留"不必逐条复述规则内容",与本版本"任何规则都不静默改写回复"的承诺保持一致。
文档更新:docs/user/features/feedback-learning.md(新增「开关语义」与精确回复形式章节)。
3. 出站媒体:未配置 mediaLocalRoots 时不再直接读取主机文件
ClawHub 审计在 #617 的合并前审计中给出一条独立 finding:普通机器人回复可能导致本地文件在没有明确本地路径边界的情况下被上传。代码核对确认属实(Issue #619):未配置 mediaLocalRoots 时 resolveAllowedHostReadPath() 原样返回调用方路径,readMediaBuffer() 随后直接 fsPromises.readFile()。出站媒体路径来自模型产出的回复,因此回复里提到任意宿主路径都可能被当作附件上传到钉钉。
PR #621(by @soimy)把顺序调整为「插件自有临时文件 → 允许;其余情况需要 roots」,mediaLocalRoots缺失时返回undefined,边界判定交回宿主 runtime media bridge。- 未配置与显式空数组
[]语义不同:未配置时 bridge 回落到宿主默认 roots;显式[]表示不授权任何普通本地 root,比未配置更严格。 readMediaBuffer()删除"有 roots 才校验"的分支,一律走readVerifiedHostFile()(O_NOFOLLOW+ 打开后持句柄复校验包含性),不再存在未经校验的直读代码路径。- 封住同类问题的第二个口子:
.ogg/.amr语音时长探测此前把调用方路径直接交给 ffprobe,完全绕过边界;现在先经stageHostMediaForExternalTool()落到插件自有临时文件再探测,探测完成即清理(调用方已提供经边界校验的preReadBuffer时直接复用,避免二次读取)。
迁移:未配置 mediaLocalRoots 的宿主不再获得出站媒体的直接主机读取,一律经由 runtime media bridge。若升级后本地媒体发送失败,请在宿主侧为出站媒体提供允许目录。文档更新:docs/user/reference/security-policies.md。
默认值变更速查
| 配置项 | v3.7.0 默认 | v3.8.0 默认 | 需要该能力时 |
|---|---|---|---|
gatewayCapabilities.tools.docs
| true
| false
| 显式设为 true,并按需配置 docs.allowedSpaceIds
|
gatewayCapabilities.tools.proactiveSend
| true
| false
| 显式设为 true,并按需配置 send.allowedTargets
|
learningEnabled
| false(仅拦截注入)
| false(拒绝写入 + 已存规则不命中)
| 设为 true
|
learningRuleTtlMs
| 不存在 | 2592000000(30 天)
| 调大该值,或设为 0 关闭过期
|
learningAllowManualGlobalRules
| 不存在 | false
| 设为 true;否则改用 /learn targets、/learn target-set apply
|
mediaLocalRoots 未配置时的出站媒体
| 直接读取主机文件 | 拒绝直读,改走 runtime media bridge | 配置允许目录 |
✨ 功能与体验改进
定向表单收集
dingtalk_ask_user_question支持向指定用户或群成员独立收集表单PR #618(by @RowanZhong)新增可选target,解决"从 A 的私聊请求 B、或请求群内指定成员填写,再把结果返回 A"的场景。未传target时完整保留原有当前用户表单的生命周期与失效规则。- 投放与回传上下文分离:
target支持用户{ "type": "user", "id": "<staffId>" }或群{ "type": "group", "id": "<conversationId>", "respondentUserIds": ["<staffId>", …] }。群名单必须包含 1–50 个不重复的真实 staffId,不接受通配或公开表单,也不从显示名反推 ID;结果只回传到发起会话与原 agent。 - 权限与回调校验:按机器人账号与填写人校验回调;发起人不会自动获得填写权限;定向卡片关闭转发。每人只接受首次提交或取消,单人取消不结束其他人的填写。
- 表单彼此独立:定向收集不会因普通聊天、新建定向表单或当前用户表单而失效。
timeoutMinutes支持1–1440分钟(默认5);收齐即提前结束,超时返回部分结果及未回应名单。 - 发起人可管理:
action=list列出当前发起人在原会话、原 agent 下的待收集表单;action=cancel使用真实questionId取消指定表单,并通过当前工具结果返回部分收集结果,不重复注入会话。两者都校验账号、发起人、会话与 agent。schema 按 action 区分(create需questions或fields,cancel需非空questionId,list无需表单字段),并用 Ajv 验证(仅开发依赖)。 - 卡片展示:群卡片使用标题右侧现有状态标签显示
1/2等进度,保留原填写说明,不公开成员答案;卡片进度与终态串行更新,处理重复回调及取消/超时竞争。本次不修改已发布卡片模板,原模板说明区仍有两行省略限制。 - webhook 有效期处理:定向收集保存原始 webhook 有效期并在实际发送时重新检查(预留 30 秒)。过期或有效期未知时改由主动消息回到原发起私聊/群,覆盖"模型生成期间跨过有效期"的情况;未指定
target的回复行为不变。 - 第三方答案信任边界:定向汇总统一拆分为固定汇总指引与第三方结构化数据,使用公开 SDK
wrapExternalContent添加来源警告与边界。继续消息会清空命令正文、关闭命令授权与解释,并跳过正文触发的路由、插件命令、停止/BTW 分类与学习上下文;原账号、会话、agent 路由不变。通过BodyForAgent保留 OpenClaw 2026.8.1 普通、排队及历史路径中的包装数据。该修复隔离第三方数据来源并禁止把定向结果直接解释为用户命令,不是无工具沙箱,也不声称完全消除提示注入。 - 原生运行器兼容:修复 OpenClaw 2026.8.1 原生运行器的可信会话键匹配与"复用工具持有旧上下文"问题;活动 dispatch 结束后清理,拒绝跨身份或歧义匹配。
- 已知边界:网关或钉钉通道重启会终止未完成表单(包括电脑睡眠后宿主健康检查触发的通道重启);答案不持久化、不在重启后恢复。未指定
target的当前用户表单仍固定 5 分钟。从群聊发起时结果回到该群,私密收集应从私聊发起。长时主动回传需要机器人具备向原发起用户或群发送主动消息的权限。 - 文档更新:
docs/user/features/form-interactive-card.md。
🛠 安全与修复
-
插件不再直接访问进程环境(
PR #614,by @soimy)- 上一轮已解决"把整个
process.env透传给解析器"的形态,但收敛后的实现仍由插件自己索引进程环境(process.env[ref.id]),静态规则只看"环境变量访问 + 网络发送"这一形态,无法区分是否授权,因此suspicious.env_credential_access依旧成立。 - 现在
env分支重构为「本地只读路径授权预检 + 调用宿主resolveReadOnlyEnvSecretRef取值」,凭据读取与所有权完全留在宿主;未授权时不调用宿主解析器,直接本地失败并给出allowlist修复指引。宿主把"未授权"与"已授权但未设置/为空"都折叠为blocked,因预检已排除前者,该分支可确定地解释为"已授权但未设置/为空",报错文案与上一轮保持一致。 file分支与字面量引用语义、授权规则、报错时机(发起任何钉钉请求之前)均不变。- 新增
scripts/ambient-env-guard.mjs:基于 TypeScript 语法树而非文本匹配的环境读取守卫,覆盖process.env[<expr>]、process?.env.X、process["env"].X、globalThis.process.env.X、const { env } = process、计算属性名解构、object-rest、别名(const p = process/fallback || process/(0, process))、import { env } from "node:process"、require("node:process")与动态键;同时不误报process.platform/process.cwd()/process.pid/typeof process,以及已登记的DINGTALK_CARD_TEMPLATE_ID读取。守卫同时用于发布前产物校验(pack:check,两条发布 workflow 都会执行)与源码级测试,避免两处判定漂移。 - 按设计不做数据流追踪;运行时字符串拼接、
Function构造等边界已在守卫与security-policies.md中写明。PR CI 增加真实 bundle 的pnpm run build+pnpm run pack:check。 - 文档同步:
docs/user/getting-started/configure.md、docs/user/getting-started/update.md、docs/user/reference/configuration.md、docs/user/reference/security-policies.md的表述改为"单键读取发生在宿主只读解析器内部,插件源码不读取该变量",并说明发布前守卫强制该约束。
- 上一轮已解决"把整个
-
随包发布依赖的已知漏洞清零(
PR #623,by @soimy)pnpm audit --prod在改动前实测 47 条 advisory(25 high / 21 moderate / 1 low),去重后 4 个模块,且全部属于会随包发布的依赖;这是确定性结论,只要不升级审计就会持续返回suspicious。- 升级:
axios1.13.6→^1.20.0、form-data^4.0.0→^4.0.6;并把传递依赖ws与@xmldom/xmldom提为显式直接依赖以把解析版本固定住(ws8.21.3、@xmldom/xmldom0.8.15,不跨到0.9.x),follow-redirects随 axios 一并升至1.16.0。锁文件确认每个模块只剩一个解析版本,没有被旧版本旁路。 - 唯一的代码适配:axios 1.20 收紧了
AxiosResponse.headers的类型,入站媒体下载处新增normalizeAxiosHeaderValue()把content-type收窄为字符串后再交给saveMediaBuffer。仓库只用到axios.get/post/put、axios.create({ timeout })与isAxiosError,未使用拦截器等易受影响的 API,请求语义无改动。 - 变更后
pnpm audit --prod→ No known vulnerabilities found(47 → 0)。 devDependencies树(vitest/vite/rollup/openclaw自身的依赖树)仍有告警,但这些不随 npm 包发布,对 ClawHub 扫描与用户安装均不可见,属于独立维护任务,不在本版本范围。
-
发布说明文档链接修正:v3.7.0 发布说明内的文档链接改为绝对链接,避免在 VitePress 之外的渲染环境下失效。
🧩 架构与工程
-
ClawHub 安全审计改为手动触发,且不再阻断发布(
PR #617,by @soimy)- 背景:ClawHub 只在服务端、对已提交的版本运行 ClawScan,本地没有等效能力;此前每次确认审计结论都只能手工发一个版本再看网页,反馈链路很长。
PR #617最初把审计做成发布门禁(clawhub-publish.yml内audit+publish两个 job,publish依赖needs: audit),随后在同一版本周期内收敛为手动触发:审计只产出结论,clawhub-publish.yml不读取任何审计状态。 .github/workflows/clawhub-audit.yml(仅workflow_dispatch):以<version>-beta.<run>.<attempt>发布到专用auditdist-tag →clawhub package publish --wait等审计到终态 → 读取公开的版本精确信任接口GET /api/v1/packages/{name}/versions/{version}/security→ 按 P2 策略判定 → 下载报告 → 默认自动撤回审计版本并复查 404。- 审计版本固定使用
auditdist-tag:用beta会抢占正式 beta 通道;而clawhub package delete --version只能撤回非 latest 版本,因此也绝不能打成latest。全程不触碰latest/beta。 - 审计版本默认审计后自动清除(撤回失败或撤回后仍可解析会让该次 run 失败);需要真机烟测时用
keep_audit_version=true显式保留,之后手动撤回。allow_suspicious=true仍是 P2 人工放行入口——两者都是workflow_dispatch输入,run 记录会留下触发者与输入值。 scripts/clawhub-beta-gate.mjs实现 P2 策略:硬失败(malicious/blockedFromDownload/quarantined/revoked/pending/stale/not-run/ 未知scanStatus/ 响应结构不合法)一律 fail-closed;suspicious默认不通过,仅allow_suspicious=true放行并记为pass-with-override。trust的每个字段都先校验存在性与类型再使用,畸形响应(如blockedFromDownload: "true")不再被读成"无阻断信号"。scripts/clawhub-audit-local.mjs把同一套审计跑在本地工作树上,证据写入 gitignored 的.clawhub-audit/;默认同样自动撤回,--keep保留。clawhub-publish.yml保留"从远端重新解析 tag 并用^{commit}剥离、确认工作树就是该 tag 指向的提交"这一步,并把它作为--source-commit,避免发布与 tag 不一致的树。- 固定安装的 ClawHub CLI 由
0.23.1升至0.23.3:--wait与"可恢复的版本撤回"语义自0.23.2才有,0.23.1的package delete --version是永久删除且没有--wait。 - 审计结论是概率性的:同一份行为相同的产物出现过
clean与suspicious两种结论,因此单次clean不作为通行证,发版前的审计属于流程约定而非 CI 强制。详见docs/contributor/npm-publish.md。
- 背景:ClawHub 只在服务端、对已提交的版本运行 ClawScan,本地没有等效能力;此前每次确认审计结论都只能手工发一个版本再看网页,反馈链路很长。
-
manifest 默认值声明与漂移防护
openclaw.plugin.json为learningEnabled/learningAutoApply/learningNoteTtlMs/gatewayCapabilities.tools.docs/.proactiveSend/learningRuleTtlMs/learningAllowManualGlobalRules补齐default(顶层与账号级两份 schema 同步)并更新描述与 WebUI hint,让"默认关闭"在元数据层可读。tests/unit/plugin-manifest.test.ts把 manifest 声明的默认值绑定到getConfig()与resolveGatewayCapabilityConfig()的实际解析结果,防止声明与运行时再次漂移;src/platform/types.ts/config-schema.ts/config.ts中残留的 "default: true" 注释一并清理。
-
测试与工程:本次区间新增/扩充了大量回归用例,覆盖审计判定策略全分支、能力面判定、学习回路三条路径、媒体边界、环境读取守卫矩阵、定向表单权限与信任边界、webhook 有效期等。已知待办:
Issue #622(拆分超过 800 行的测试文件)仍为 open,不在本版本处理。
📋 升级检查清单
- 若使用
dingtalk.docs.*或dingtalk-connector.docs.*RPC:设置gatewayCapabilities.tools.docs = true - 若使用主动发送 RPC:设置
gatewayCapabilities.tools.proactiveSend = true - 建议同时配置
gatewayCapabilities.docs.allowedSpaceIds与gatewayCapabilities.send.allowedTargets(最小权限) - 若依赖反馈学习规则生效:设置
learningEnabled = true - 若依赖长期有效的学习规则:按需调大
learningRuleTtlMs,或设为0关闭过期 - 若使用
/learn global账号级规则:设置learningAllowManualGlobalRules = true,否则改用/learn targets - 若通过
mediaLocalRoots发送本地媒体:确认允许目录配置正确;未配置的部署现在经由 runtime media bridge - 宿主版本 ≥
OpenClaw 2026.8.1(未变化)
🤝 鸣谢
- @RowanZhong(
PR #618)—— 定向表单收集功能的设计与实现,包含多轮真机验证(跨用户私聊、私聊发起群内填写、多人收齐/超时/取消、独立收集生命周期、第三方答案信任边界与汇总展示),并同步更新用户文档与回归测试。 - @soimy(
PR #614、PR #617、PR #621、PR #623)
同时感谢问题反馈与审计跟进:
Issue #595(收敛 ClawHub 安全审计中的敏感读取与发布物暴露)Issue #608(ClawHub 安全审核因 SecretInput 与权限边界触发 Review)Issue #616(剩余suspicious来自 Gateway RPC 与学习回路的默认暴露)Issue #619(未配置mediaLocalRoots时出站媒体可读取任意宿主文件)Issue #620(ClawHub 发布门禁服务端 OOM,已判定为上游偶发)Issue #622(拆分超长测试文件,仍 open)
发布页面:https://github.com/soimy/openclaw-channel-dingtalk/releases/tag/v3.8.0
Full Changelog: v3.7.0...v3.8.0
{ "channels": { "dingtalk": { "gatewayCapabilities": { "tools": { "docs": true, "proactiveSend": true }, "docs": { "allowedSpaceIds": ["<spaceId>"] }, "send": { "allowedTargets": ["user:<staffId>", "group:<conversationId>"] } } } } }