github soimy/openclaw-channel-dingtalk v3.8.0

4 hours ago

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.docsgatewayCapabilities.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.statusdingtalk-connector.probe 不受影响,无需额外配置。
  • 白名单语义不变:gatewayCapabilities.docs.allowedSpaceIdsgatewayCapabilities.send.allowedTargets 未配置时不限制;配置后至少一项,[] 会被 schema 直接拒绝,运行时按 fail-closed 全部拒绝。
  • 多账号仍按子键(tools / docs / send)与渠道级合并,账号级同子键优先。

迁移:升级后如需继续使用 docs 或主动发送 RPC,请显式开启对应开关,并按需配置 allowedSpaceIds / allowedTargets

{
  "channels": {
    "dingtalk": {
      "gatewayCapabilities": {
        "tools": { "docs": true, "proactiveSend": true },
        "docs": { "allowedSpaceIds": ["<spaceId>"] },
        "send": { "allowedTargets": ["user:<staffId>", "group:<conversationId>"] }
      }
    }
  }
}

文档更新:docs/user/reference/gateway-rpc.mddocs/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.jsonlearningEnabled 描述与 WebUI hint 说明写入会被拒绝、已存规则不会命中,以及 owner 规则可强制精确回复、绕过模型。
  • prompt 卫生调整:注入块不再要求模型对用户隐瞒"规则来自存储",只保留"不必逐条复述规则内容",与本版本"任何规则都不静默改写回复"的承诺保持一致。

文档更新:docs/user/features/feedback-learning.md(新增「开关语义」与精确回复形式章节)。

3. 出站媒体:未配置 mediaLocalRoots 时不再直接读取主机文件

ClawHub 审计在 #617 的合并前审计中给出一条独立 finding:普通机器人回复可能导致本地文件在没有明确本地路径边界的情况下被上传。代码核对确认属实(Issue #619):未配置 mediaLocalRootsresolveAllowedHostReadPath() 原样返回调用方路径,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 区分(createquestionsfieldscancel 需非空 questionIdlist 无需表单字段),并用 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.Xprocess["env"].XglobalThis.process.env.Xconst { 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.mddocs/user/getting-started/update.mddocs/user/reference/configuration.mddocs/user/reference/security-policies.md 的表述改为"单键读取发生在宿主只读解析器内部,插件源码不读取该变量",并说明发布前守卫强制该约束。
  • 随包发布依赖的已知漏洞清零PR #623,by @soimy

    • pnpm audit --prod 在改动前实测 47 条 advisory(25 high / 21 moderate / 1 low),去重后 4 个模块,且全部属于会随包发布的依赖;这是确定性结论,只要不升级审计就会持续返回 suspicious
    • 升级:axios 1.13.6^1.20.0form-data ^4.0.0^4.0.6;并把传递依赖 ws@xmldom/xmldom 提为显式直接依赖以把解析版本固定住(ws 8.21.3@xmldom/xmldom 0.8.15,不跨到 0.9.x),follow-redirects 随 axios 一并升至 1.16.0。锁文件确认每个模块只剩一个解析版本,没有被旧版本旁路。
    • 唯一的代码适配:axios 1.20 收紧了 AxiosResponse.headers 的类型,入站媒体下载处新增 normalizeAxiosHeaderValue()content-type 收窄为字符串后再交给 saveMediaBuffer。仓库只用到 axios.get/post/putaxios.create({ timeout })isAxiosError,未使用拦截器等易受影响的 API,请求语义无改动。
    • 变更后 pnpm audit --prodNo 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.ymlaudit + publish 两个 job,publish 依赖 needs: audit),随后在同一版本周期内收敛为手动触发:审计只产出结论,clawhub-publish.yml 不读取任何审计状态。
    • .github/workflows/clawhub-audit.ymlworkflow_dispatch):以 <version>-beta.<run>.<attempt> 发布到专用 audit dist-tag → clawhub package publish --wait 等审计到终态 → 读取公开的版本精确信任接口 GET /api/v1/packages/{name}/versions/{version}/security → 按 P2 策略判定 → 下载报告 → 默认自动撤回审计版本并复查 404
    • 审计版本固定使用 audit dist-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-overridetrust 的每个字段都先校验存在性与类型再使用,畸形响应(如 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.1package delete --version 是永久删除且没有 --wait
    • 审计结论是概率性的:同一份行为相同的产物出现过 cleansuspicious 两种结论,因此单次 clean 不作为通行证,发版前的审计属于流程约定而非 CI 强制。详见 docs/contributor/npm-publish.md
  • manifest 默认值声明与漂移防护

    • openclaw.plugin.jsonlearningEnabled / 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.allowedSpaceIdsgatewayCapabilities.send.allowedTargets(最小权限)
  • 若依赖反馈学习规则生效:设置 learningEnabled = true
  • 若依赖长期有效的学习规则:按需调大 learningRuleTtlMs,或设为 0 关闭过期
  • 若使用 /learn global 账号级规则:设置 learningAllowManualGlobalRules = true,否则改用 /learn targets
  • 若通过 mediaLocalRoots 发送本地媒体:确认允许目录配置正确;未配置的部署现在经由 runtime media bridge
  • 宿主版本 ≥ OpenClaw 2026.8.1(未变化)

🤝 鸣谢

  • @RowanZhongPR #618)—— 定向表单收集功能的设计与实现,包含多轮真机验证(跨用户私聊、私聊发起群内填写、多人收齐/超时/取消、独立收集生命周期、第三方答案信任边界与汇总展示),并同步更新用户文档与回归测试。
  • @soimyPR #614PR #617PR #621PR #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

Don't miss a new openclaw-channel-dingtalk release

NewReleases is sending notifications on new releases.