From 39b46a95b37bc7aa4fd683c455d2740e31600643 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Mon, 18 May 2026 22:05:40 +0800 Subject: [PATCH 01/44] docs(spec): gap #01 DingTalk native approval design MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 gap-01 从分析升级为可执行设计:14 项决策(D1-D14)、9 个新模块的职责分工、 Capability SDK 工厂参数、5 个 native runtime sub-adapter 详解、6 类数据流场景 (含 dedupeKey 去重、both-surface 同步、点击解码到上游 resolve、过期/取消/兜底)、 卡片模板字段与五态 mockup(pending / approved / rejected / expired / plugin-DM)、 10 项错误处理矩阵、115+ 测试用例规划、3 阶段实施路径。 按钮 payload 直接复用上游 /approve 命令字面量,与 Telegram 同 pattern; 卡片模板 ID 内置常量、零 env 配置,对齐 D9 用户零部署摩擦原则。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 1199 +++++++++++++++++ 1 file changed, 1199 insertions(+) create mode 100644 docs/features/2026-05-18-gap-01-approval-native-design.html diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html new file mode 100644 index 00000000..1d2d1906 --- /dev/null +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -0,0 +1,1199 @@ + + + + + + Gap #01 · DingTalk Native Approval 设计方案 + + + +
+ + + +
+

1. 总览

+

+ DingTalk Channel 当前 src/channel.ts:22-127 没有声明 approvalCapability,所以 OpenClaw 的 exec / plugin 审批流程在钉钉用户那里完全走不通——agent 想跑命令时只能在 WebUI / 终端 UI 里批,从钉钉群里发起的 agent 任务等于 dead-end。本设计填补这一空缺,让钉钉群(origin)与 approver 私聊(DM)两端都能直接收到三按钮卡片,点击即可完成审批。 +

+ +
+ 本设计的核心原则 +
    +
  • 不做一半:exec + plugin 两类 approval 一次到位,5 个 native runtime sub-adapter 全部实现
  • +
  • 与 peer 对齐:所有可对照决策(surface / approver schema / ID 不短化 / slash 命令复用上游)都跟 Discord/Telegram/Slack 一致,不为 DingTalk 发明独有约定
  • +
  • 钉钉特性最大化:审批永远用 DingTalk 互动卡片(独立模板)渲染,与 messageType: card | markdown 配置无关
  • +
  • 上游约定为权威:按钮点击解码后回写到 exec.approval.resolve / plugin.approval.resolve gateway method,与用户手敲 /approve <id> <decision> 走同一条解析链
  • +
  • 用户零部署摩擦:审批卡片模板 ID 内置常量,无环境变量配置;只需把 approver staffId 列表写进 channels.dingtalk.execApprovals.approvers 即可启用
  • +
+
+ +

1.1 范围明确不做的事

+
    +
  • 不抽通用 action dispatcher / registry —— peer 三家都没做,approval 走自己的回调前缀分支,与 feedback_up/down / btn_stop 在 TOPIC_CARD listener 中同级并列
  • +
  • 不动 btn_stop 与 feedback 既有路径(向后兼容,零回归风险)
  • +
  • 不为 markdown 模式做"教用户打 /approve 命令"的主路径文案——卡片是唯一推广 UX
  • +
  • 不做重启后主动 rebind pending approval(v1 范围;用户点过期/失效按钮时显式降级提示)
  • +
  • 不引入 select / input / datepicker 等高级组件——仅 button(已 CONFIRMED 平台支持)
  • +
+
+ + +
+

2. 已确认的决策清单

+

本节是设计的 single source of truth,下文所有 section 的实现细节都基于这 10 条决策。任何变更需要回到这里更新。

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
#决策点选定方案关联 peer
D1实现范围Full native runtime(5 个 sub-adapter 全做)Discord / Slack / Telegram
D2Slash 命令纯复用上游 /approve <id> <decision>,channel 端零注册三家完全一致
D3Approval ID 形式全 ID 不短化(DingTalk 无 byte 限制;Telegram 短到 8 字符仅因 64 字节硬限制)Discord / Slack
D4Surface 默认值both + notifyOriginWhenDmOnly=true三家完全一致
D5自批准(self-approval)允许(clicker 在 approver 名单里即可,不查 originator vs clicker)三家完全一致
D6Origin == DM 去重transport.prepareTarget 返回的 dedupeKey = accountId:target.toSDK 标准机制
D7Approver schema仅 staffId(含 dingtalk:/dd:/ding: 前缀剥离);多账号 override;fallback 到 commands.ownerAllowFromenabled: auto三家完全一致
D8Approval 类别exec + plugin 一次到位(eventKinds: ["exec", "plugin"]三家完全一致
D9卡片模板新建 approval 专用模板(独立 templateId),纯常量内置,无 env 覆盖;JSON 源 commit 到 docs/assets/—(DingTalk 特化)
D10渲染策略所有 approval 永远用 approval 卡片,与 messageType 无关;markdown/text 仅作 createAndDeliver 失败时的 error-recovery 兜底三家完全一致
D11TTL 归属上游 approval store 管 expiresAtMs;channel 端不跑自己的 timer,跟随上游事件SDK 标准
D12重启恢复v1 不主动 rebind;遗留卡片的按钮点击触发"过期/失效"显式降级提示—(v1 范围)
D13停机取消复刻现有 finalizeActiveCardsForAccount 模式,stopClient 时把所有 pending approval 卡片刷成"已取消(通道断开)"—(DingTalk 特化)
D14终态展示kind: "update"——PUT /v1.0/card/instances 改 statusFooter + 隐藏按钮组,卡片保留Slack 同款
+
+ + +
+

3. 架构与模块布局

+ +

3.1 上下游分工

+
┌──────────────────────────── upstream OpenClaw ────────────────────────────┐
+│                                                                            │
+│  exec-approval / plugin-approval store ─┐    (createApprovalRequest)       │
+│                                          │                                  │
+│  approval-handler-runtime ────────────► nativeRuntime 5 子 adapter         │
+│  (src/infra/approval-handler-runtime.ts) ▲      (DingTalk channel 实现)    │
+│                                          │                                  │
+│  resolve gateway ◄───────────────────────┤      调上游回写                  │
+│   ├─ exec.approval.resolve { id, decision }                                │
+│   └─ plugin.approval.resolve { id, decision }                              │
+│                                                                            │
+│  parseExecApprovalCommandText(text)  ◄────  按钮 payload 字面复用          │
+│   (用户敲 /approve   也走这里)                                │
+│                                                                            │
+└───────────────────────────────────┬────────────────────────────────────────┘
+                                    │ adapter 实现
+                                    ▼
+┌──────────────────── openclaw-channel-dingtalk (本仓库) ────────────────────┐
+│                                                                            │
+│  src/approval/  ── 新增 domain 目录                                         │
+│    ├─ approval-capability.ts            ApprovalCapability 单例装配         │
+│    ├─ approval-native-runtime.ts        5 子 adapter 入口(lazy load)      │
+│    ├─ approval-card-template.ts         模板 ID 常量 + cardParamMap helper  │
+│    ├─ approval-card-render.ts           pending / resolved / expired 渲染   │
+│    ├─ approval-fallback-render.ts       markdown 兜底(仅 error-recovery)  │
+│    ├─ approval-target-resolver.ts       origin / DM target 解析             │
+│    ├─ approval-callback-handler.ts      TOPIC_CARD 回调入口 approve: 分支   │
+│    ├─ approval-config.ts                execApprovals.* schema 读写         │
+│    ├─ approval-store.ts                 内存 pendingEntry (outTrackId↔id)   │
+│    └─ approval-cancel.ts                stopClient 时 finalize 所有 pending │
+│                                                                            │
+│  改造点(增量、向后兼容)                                                    │
+│    ├─ src/channel.ts                    新增 approvalCapability 字段        │
+│    ├─ src/config-schema.ts              新增 execApprovalsSchema           │
+│    ├─ src/gateway/channel-gateway.ts    TOPIC_CARD listener 加 approve 分支│
+│    ├─ src/card/card-template.ts         加 BUILTIN_APPROVAL_CARD_TEMPLATE_ID│
+│    └─ src/types.ts                      加 ApprovalCardEntry / Decision    │
+│                                                                            │
+│  资产                                                                       │
+│    ├─ docs/assets/card-template-approval-v1.json       低代码 schema       │
+│    ├─ docs/assets/card-template-approval-v1-source.md  字段语义说明         │
+│    └─ docs/user/features/exec-approval.md              用户配置指南          │
+│                                                                            │
+└────────────────────────────────────────────────────────────────────────────┘
+ +

3.2 模块单一职责表

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
模块单一职责主要依赖预计行数
approval-capability.tscreateApproverRestrictedNativeApprovalCapability 工厂把所有零件组装成单例 ChannelApprovalCapabilitySDK 工厂 + 其它 8 个 approval-* 模块~80
approval-native-runtime.ts实现 5 个子 adapter,用 createLazyChannelApprovalNativeRuntimeAdapter 懒加载render / store / target-resolver / card-service~220
approval-card-template.tsBUILTIN_APPROVAL_CARD_TEMPLATE_ID 常量 + buildApprovalCardParamMap() 把 view+phase 转 KV~120
approval-card-render.ts给 request+phase 输出 cardParamMap(pending/resolved/expired/canceled × exec/plugin)approval-card-template~180
approval-fallback-render.ts仅 createAndDeliver 失败时调用,生成 markdown/text 兜底正文approval-config~60
approval-target-resolver.tsresolveOriginTarget + resolveApproverDmTargets,从 request 与 config 提取 DingTalk targetnormalizeDingTalkTarget, config~120
approval-callback-handler.tsTOPIC_CARD 收到 "/approve …" 前缀的 actionId 时:parse → authorize → resolve gateway → ackgateway primitive, capability auth, store~150
approval-config.ts纯读 helper:getExecApprovalsConfig / listExecApprovers / isExecAuthorizedSender / resolveNativeDeliveryModeconfig 模块~90
approval-store.tsper-process Map:approvalId ↔ entry,支持 register/get*/remove/finalizeAll~80
approval-cancel.tsfinalizeActiveApprovalCardsForAccount(accountId, reason):扫 store + PUT update 为"已取消(通道断开)"approval-store, card-service~70
+

合计新增 ~1170 行业务代码;测试代码预计 ~2200 行(按 2× 比例,含 fixture)。

+ +

3.3 与现有代码的接触面

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
文件改动风险
src/channel.ts:22-127plugin 对象新增 approvalCapability 字段(1 行 import + 1 行赋值)极低
src/config-schema.ts新增 execApprovalsSchema,加到 DingTalkConfigSchema 与 account override schema
src/gateway/channel-gateway.ts:330-407TOPIC_CARD listener 在调 handleCardAction 之前插入 tryHandleApprovalCallback();命中则 return,否则继续既有路径中(需谨慎保持 feedback / btn_stop 行为不变)
src/card/card-template.ts新增 BUILTIN_APPROVAL_CARD_TEMPLATE_ID 常量(与 AI Card 常量并列)极低
src/types.ts新增 ApprovalCardEntry / ApprovalDecision / ApprovalPhase 类型极低
+ +
+ 明确不修改的文件(向后兼容保证) +
    +
  • src/card/card-action-handler.ts(btn_stop 不动)
  • +
  • src/card-callback-service.tsextractCardActionId 已能取任意字符串)
  • +
  • src/feedback-learning-service.ts(与 approval 完全不交叉)
  • +
  • 所有 src/reply-strategy-*(approval 不进 agent reply 路径,独立卡片)
  • +
+
+
+ + +
+

4. Capability 装配

+ +

4.1 用 SDK 工厂的输入清单

+

createApproverRestrictedNativeApprovalCapabilityopenclaw/src/plugin-sdk/approval-delivery-helpers.ts:30-261)一次组装出完整 ChannelApprovalCapability。下面是 DingTalk 端要传的 16 个参数:

+ + + + + + + + + + + + + + + + + + + +
参数必填DingTalk 实现
channel"dingtalk"
channelLabel"DingTalk"
listAccountIds复用 dingtalkPlugin.config.listAccountIds
hasApproverslistExecApprovers({ cfg, accountId }).length > 0
isExecAuthorizedSenderisExecAuthorizedSender({ cfg, accountId, senderId }),clicker staffId 在名单中
isPluginAuthorizedSender可选默认 = isExecAuthorizedSender(同一份 approver 名单管两类)
isNativeDeliveryEnabled检查 execApprovals.enabled !== false && hasApproversauto 视为 true)
resolveNativeDeliveryModeexecApprovals.target"both" | "dm" | "channel"),默认 "both"
requireMatchingTurnSourceChannel可选false(允许 CLI/WebUI 触发的 approval 也通过 DingTalk 投递)
resolveOriginTarget可选详见 §6.1:从 turnSourceTo + turnSourceThreadId 还原 DingTalk target
resolveApproverDmTargets可选详见 §6.1:approver staffId 列表 → 每个 staffId 一条 { to: "user:<staffId>" }
notifyOriginWhenDmOnly可选true(D4)
nativeRuntime可选createLazyChannelApprovalNativeRuntimeAdapter({ load, isConfigured, shouldHandle, eventKinds: ["exec","plugin"] }),详见 §5
describeExecApprovalSetup可选返回中文配置指南字符串,引导用户填 channels.dingtalk.execApprovals.approvers
+ +

4.2 配置 schema(D7 落地)

+
# channel-level(也是 default account 的配置)
+channels:
+  dingtalk:
+    # 现有字段(不变)
+    clientId: "..."
+    clientSecret: "..."
+    messageType: card
+
+    # 新增:execApprovals 块
+    execApprovals:
+      enabled: auto             # true | false | auto(auto = approvers 非空时启用)
+      approvers:                # staffId 列表,支持 dingtalk:/dd:/ding: 前缀
+        - "staff001"
+        - "dingtalk:staff002"   # ← 等价于 "staff002"
+      target: both              # both | dm | channel(默认 both)
+      ttlMs: 600000             # 可选 channel-level TTL hint;不传时跟随上游默认(建议留空)
+
+    # 多账号 override
+    accounts:
+      acme:
+        execApprovals:
+          approvers: ["staff100"]   # 完全替换 channel-level 名单
+          target: dm                # 此账号偏好 DM-only
+
+# 全局 fallback(peer 惯例;与 dingtalk 无关,由上游统一处理)
+commands:
+  ownerAllowFrom: ["staff999"]
+ +
+ 配置语义细节 +
    +
  • enabled: auto = approvers 非空则启用;显式 false 优先级最高(即使有 approvers 也禁用)
  • +
  • 多账号 override 完全替换名单(不与 channel-level 合并),与现有 allowFrom 的语义保持一致
  • +
  • fallback 优先级:account-level approvers → channel-level approverscommands.ownerAllowFrom → 空
  • +
  • 所有 staffId 在写入时 normalize:raw.replace(/^(dingtalk|dd|ding):/i, "")(与 src/channel.ts:86 dmPolicy 完全一致)
  • +
+
+
+ + +
+

5. 5 个 Sub-Adapter 详解

+

上游 ChannelApprovalNativeRuntimeAdapteropenclaw/src/infra/approval-handler-runtime-types.ts:216-235)定义了 5 个子接口,approval-runtime 按下面顺序调用它们完成"投递 / 更新 / 终态"。本节给每个子接口的 DingTalk 实现锚到具体函数与代码骨架。

+ +

5.1 availability

+ + + + + + + + + + + + +
方法DingTalk 实现
isConfigured({ cfg, accountId })返回 isNativeDeliveryEnabled({ cfg, accountId })——即 enabled !== false && hasApprovers
shouldHandle({ cfg, accountId, request })三连判:(1) isConfigured 为 true;(2) request 的 turnSourceChannel 为空 OR 为 "dingtalk";(3) approver 名单非空
+ +

5.2 presentation

+ + + + + + + + + + + + + + + + +
方法DingTalk 实现
buildPendingPayload({ request, nowMs, view })approval-card-render.buildPendingCardParamMap(request, view),返回 { kind: "card", templateId, cardParamMap, actionIds }
actionIds 是 3 个:"/approve <id> allow-once"、"/approve <id> allow-always"、"/approve <id> deny"
buildResolvedResult({ request, resolved, view, entry })返回 { kind: "update", payload: buildResolvedCardParamMap(request, resolved) }。payload 把 statusFooter 改成"✅ 已批准 by @<resolverDisplayName>"或对应的拒绝文案;buttonGroupVisible=false
buildExpiredResult({ request, view, entry })返回 { kind: "update", payload: buildExpiredCardParamMap(request) }。statusFooter = "⏰ 已过期(未在 <X> 分钟内响应)";buttonGroupVisible=false
+ +

5.3 transport

+ + + + + + + + + + + + + + + + + + + + +
方法DingTalk 实现
prepareTarget({ plannedTarget, request, view, pendingPayload })plannedTarget.tonormalizeApprovalTargetTo(§6.1)确保带 user:/group: 前缀;返回 { target: { to: normalizedTo, threadId: null }, dedupeKey: \`${accountId}:${normalizedTo}\` }。DingTalk 无 thread 概念,threadId 永远 null。
deliverPending({ preparedTarget, request, view, pendingPayload })根据 preparedTarget.target.to 的前缀分支:
group:cid_xxxcard-service.createAndDeliver({ openConversationId: stripPrefix(target.to), ... })
user:<staffId>card-service.createAndDeliver({ userIds: [stripPrefix(target.to)], ... })
统一参数:outTrackId = \`approval-${request.id}-${hash(target.to)}\`(hash 避免 outTrackId 含冒号导致 PUT 路径解析问题),templateId = BUILTIN_APPROVAL_CARD_TEMPLATE_IDcardParamMap 来自 buildPendingPayloadcallbackType: "STREAM"
成功:approval-store.register({ approvalId, outTrackId, accountId, target: preparedTarget.target, deliveredAt: Date.now(), kind: request.kind }),返回 entry。
失败:observe.onDeliveryError + 调 approval-fallback-render 发 markdown/text 兜底(仍写一条 fallback entry 到 store 留痕)
updateEntry({ entry, payload, phase })updateCardVariables(entry.outTrackId, payload.cardParamMap, token)(即 PUT /v1.0/card/instances)。phase=expired/resolved 时同时 approval-store.remove(entry.approvalId)
deleteEntry({ entry, phase })not used(D14: 永远 update,从不 delete)。占位实现:仅 approval-store.remove
+ +

5.4 interactions(可选,但本设计实现)

+ + + + + + + + + + + + + + + + + + + + +
方法DingTalk 实现
bindPending({ entry, request, view, pendingPayload })no-op(DingTalk 端按钮的 actionId 编码已经在 buildPendingPayload 写进 cardParamMap,平台侧无需额外 binding)。返回 null 表示无 binding
unbindPending({ entry, binding, request })no-op
clearPendingActions({ entry, phase })updateCardVariables(entry.outTrackId, { buttonGroupVisible: false }, token),仅隐藏按钮;statusFooter 不动(由 updateEntry 负责)
cancelDelivered({ entry, request })updateCardVariables 把 statusFooter 改成"❌ 已取消",buttonGroupVisible=false
+ +

5.5 observe(可选,本设计仅实现 onDeliveryError 用于日志)

+ + + + + + + + + + + + +
方法DingTalk 实现
onDeliveryError({ error, plannedTarget, request })结构化日志 [DingTalk][Approval][DeliveryError] approvalId=<id> target=<to> error=<msg>
onDuplicateSkipped / onDelivered仅 debug 日志
+
+ + +
+

6. 数据流(交互逻辑详解)

+ +

6.1 Target 解析

+ +

resolveOriginTarget

+
输入 request.request:
+  ├─ turnSourceChannel: "dingtalk" | null
+  ├─ turnSourceAccountId: string | null
+  ├─ turnSourceTo: string | null         // e.g. "group:cidxxxxx" / "user:staff001"
+  └─ turnSourceThreadId: null            // DingTalk 无 thread
+
+if turnSourceChannel !== "dingtalk" → return null(CLI/WebUI 触发,无 origin 可投)
+if !turnSourceTo                    → return null
+return {
+  to: normalizeApprovalTargetTo(turnSourceTo),   // 保留 "user:" / "group:" 前缀
+  threadId: null,
+}
+
+// normalizeApprovalTargetTo 约定:
+//   - 输入若已带 "user:" / "group:" 前缀 → 原样返回
+//   - 输入若为裸 conversationId("cid..." 开头)→ 加 "group:" 前缀
+//   - 输入若为裸 staffId → 加 "user:" 前缀
+//   - 与 sendProactiveTextOrMarkdown 内部 stripTargetPrefix 的反向约定保持一致
+ +

resolveApproverDmTargets

+
approvers = listExecApprovers({ cfg, accountId })  // staffId[] after normalize
+return approvers.map(staffId => ({
+  to: `user:${staffId}`,                  // 永远带 user: 前缀
+  threadId: null,
+}))
+ +

6.2 投递 pending(origin + DM 双路径 + 去重)

+
场景:用户在钉钉群 cid_xxx 里 @ agent,agent 跑命令需要批准。
+       approvers = ["staffA", "staffB"]
+       触发用户也是 staffA(在群里)
+
+approval-runtime 投递计划:
+  ├─ origin   = { to: "group:cid_xxx" }
+  ├─ DM[0]    = { to: "user:staffA" }
+  └─ DM[1]    = { to: "user:staffB" }
+
+依次调 prepareTarget:
+  ├─ origin    → dedupeKey = "default:group:cid_xxx"
+  ├─ DM[0]     → dedupeKey = "default:user:staffA"
+  └─ DM[1]     → dedupeKey = "default:user:staffB"
+  → 三个 dedupeKey 全不同 → 三条都投
+
+依次调 deliverPending:
+  ├─ group:cid_xxx → createAndDeliver outTrackId="approval-abc123-group:cid_xxx"  ✓
+  ├─ user:staffA   → createAndDeliver outTrackId="approval-abc123-user:staffA"    ✓
+  └─ user:staffB   → createAndDeliver outTrackId="approval-abc123-user:staffB"    ✓
+
+approval-store 写 3 条 entry,所有 entry.approvalId="abc123"
+
+———————————————————————————————————————————————————————————————————
+
+变体场景:用户 staffA 直接跟 agent DM 触发 exec:
+       origin   = { to: "user:staffA" }
+       DM[0]    = { to: "user:staffA" }   ← 与 origin 相同
+       DM[1]    = { to: "user:staffB" }
+
+prepareTarget:
+  ├─ origin → dedupeKey = "default:user:staffA"
+  ├─ DM[0]  → dedupeKey = "default:user:staffA"  ⚠ 重复!
+  └─ DM[1]  → dedupeKey = "default:user:staffB"
+
+上游 SDK 见 dedupeKey 重复 → DM[0] 自动跳过(onDuplicateSkipped 日志)
+最终只投 2 条:staffA 一张(origin 角色)+ staffB 一张
+ +

6.3 点击 approve → 上游 resolve(核心交互链路)

+
用户在卡片上点"允许一次"
+
+t=0   DingTalk Stream 平台推送 TOPIC_CARD 回调
+        payload.cardPrivateData.actionIds = ["/approve abc123 allow-once"]
+        payload.userId                    = "staffA"
+        payload.spaceId / spaceType       = "cid_xxx" / "group"
+        payload.outTrackId                = "approval-abc123-group:cid_xxx"
+
+t=1   src/gateway/channel-gateway.ts:330 listener 触发
+        ├─ messageId = res.headers.messageId
+        ├─ payload   = JSON.parse(res.data)
+        ├─ analysis  = analyzeCardCallback(payload)
+        │    analysis.actionId = "/approve abc123 allow-once"
+        │    analysis.userId   = "staffA"
+        │
+        ├─ 【新增分支】tryHandleApprovalCallback(analysis, ...)
+        │    │
+        │    1. 前缀匹配
+        │       if (!analysis.actionId?.startsWith("/approve ")) return { handled: false }
+        │
+        │    2. 解析(复用上游!)
+        │       parsed = parseExecApprovalCommandText(analysis.actionId)
+        │       → { kind?: "exec"|"plugin", id: "abc123", decision: "allow-once" }
+        │
+        │    3. 查 store 找 entry
+        │       entry = approval-store.getByApprovalId("abc123")
+        │       if (!entry) {
+        │         updateCardVariables(payload.outTrackId,
+        │                             { statusFooter: "⏰ 已过期或已关闭",
+        │                               buttonGroupVisible: false }, token)
+        │         return { handled: true, reason: "not-found" }
+        │       }
+        │
+        │    4. 权限校验
+        │       authorized = capability.authorizeActorAction({
+        │         cfg, accountId, senderId: analysis.userId,
+        │         action: "approve",
+        │         approvalKind: entry.kind })
+        │       if (!authorized.authorized) {
+        │         sendProactiveTextOrMarkdown(
+        │           config,
+        │           `user:${analysis.userId}`,             // 带 user: 前缀,明确为 oto 投递
+        │           "⛔ 你不在 approver 名单,无权批准此请求",
+        │           { accountId, log })
+        │         return { handled: true, reason: "unauthorized" }
+        │       }
+        │
+        │    5. 调上游 gateway resolve 方法回写
+        │       // gateway 实例从 channel-gateway 闭包注入到 callback-handler 构造
+        │       // (registerCallbackListener 注册时已持有 gateway 引用,
+        │       //  与现有 recordExplicitFeedbackLearning 的 gateway 注入方式一致)
+        │       method = entry.kind === "exec"
+        │         ? "exec.approval.resolve"
+        │         : "plugin.approval.resolve"
+        │       await invokeGatewayMethod(method, {
+        │         id: parsed.id,
+        │         decision: parsed.decision,
+        │       })
+        │
+        │       (上游 resolve 触发 approval-handler-runtime 调
+        │        presentation.buildResolvedResult →
+        │        transport.updateEntry({ phase: "resolved" }) →
+        │        所有 3 张卡片都被更新为终态,包含本机这张)
+        │
+        │    6. 兜底立即 update 本机这张(防止上游事件回环延迟)
+        │       updateCardVariables(payload.outTrackId,
+        │         buildResolvedCardParamMap(...), token)
+        │
+        │    7. 日志 + return { handled: true, reason: "resolved" }
+        │
+        ├─ 分支命中 → 跳过既有 handleCardAction
+        └─ finally: socketCallBackResponse(messageId, { success: true }) (ack 平台)
+ +

6.4 上游 resolve 后的卡片状态同步

+

当任意一张卡片被点击 → 上游 store 标记为 resolved → approval-handler-runtime 会对**所有 entry**(origin + 所有 DM)调一次 transport.updateEntry({ phase: "resolved" })。这保证所有人看到的卡片都同步更新。

+ +
approval abc123 被 staffA 在群里点了"允许一次"
+
+       ┌─────────────────────────┐
+       │ upstream approval store │
+       │  abc123 → resolved      │
+       │  decision = allow-once  │
+       │  resolvedBy = staffA    │
+       └────────────┬────────────┘
+                    │ 触发
+                    ▼
+       presentation.buildResolvedResult()
+       returns { kind: "update", payload: {
+         statusFooter: "✅ 已批准 by @staffA (allow-once)",
+         buttonGroupVisible: false,
+       }}
+                    │
+                    │  approval-runtime 遍历所有 entry
+                    ▼
+       ┌─────────────────────────────────────────────┐
+       │ for entry of store.findByApprovalId(abc123) │
+       │   transport.updateEntry({ entry, payload })  │
+       │   → PUT /v1.0/card/instances                 │
+       └─────────────────────────────────────────────┘
+                    │
+       ┌────────────┼────────────┐
+       ▼            ▼            ▼
+   group 卡片    DM-A 卡片    DM-B 卡片
+   都被 PUT 成   都被 PUT 成   都被 PUT 成
+   "✅ 已批准"   "✅ 已批准"   "✅ 已批准"
+ +

6.5 用户点 deny / allow-always 的差异

+ + + + + + + +
按钮actionIdstatusFooter 终态
✅ 允许一次/approve <id> allow-once✅ 已批准 by @user · 允许一次
✅ 总是允许/approve <id> allow-always✅ 已批准 by @user · 总是允许
⛔ 拒绝/approve <id> deny⛔ 已拒绝 by @user
+ +

6.6 失败 / 边界场景

+ +

用户重复点击(按钮看起来还在但已 resolved)

+
    +
  1. 上游 exec.approval.resolve 返回 already-resolved 错误
  2. +
  3. callback-handler 捕获错误,调 updateCardVariables 把卡片刷成终态(即使 UI 还没刷过来也立即修正)
  4. +
  5. 对用户:第二次点击看起来等同于第一次"已生效",无打扰提示
  6. +
+ +

非 approver 点击

+
    +
  1. authorizeActorAction 返回 authorized: false
  2. +
  3. sendProactiveTextOrMarkdown(config, "user:" + clicker.staffId, "⛔ 你不在 approver 名单", ...) 给点击者私聊(target 必须带 user: 前缀走 oto 投递)
  4. +
  5. 卡片不变(按钮保留,给真正的 approver 用)
  6. +
  7. 日志 [DingTalk][Approval][Denied] approvalId=<id> clicker=<userId>
  8. +
+ +

Channel 重启后用户点旧卡片

+
    +
  1. approval-store 内存清空 → getByApprovalId 返 null
  2. +
  3. callback-handler 进 not-found 分支
  4. +
  5. 卡片刷成"⏰ 已过期或已关闭",按钮隐藏
  6. +
  7. 用户体感:按钮"点了一下变灰,没批成"——对长时间下线降级,可接受
  8. +
+ +

上游过期事件触达

+
upstream timer 触发 approval.expired event
+  ├─ presentation.buildExpiredResult() → { kind: "update", payload: {
+  │     statusFooter: "⏰ 已过期(未在 10 分钟内响应)",
+  │     buttonGroupVisible: false,
+  │   }}
+  └─ transport.updateEntry({ phase: "expired" }) for all entries
+       → 所有卡片同步更新为过期态
+       → approval-store.remove(approvalId)
+ +

Channel stopClient(账号停用 / gateway 重启)

+
gateway.stopClient(accountId)
+  ├─ finalizeActiveCardsForAccount(accountId)        ── 现有 AI Card 收尾
+  └─ finalizeActiveApprovalCardsForAccount(accountId)  ← 新增
+        for entry of approval-store.findByAccountId(accountId):
+          updateCardVariables(entry.outTrackId, {
+            statusFooter: "❌ 已取消(钉钉通道已断开)",
+            buttonGroupVisible: false,
+          }, token).catch(noop)
+          approval-store.remove(entry.approvalId)
+ +

6.7 createAndDeliver 失败 → markdown 兜底(error-recovery)

+
deliverPending(target)
+  ├─ card-service.createAndDeliver(...)
+  │
+  ├─ 成功 → store.register(entry) → return entry
+  │
+  └─ 失败(网络 / API 4xx/5xx / 模板未发布)
+       │
+       ├─ observe.onDeliveryError({ error, plannedTarget, request })
+       │
+       ├─ approval-fallback-render.buildMarkdownFallback(request)
+       │  →  内容包含:
+       │     ### ⚠️ 需要审批:<exec command / plugin tool 简介>
+       │     **ID**: `abc123`
+       │     **过期时间**: 10 分钟
+       │
+       │     批准(仅一次):`/approve abc123 allow-once`
+       │     批准(总是):`/approve abc123 allow-always`
+       │     拒绝:`/approve abc123 deny`
+       │
+       ├─ sendProactiveTextOrMarkdown(target, markdownText, ...)
+       │
+       ├─ 仍写一条降级 entry 到 store(无 outTrackId,type="fallback")
+       │     这样后续 expired/resolved 事件还能 try-best 触达——
+       │     虽然 markdown 消息不能 update,但至少不会留下孤儿状态
+       │
+       └─ return fallback-entry
+ +
+ 兜底路径的设计取舍 + markdown 消息一旦发出无法编辑(钉钉机器人 API 无 edit)。所以兜底卡片的"终态同步"做不到——expire / resolve 时不会发新消息覆盖原 markdown。这是已知降级;接受理由:(1) createAndDeliver 失败本身就是异常通路;(2) 不再追发消息免得用户被刷屏;(3) 用户用 /approve 命令照样能完成审批。 +
+
+ + +
+

7. 审批卡片设计

+ +

7.1 模板字段约定(approval-card-template-v1)

+ + + + + + + + + + + + + +
变量 key类型语义
kindBadgeString"⚙️ 命令执行" | "🔌 插件调用"
titleStringe.g. "Agent 请求批准执行命令"
bodyMarkdownString正文,markdown 渲染(含命令 preview / 工具描述)
detailRowsloopArray"cwd: /tmp"、"severity: high" 等 metadata,逐行渲染
severityBadgeString"🟢 low" | "🟡 medium" | "🔴 high"
approvalIdString显示用,与按钮 actionId 内 ID 一致
expiryHintString"⏰ <X> 分钟后过期"
buttonGroupVisibleBoolean控制三按钮可见性(终态 = false)
statusFooterString终态条:"✅ 已批准…" / "⛔ 已拒绝…" / "⏰ 已过期" / "❌ 已取消"
+ +

7.2 三按钮配置(actionId 编码与 D2/D3 对齐)

+
ButtonGroup (在 buttonGroupVisible=true 时渲染):
+  ├─ ✅ 允许一次   actionId = "/approve ${approvalId} allow-once"   style=primary
+  ├─ ✅ 总是允许   actionId = "/approve ${approvalId} allow-always" style=ghost
+  └─ ⛔ 拒绝       actionId = "/approve ${approvalId} deny"         style=danger
+ +

7.3 状态机

+
           ┌─ user click allow-once  ──► RESOLVED (allow-once)
+           ├─ user click allow-always ──► RESOLVED (allow-always)
+           ├─ user click deny         ──► RESOLVED (deny)
+PENDING ───┤
+           ├─ upstream expired event  ──► EXPIRED
+           ├─ channel stopClient      ──► CANCELED
+           └─ upstream canceled event ──► CANCELED
+
+(终态:buttonGroupVisible=false; statusFooter 文案对应)
+ +

7.4 Mockup(pending · exec approval · 群聊)

+
+
#OpenClaw 工作群
+
+
+
U
+
小赵
@OpenClaw 帮我把 720 小时之前的 docker image 清理掉
+
+
+
O
+
+
OpenClaw
+
+
+ ⚙️ 命令执行 · 等待审批 + claude-opus-4.7 +
+
+ Agent 请求批准执行命令 +
docker image prune -a -f --filter "until=720h"
+
    +
  • cwd: /Users/zhumin/projects/openclaw
  • +
  • security: 🟡 medium(删除大量镜像不可逆)
  • +
  • approval id: exec-abc123
  • +
+
+
+ + + +
+ +
+
+
+
+
+ +

7.5 Mockup(resolved · 已批准)

+
+
+
+
O
+
+
OpenClaw
+
+
+ ⚙️ 命令执行 · ✅ 已批准 + claude-opus-4.7 +
+
+ Agent 请求批准执行命令 +
docker image prune -a -f --filter "until=720h"
+
    +
  • approval id: exec-abc123
  • +
+
+ +
+
+
+
+
+ +

7.6 Mockup(resolved · 已拒绝)

+
+
+
+
O
+
+
OpenClaw
+
+
+ ⚙️ 命令执行 · ⛔ 已拒绝 +
+
+ Agent 请求批准执行命令 +
docker image prune -a -f --filter "until=720h"
+
+ +
+
+
+
+
+ +

7.7 Mockup(expired · 已过期)

+
+
+
+
O
+
+
OpenClaw
+
+
+ ⚙️ 命令执行 · ⏰ 已过期 +
+
+ Agent 请求批准执行命令 +
docker image prune -a -f --filter "until=720h"
+
+ +
+
+
+
+
+ +

7.8 Mockup(plugin approval · pending · DM 私聊)

+
+
OpenClaw · 私聊
+
+
+
O
+
+
OpenClaw
+
+
+ 🔌 插件调用 · 等待审批 + codex agent +
+
+ Agent 请求授权使用工具:postgres-mcp +

将对你的生产 PG 库执行:

+
tool: query_database
+description: 对 production.orders 表查询近 7 天订单
+
    +
  • severity: 🔴 high(涉及生产数据)
  • +
  • approval id: plugin-xyz789
  • +
+
+
+ + + +
+ +
+
+
+
+
+
+ + +
+

8. 错误处理矩阵

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
失败点触发条件处理用户体验
createAndDeliver 网络/HTTP 失败钉钉 API 5xx、超时、429onDeliveryError 日志 + 走 §6.7 markdown 兜底收到 markdown 消息,含 /approve 命令模板
approval-card 模板未发布templateId 在 DingTalk 侧不存在同上(错误归一化为 createAndDeliver 失败)+ ERROR 级日志降级到 markdown;运维需看日志修复
updateCardVariables 失败PUT /v1.0/card/instances 5xxWARN 日志,重试(避免 race);卡片视觉短暂不一致,下次相关事件会再覆盖偶发不一致;上游 store 仍是真相
parseExecApprovalCommandText 失败actionId 被人手改坏 / 编码 bugWARN 日志 + ack 平台 + 卡片不变(保留按钮)用户感觉点击没反应;可通过 /approve 命令兜底
权限校验失败clicker.staffId 不在 approver 名单调 sendProactiveTextOrMarkdown 给点击者私聊"⛔ 无权批准";卡片不变;日志点击者私聊收到拒绝提示,原卡片对其他人仍可用
上游 resolve 返回 already-resolved用户重复点击 / 多端同时点callback-handler 静默成功,强制 update 本机卡片到终态第二次点击看起来等同生效,无打扰
上游 resolve 返回 not-foundapprovalId 在上游已过期/清除(如 channel 长时间下线)callback-handler 更新卡片为"⏰ 已过期或已关闭"点击后卡片直接刷成过期态
approval-store entry 缺失(点击时)channel 重启后内存清空同上"not-found"路径同上
DM 投递权限不足机器人对某 approver 无法主动 DM(企业权限问题)onDeliveryError 日志 + 该 target 走 markdown 兜底(可能也失败);其它 target 不受影响未配置 DM 权限的 approver 收不到通知;运维需排查
同一 approver 既在 origin 群里又收 DM正常场景SDK dedupeKey 自动去重(§6.2 变体);同一 target 只投一次不会重复打扰
+
+ + +
+

9. 测试策略

+ +

9.1 测试文件布局

+ + + + + + + + + + + + + + + + +
文件覆盖目标预计 case 数
tests/unit/approval-config.test.tsschema 解析、normalize、enabled=auto、fallback chain~12
tests/unit/approval-card-template.test.tscardParamMap 构造(含 stringify 规则)~8
tests/unit/approval-card-render.test.tspending/resolved/expired/canceled × exec/plugin = 8 个矩阵~16
tests/unit/approval-target-resolver.test.tsorigin 解析(含 turnSourceChannel=null)、DM 列表构造~10
tests/unit/approval-callback-handler.test.tsactionId parse、权限校验、resolve 调用、各错误分支~18
tests/unit/approval-store.test.tsregister/get/remove/findByAccountId/finalizeAll~8
tests/unit/approval-cancel.test.tsstopClient 触发 finalize~4
tests/unit/approval-fallback-render.test.tsmarkdown 兜底文案、exec/plugin 分支~6
tests/unit/approval-capability.test.tsSDK 工厂参数装配正确、capability 单例~6
tests/unit/approval-native-runtime.test.ts5 子 adapter 集成 mock~14
tests/integration/approval-end-to-end.test.ts模拟 createApprovalRequest → 投递 → 点击 → resolve 回写 → 卡片刷新;含 dedupeKey、both-surface、self-approval~10
tests/integration/approval-channel-stop.test.tsstopClient 触发的 finalize 链路~3
+

合计 ~115 case;按现有测试目录 1 case ≈ 18-25 行估算,测试代码约 2000-2800 行。

+ +

9.2 Mock 策略

+
    +
  • 所有 DingTalk HTTP(createAndDeliver / updateCardVariables / sendProactiveTextOrMarkdown):vi.mock("../../src/http-client")vi.mock("../../src/auth"),不打真实 API
  • +
  • 上游 SDK 工厂:vi.mock("openclaw/plugin-sdk") 注入 spy,验证传入参数完整
  • +
  • 上游 gateway resolve:vi.mock 抽象的 invoke,断言 method + payload
  • +
  • approval-store:测试用 fresh instance,避免跨 case 污染(现有 vitest clearMocks/restoreMocks/mockReset 全局开启)
  • +
+ +

9.3 关键 integration 场景

+
    +
  1. 群 + 双 approver DM:发起 → 3 卡片投出 → 群里点 approve → 3 卡片同步刷新
  2. +
  3. self-approval 在 DM:approver 自己 DM 发起 exec → 仅投 1 卡(dedupe)→ 自己点 → 通过
  4. +
  5. 非 approver 点击:投 1 卡 → 非 approver 用户点 → 收私聊拒绝 → 卡片不变
  6. +
  7. 过期:投卡 → mock 上游 expired event → 所有卡片刷成过期
  8. +
  9. stopClient:投 2 卡 → 调 stopClient → 2 卡都刷成"已取消"
  10. +
  11. createAndDeliver 失败 fallback:mock HTTP 错 → 走 markdown 兜底 → 验证 sendProactiveTextOrMarkdown 调用与文案
  12. +
  13. 重复点击:第一次成功 → mock 上游返 already-resolved → 卡片仍刷成终态,不再调 resolve
  14. +
  15. 未配置 approver:execApprovals 缺省 → isConfigured=false → shouldHandle=false → 上游不会调 DingTalk 路径
  16. +
+ +

9.4 覆盖率目标

+
    +
  • src/approval/*:line ≥ 90%,branch ≥ 85%
  • +
  • 整体仓库 coverage 不下降(pnpm test:coverage)
  • +
+
+ + +
+

10. 实施阶段

+

按"先上链路、再上 UX、最后部署模板"分 3 阶段。每阶段独立可合并,前一阶段不阻塞后一阶段对应的 PR review。

+ +

阶段 1 · 接口骨架与权限链路(PR-1)

+
    +
  • 新增 src/approval/ 目录与全部 9 个文件骨架
  • +
  • 实现 approval-config.tsapproval-store.tsapproval-target-resolver.tsapproval-capability.ts(不含 native runtime 完整实现)
  • +
  • src/channel.ts 挂上 approvalCapability,但 nativeRuntime 暂留 undefined,capability 仅生效 authorizeActorAction / resolveApproveCommandBehavior 等权限部分
  • +
  • schema、配置文档(草稿)
  • +
  • 测试:approval-configapproval-storeapproval-target-resolverapproval-capability
  • +
  • 这阶段交付后:用户已经能在钉钉里手敲 /approve <id> <decision> 完成审批(权限校验生效)。Feishu 同档能力。
  • +
+ +

阶段 2 · 完整 native runtime(PR-2)

+
    +
  • 实现 approval-card-template.ts(先用占位 templateId,注释 TODO)、approval-card-render.tsapproval-fallback-render.tsapproval-callback-handler.tsapproval-native-runtime.tsapproval-cancel.ts
  • +
  • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支
  • +
  • 在阶段 1 的 capability 里挂上 nativeRuntime
  • +
  • 测试:剩余 6 个 unit 文件 + 2 个 integration 文件
  • +
  • 这阶段交付后:完整三按钮卡片 UX 可用,但 templateId 仍是占位——本地 mock 测试已能验证全链路;真实部署需阶段 3
  • +
+ +

阶段 3 · 模板上传与发布(PR-3)

+
    +
  • 登录 open-dev.dingtalk.com/fe/card,把 docs/assets/card-template-approval-v1.json 导入卡片搭建器
  • +
  • 调试模板布局(按 §7 mockup 对齐)
  • +
  • 发布为统一预置模板,获得正式 templateId
  • +
  • 替换 approval-card-template.ts 里的占位常量为真实 templateId
  • +
  • 真机回归(参照 skills/dingtalk-real-device-testing/SKILL.md
  • +
  • 更新用户文档 docs/user/features/exec-approval.md
  • +
  • 这阶段交付后:用户安装即用,零部署摩擦
  • +
+ +
+ 分阶段的好处 +
    +
  • PR-1 可独立 merge,立刻让"在钉钉里用 /approve 命令"成为可能(Feishu-同档 baseline)
  • +
  • PR-2 引入大量代码但完全自给自足(不依赖模板上传),测试可用 mock templateId 全覆盖
  • +
  • PR-3 只动配置常量与文档,code-review 量极小,便于聚焦真机回归
  • +
  • 任何阶段回滚都不破坏前阶段已交付能力
  • +
+
+
+ + +
+

11. 非目标与已知风险

+ +

11.1 明确不在本 spec 范围

+
    +
  • 通用 action dispatcher(gap 文档 #01 sub-1, sub-2):peer 三家都没做,approval 按前缀走自己的分支已经够清晰
  • +
  • 抽象 interactive payload → AI Card 自动渲染(gap 文档 #01 sub-4):approval 是固定 3 按钮,不需要 runtime 任意组件渲染;任意组件需登录卡片平台核实清单后单独立项
  • +
  • 统一交互状态模型(gap 文档 #01 sub-6):本 spec 已为 approval 实现 pending/resolved/expired/canceled 状态机;将之普适到其它交互需要更多用例验证,留待 #12 message tool action surface
  • +
  • per-account 配置 surface 降级(D4 D 选项):当前 schema 已支持 execApprovals.target 字段,但默认值固定为 both,不主推切换
  • +
  • 主动 rebind on restart(D12 B 选项):留待 v2
  • +
+ +

11.2 已知风险

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
风险影响缓解
approval-card 模板需在 DingTalk 侧发布为跨租户预置若发布失败 → 所有用户都走 markdown 兜底阶段 3 在发布后做真机回归;fallback 路径保证不出现 approval 静默消失
callback userId 字段语义假设(认为是 staffId)若实际是 unionId → 权限校验全部失败真机回归阶段必查;若不符则在 callback-handler 加一次 unionId→staffId 解析(用现有 getUnionIdByStaffId 反向 API),命中后缓存
机器人 DM approver 失败(企业权限)该 approver 收不到 DM;origin 仍可见onDeliveryError 日志;用户文档明示需配置工作通知权限
updateCardVariables 在频繁 approve/expire 时遇上 race卡片视觉短暂不一致"upstream store 是真相"原则;不重试 update;下次相关事件会覆盖
多账号场景下 approval-store 单例如何按账号隔离潜在跨账号污染store 内 entry 强制带 accountId;所有查询 API 必须传 accountId;测试覆盖跨账号场景
+
+ + +
+

12. 参考来源

+ +

上游 OpenClaw 核心类型与运行时

+
    +
  • openclaw/src/channels/plugins/approval-native.types.ts — Native surface/target/adapter 接口
  • +
  • openclaw/src/channels/plugins/types.adapters.ts:576-669 — ChannelApprovalCapability/Adapter/Delivery/Render 完整签名
  • +
  • openclaw/src/channels/plugins/types.plugin.ts:84-86 — plugin 字段 approvalCapability?: ChannelApprovalCapability
  • +
  • openclaw/src/infra/approval-handler-runtime-types.ts:216-235 — ChannelApprovalNativeRuntimeAdapter 完整签名
  • +
  • openclaw/src/infra/approval-handler-adapter-runtime.ts:10-136 — createLazyChannelApprovalNativeRuntimeAdapter
  • +
  • openclaw/src/infra/approval-gateway-resolver.ts:29-46 — resolve gateway method 入口
  • +
  • openclaw/src/infra/exec-approvals.ts — ExecApprovalRequest / Resolved
  • +
  • openclaw/src/infra/plugin-approvals.ts — PluginApprovalRequest / Resolved
  • +
  • openclaw/src/plugin-sdk/approval-delivery-helpers.ts:30-261 — createApproverRestrictedNativeApprovalCapability 工厂
  • +
  • openclaw/src/auto-reply/reply/commands-approve.ts/approve 命令解析与 dispatcher
  • +
+ +

对标 channel 实现

+
    +
  • openclaw/extensions/discord/src/approval-native.ts(219 行)+ approval-handler.runtime.ts(636 行)
  • +
  • openclaw/extensions/telegram/src/approval-native.ts(168 行)+ approval-handler.runtime.ts(195 行)+ approval-callback-data.ts(64 字节限制处理)
  • +
  • openclaw/extensions/slack/src/approval-native.ts(211 行)+ approval-handler.runtime.ts(352 行)
  • +
+ +

本仓库相关代码

+
    +
  • src/channel.ts:22-127 — plugin 定义入口(待新增 approvalCapability
  • +
  • src/gateway/channel-gateway.ts:330-407 — TOPIC_CARD listener(待插入 approval 分支)
  • +
  • src/card-callback-service.ts:68-92 — extractCardActionId(已支持任意字符串)
  • +
  • src/card-callback-service.ts:175-201 — updateCardVariables 即 PUT /v1.0/card/instances
  • +
  • src/card-service.ts:649 — sendProactiveCardText(DM 投卡链路)
  • +
  • src/card-service.ts:711-785 — finalizeActiveCardsForAccount(停机收尾模式参考)
  • +
  • src/card-service.ts:795-855 — createAndDeliver
  • +
  • src/send-service.ts:352-482 — sendProactiveTextOrMarkdown(markdown 兜底链路)
  • +
  • src/card/card-template.ts — 现有 AI Card 模板常量(新模板按此模式并列)
  • +
  • src/card/card-action-handler.ts:18-62 — btn_stop owner 校验模式参考
  • +
  • src/channel.ts:86 — normalize approver 复用此函数 raw.replace(/^(dingtalk|dd|ding):/i, "")
  • +
  • src/messaging/quoted-file-service.ts:93 — getUnionIdByStaffId(应急 ID 解析备用)
  • +
+ +

钉钉平台 API

+ + +

项目内规范

+
    +
  • CLAUDE.md — 代码约定、日志前缀规范、测试目标
  • +
  • .github/instructions/code-review.instructions.md — review 评论使用简体中文
  • +
  • skills/dingtalk-real-device-testing/SKILL.md — 真机回归 checklist
  • +
+
+
+ + From ad2d5b0e06aea9124c162e0cd98c364f4002a52e Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Mon, 18 May 2026 22:25:09 +0800 Subject: [PATCH 02/44] =?UTF-8?q?docs(spec):=20v2=20=E4=BF=AE=E8=AE=A2=20g?= =?UTF-8?q?ap#01=20=E2=80=94=20=E5=AF=B9=E6=AF=94=20PR#489=20+=20=E7=9C=9F?= =?UTF-8?q?=E6=9C=BA=E5=9B=9E=E8=B0=83=E8=AF=81=E6=8D=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 主要改动(PR#489 对比 + 真机回调 payload 实证驱动): 1. 按钮 payload 编码(D15 新增):三按钮共享 actionId "approval",decision/id 走 cardPrivateData.params 结构化字段。原因——真机回调 (2026-03-21) 证明 DingTalk 会在 actionId 末尾自动追加 button index(btn_approve → btn_approve0), v1 设想的"/approve " 字符串字面量方案不可行。 2. /approve 命令必须早期 intercept(D2 重写 + 新增 §6.8):PR#489 揭示 Plugin Approval 的 waitDecision 持有 session lock,若 /approve 走正常 inbound dispatch 会 acquireSessionLock 死锁。channel 端必须在入口 regex match 后直接调 resolveApprovalOverGateway,不进 reply 派发。 3. card-callback-service 必须扩展(D16 新增):CardCallbackAnalysis 加 cardPrivateData 字段,analyzeCardCallback 抽出 params。v1 误判此文件不修改。 4. 前置依赖(D17 新增):peerDependency bump 到 openclaw>=2026.4.7;PR#480 (CardBtn[] + sendCardRequest) 必须先合。阶段 0 章节明示。 5. 用户确认 callback userId === staffId(不是 unionId),§4.2 approver schema 不变;§11.2 风险表删除该假设的风险条目。 保留的设计(优于 PR#489 之处): - approver schema + authorizeActorAction(PR#489 完全无权限检查) - finalizeActiveApprovalCardsForAccount(PR#489 停机留鬼按钮) - markdown 兜底 error-recovery - both surface(PR#489 仅 origin,CLI 触发场景无法投递) - ~115 test case 覆盖目标 修订面:D2 重写,新增 D15/D16/D17;新增 §1.2 按钮编码与解码、§6.8 命令早期 intercept、§10 阶段 0 前置依赖;§3.3 / §5.2 / §6.3 / §7.2 / §8 / §9 / §10 阶段 2-3 / §11.2 / §12 全部对齐 v2 状态。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 365 +++++++++++++++--- 1 file changed, 308 insertions(+), 57 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index 1d2d1906..7f8273a4 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -12,7 +12,7 @@

Gap #01 · DingTalk Native Approval 设计方案

- 为 DingTalk Channel 接入 OpenClaw 的 原生审批能力(exec approval + plugin approval),全量实现 ChannelApprovalCapability 与 5 个子 native runtime adapter。审批以 AI 卡片三按钮形式投放,按钮 payload 直接复用上游 /approve 命令字面量,与 Discord / Telegram / Slack 三个 peer channel 对齐。 + 为 DingTalk Channel 接入 OpenClaw 的 原生审批能力(exec approval + plugin approval),全量实现 ChannelApprovalCapability 与 5 个子 native runtime adapter。审批以 AI 卡片三按钮形式投放,按钮 payload 用 DingTalk sendCardRequest 的 cardPrivateData.params 结构化字段编码 decision/approvalId(v2 修订;v1 曾设想用 /approve 命令字面量,被真机回调证伪)。按钮点击与 /approve 文本命令两条路径都收敛到上游公开 API resolveApprovalOverGateway

P0 · 核心缺口 @@ -49,6 +49,68 @@

1.1 范围明确不做的事

  • 不做重启后主动 rebind pending approval(v1 范围;用户点过期/失效按钮时显式降级提示)
  • 不引入 select / input / datepicker 等高级组件——仅 button(已 CONFIRMED 平台支持)
  • + +

    1.2 按钮 payload 编码与解码(D15 落地)

    +
    + 真机回调实证(2026-03-21):DingTalk 会自动在 actionId 末尾追加 button 索引
    + 定义 btns = [{actionId:"btn_approve"}, {actionId:"btn_approve_once"}, {actionId:"btn_deny"}] → + 回调拿到的 cardPrivateData.actionIds[0] 依次是 "btn_approve0" / "btn_approve_once1" / "btn_deny2"。 + 所以 actionId 不能承载结构化语义(解析正则会乱);结构化信息必须走 params。 +
    + +

    编码(构造按钮时)

    +

    三按钮共享同一个 actionId: "approval",靠 params 区分:

    +
    // approval-card-service.ts 内部
    +btn.event = {
    +  type: "sendCardRequest",
    +  params: {
    +    actionId: "approval",                              // 三按钮全用这个
    +    params: { t: "approval", d: "<decision>", id: "<approvalId>" },
    +  },
    +}
    +// decision ∈ { "allow-once" | "allow-always" | "deny" }
    + +

    回调实测形态

    +
    // 用户点"允许"按钮(btn[0]),平台 push 的 callback data.content:
    +{
    +  "cardPrivateData": {
    +    "actionIds": ["approval0"],                        // ← 平台追加 index "0"
    +    "params": { "t": "approval", "d": "allow-once", "id": "abc123" }
    +  }
    +}
    +// 注意:content 与 value 字段内容一致(钉钉冗余)
    +// userId 字段:staffId(已用户确认),不是 unionId
    + +

    解码(callback 入口)

    +
    // 第一步:扩展后的 analyzeCardCallback 把 cardPrivateData 整体放进 analysis
    +const cpd = analysis.cardPrivateData;                  // { actionIds, params }
    +
    +// 第二步:parseApprovalFromCardPrivateData(cpd)
    +if (!cpd?.actionIds?.[0]?.startsWith("approval")) return null;  // 前缀匹配兼容 index 后缀
    +if (cpd.params?.t !== "approval") return null;
    +if (!["allow-once","allow-always","deny"].includes(cpd.params?.d)) return null;
    +if (typeof cpd.params?.id !== "string") return null;
    +return { approvalId: cpd.params.id, decision: cpd.params.d };
    + +

    回写上游

    +
    // 用 SDK 公开 API(v2026.4.7+):
    +import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime";
    +
    +await resolveApprovalOverGateway({
    +  cfg,
    +  approvalId,
    +  decision,
    +  senderId: analysis.userId,           // staffId
    +  clientDisplayName: "DingTalk",
    +});
    +// SDK 内部按 approval kind 自动 dispatch 到
    +// exec.approval.resolve / plugin.approval.resolve
    + +
    + 命令路径与按钮路径的关系
    + v1 spec 宣称"按钮和 /approve 命令走完全同一条解析链"——v2 不再成立(按钮走 cardPrivateData,命令走文本 regex)。 + 但两条路径都最终调同一个 resolveApprovalOverGateway,**等价收敛到上游 store**。用户感知一致,channel 内部解码逻辑分两条。 +
    @@ -68,8 +130,8 @@

    2. 已确认的决策清单

    D2 Slash 命令 - 纯复用上游 /approve <id> <decision>,channel 端零注册 - 三家完全一致 + 命令字面量与上游一致(/approve <id> <decision>),但 channel 端必须在 inbound-handler 入口早期 intercept,直接调 resolveApprovalOverGateway——不走正常 inbound dispatch 路径
    原因(v1 实施踩坑确认):Plugin Approval 的 waitDecision 阻塞在 dispatchReply 内并持有 DingTalk 自己的 session lock,若 /approve 走 normal pipeline 会 session lock 死锁,120s 超时失败。
    decision 别名按上游 commands-approve.ts 完整支持(once/always/allow/deny/reject/block) + 对齐 PR #489 揭示的运行时约束 D3 @@ -143,8 +205,35 @@

    2. 已确认的决策清单

    kind: "update"——PUT /v1.0/card/instances 改 statusFooter + 隐藏按钮组,卡片保留 Slack 同款 + + D15 + 按钮 payload 编码 + 所有三按钮共享 actionId "approval",decision/id 走 params 结构化字段:
    event.params = { actionId: "approval", params: { t: "approval", d: "allow-once"|"allow-always"|"deny", id: <approvalId> } }
    原因(真机回调证据):DingTalk 会在 actionId 末尾自动追加 button index("approval" → "approval0"/"approval1"/"approval2"),用前缀匹配 + 结构化 params 比字符串字面量解析稳得多。 + 对齐 PR #489 + 真机 payload + + + D16 + card-callback-service 扩展 + 必须修改 src/card-callback-service.ts:在 CardCallbackAnalysis 增加 cardPrivateData?: { actionIds?: string[]; params?: Record<string, unknown> } 字段,在 analyzeCardCallback 中从 content/value 嵌套 JSON 抽出 cardPrivateData.params 保留到 analysis 结果 + 对齐 PR #489(之前 spec 误判此文件不修改) + + + D17 + 前置依赖 + peerDependency 必须 bump 到 openclaw >= 2026.4.7(才有 ChannelApprovalNativeRuntimeAdapter 契约与 resolveApprovalOverGateway 公开 API);真机验证需要本仓库 PR #480(AI Card v2 / CardBtn[] + sendCardRequest 回调格式) 先合并 + 对齐 PR #489 实施约束 + + +
    + 本设计的版本演进 +
      +
    • v1(2026-05-18 初版):基于上游 SDK + peer 三家对标做的纸面设计,D2 写"纯复用 /approve dispatcher",按钮 payload 用 Telegram 风格字符串字面量
    • +
    • v2(2026-05-18 修订):对比 PR #489(@BrilliantWang 并行实施)+ 真机回调 payload 实证,确认 4 处必须改的事实约束 → 新增 D15/D16/D17,D2 重写。下文 §1.2 / §3.3 / §5.2 / §6.3 / §6.8 / §7.2 / §8 / §10 / §11 已全部对齐 v2 状态
    • +
    • userId 字段身份核实:用户在 v2 修订过程中确认 callback userId === staffId(不是 unionId),所以 §4.2 approver schema(staffId 名单)保持不变
    • +
    +
    @@ -163,8 +252,9 @@

    3.1 上下游分工

    │ ├─ exec.approval.resolve { id, decision } │ │ └─ plugin.approval.resolve { id, decision } │ │ │ -│ parseExecApprovalCommandText(text) ◄──── 按钮 payload 字面复用 │ -│ (用户敲 /approve 也走这里) │ +│ resolveApprovalOverGateway({approvalId,decision,senderId,...}) │ +│ 公开 API(v2026.4.7+):按钮点击 + /approve 命令两条路径都调它 │ +│ 内部按 approval kind dispatch 到 exec/plugin.approval.resolve │ │ │ └───────────────────────────────────┬────────────────────────────────────────┘ │ adapter 实现 @@ -291,9 +381,24 @@

    3.3 与现有代码的接触面

    src/types.ts - 新增 ApprovalCardEntry / ApprovalDecision / ApprovalPhase 类型 + 新增 ApprovalCardEntry / ApprovalDecision / ApprovalPhase / CardBtn 类型(CardBtn 来自 PR #480) 极低 + + src/card-callback-service.ts
    (D16:v2 新增改动面) + CardCallbackAnalysis 接口加 cardPrivateData?: { actionIds?: string[]; params?: Record<string, unknown> } 字段;analyzeCardCallback 中遍历 embeddedContent/embeddedValue/record 抽出 cardPrivateData 整体(actionIds 数组 + params 对象)并附到返回 analysis 上 + 低(仅追加字段,既有调用方不受影响;现有 extractCardActionId 抽 actionIds[0] 的逻辑不变) + + + src/inbound-handler.ts
    (D2:v2 新增改动面) + 在 handleDingTalkMessage 入口(command dispatch 之后、reply 派发之前)插入 /approve 命令早期 intercept 分支,详见 §6.8 + 中(需谨慎确保 intercept 之前的 dedup/self-filter/content-extract 行为不变) + + + package.json · peerDependencies.openclaw
    (D17:v2 新增) + 从当前 >=<current-version> bump 到 >=2026.4.7 + BREAKING(用户需升级上游 openclaw) + @@ -301,7 +406,6 @@

    3.3 与现有代码的接触面

    明确不修改的文件(向后兼容保证)
    • src/card/card-action-handler.ts(btn_stop 不动)
    • -
    • src/card-callback-service.tsextractCardActionId 已能取任意字符串)
    • src/feedback-learning-service.ts(与 approval 完全不交叉)
    • 所有 src/reply-strategy-*(approval 不进 agent reply 路径,独立卡片)
    @@ -401,7 +505,18 @@

    5.2 presentation

    buildPendingPayload({ request, nowMs, view }) - 调 approval-card-render.buildPendingCardParamMap(request, view),返回 { kind: "card", templateId, cardParamMap, actionIds }
    actionIds 是 3 个:"/approve <id> allow-once"、"/approve <id> allow-always"、"/approve <id> deny" + 调 approval-card-render.buildPendingCardParamMap(request, view),返回 { kind: "card", templateId, cardParamMap }
    cardParamMap 含 btns 字段(JSON-stringified CardBtn[]),三按钮 共享 actionId "approval",靠 params.d 区分(D15): +
    btns = [
    +  { text:"✅ 允许一次", color:"green",
    +    event:{type:"sendCardRequest", params:{
    +      actionId:"approval", params:{t:"approval", d:"allow-once", id:request.id}}}},
    +  { text:"✅ 总是允许", color:"blue",
    +    event:{type:"sendCardRequest", params:{
    +      actionId:"approval", params:{t:"approval", d:"allow-always", id:request.id}}}},
    +  { text:"⛔ 拒绝", color:"red",
    +    event:{type:"sendCardRequest", params:{
    +      actionId:"approval", params:{t:"approval", d:"deny", id:request.id}}}},
    +]
    buildResolvedResult({ request, resolved, view, entry }) @@ -560,17 +675,25 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    ├─ messageId = res.headers.messageId ├─ payload = JSON.parse(res.data) ├─ analysis = analyzeCardCallback(payload) - │ analysis.actionId = "/approve abc123 allow-once" - │ analysis.userId = "staffA" + │ analysis.actionId = "approval0" ← 平台追加 button index + │ analysis.userId = "staffA" ← staffId(已用户确认) + │ analysis.cardPrivateData = { ← D16 新增字段 + │ actionIds: ["approval0"], + │ params: { t:"approval", d:"allow-once", id:"abc123" } + │ } │ ├─ 【新增分支】tryHandleApprovalCallback(analysis, ...) │ │ - │ 1. 前缀匹配 - │ if (!analysis.actionId?.startsWith("/approve ")) return { handled: false } + │ 1. 前缀 + 结构化字段双校验 + │ parsed = parseApprovalFromCardPrivateData(analysis.cardPrivateData) + │ // 内部:actionIds[0].startsWith("approval") + params.t==="approval" + │ // + params.d ∈ {allow-once|allow-always|deny} + │ // + typeof params.id === "string" + │ if (!parsed) return { handled: false } + │ parsed = { approvalId: "abc123", decision: "allow-once" } │ - │ 2. 解析(复用上游!) - │ parsed = parseExecApprovalCommandText(analysis.actionId) - │ → { kind?: "exec"|"plugin", id: "abc123", decision: "allow-once" } + │ 2. (v2 不再走 parseExecApprovalCommandText——按钮路径走 cardPrivateData 解析; + │ 命令路径走 §6.8 早期 intercept 的 regex;两条路径最终都调 resolveApprovalOverGateway) │ │ 3. 查 store 找 entry │ entry = approval-store.getByApprovalId("abc123") @@ -595,17 +718,19 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ return { handled: true, reason: "unauthorized" } │ } │ - │ 5. 调上游 gateway resolve 方法回写 - │ // gateway 实例从 channel-gateway 闭包注入到 callback-handler 构造 - │ // (registerCallbackListener 注册时已持有 gateway 引用, - │ // 与现有 recordExplicitFeedbackLearning 的 gateway 注入方式一致) - │ method = entry.kind === "exec" - │ ? "exec.approval.resolve" - │ : "plugin.approval.resolve" - │ await invokeGatewayMethod(method, { - │ id: parsed.id, - │ decision: parsed.decision, + │ 5. 调上游 SDK 公开 API 回写(v2026.4.7+) + │ import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime" + │ + │ await resolveApprovalOverGateway({ + │ cfg, + │ approvalId: parsed.approvalId, + │ decision: parsed.decision, // "allow-once" | "allow-always" | "deny" + │ senderId: analysis.userId, // staffId + │ clientDisplayName: "DingTalk", │ }) + │ // SDK 内部根据 approval kind 自动 dispatch 到 + │ // exec.approval.resolve / plugin.approval.resolve + │ // channel 端不需要关心 kind 区分 │ │ (上游 resolve 触发 approval-handler-runtime 调 │ presentation.buildResolvedResult → @@ -742,6 +867,87 @@

    6.7 createAndDeliver 失败 → markdown 兜底(error-recovery)

    兜底路径的设计取舍 markdown 消息一旦发出无法编辑(钉钉机器人 API 无 edit)。所以兜底卡片的"终态同步"做不到——expire / resolve 时不会发新消息覆盖原 markdown。这是已知降级;接受理由:(1) createAndDeliver 失败本身就是异常通路;(2) 不再追发消息免得用户被刷屏;(3) 用户用 /approve 命令照样能完成审批。
    + +

    6.8 /approve 命令必须早期 intercept(D2 落地)

    + +
    + Session lock 死锁 — PR #489 揭示的真实运行时约束
    + Plugin Approval 的 waitDecision 阻塞在 dispatchReply 内部,持有 DingTalk 自己的 session lock。 + 如果 /approve 命令走正常 inbound dispatch 路径(→ command dispatcher → reply 派发), + 要再去 acquireSessionLock 才能继续——而锁已经被 waitDecision 持有, + 于是发生 session lock 死锁,120s session 超时后整个 approval 链路失败。 +
    + +

    解决方案:在 handleDingTalkMessage 入口最早处直接拦截,不走正常 dispatch

    +
    // src/inbound-handler.ts 内 handleDingTalkMessage 函数:
    +//   既有顺序:dedup → self-filter → content extract → authorization
    +//             → session routing → command dispatch → reply 派发
    +//
    +//   v2 修订:在 "command dispatch" 之后、"reply 派发"之前,加入 /approve early intercept。
    +//            (放在 command dispatch 之后是为了让 dedup / self-filter / content extract
    +//            等基础设施先 run;放在 reply 派发之前是关键——避免 session lock 死锁。)
    +
    +// ---- Early /approve bypass: resolveApprovalOverGateway ----
    +const textForApproveCheck = !isDirect
    +  ? extractedContent.text.replace(/^(?:@\S+\s+)*/u, "").trim()  // 群里剥前导 @mention
    +  : extractedContent.text.trim();
    +
    +if (/^\/approve\b/i.test(textForApproveCheck)) {
    +  const m = textForApproveCheck.match(
    +    /^\/approve\s+(\S+)\s+(allow-once|allow-always|allow|once|always|deny|reject|block)\s*$/i,
    +  );
    +  if (!m) {
    +    log?.warn?.("[DingTalk] /approve malformed — usage: /approve <id> <decision>");
    +    return;
    +  }
    +  const approvalId = m[1];
    +  // 与上游 commands-approve.ts 的 alias 表对齐——保持跨 channel 体验一致
    +  const rawDecision = m[2].toLowerCase();
    +  const decision: "allow-once" | "allow-always" | "deny" =
    +    rawDecision === "allow" || rawDecision === "once" ? "allow-once"
    +    : rawDecision === "always" ? "allow-always"
    +    : rawDecision === "reject" || rawDecision === "block" ? "deny"
    +    : (rawDecision as "allow-once" | "allow-always" | "deny");
    +
    +  log?.info?.(`[DingTalk] /approve intercept id=${approvalId} decision=${decision}`);
    +  try {
    +    const { resolveApprovalOverGateway } = await import(
    +      "openclaw/plugin-sdk/approval-gateway-runtime"
    +    );
    +    await resolveApprovalOverGateway({
    +      cfg,
    +      approvalId,
    +      decision,
    +      senderId,                                          // 入站消息的 senderStaffId
    +      clientDisplayName: "DingTalk",
    +    });
    +  } catch (err) {
    +    log?.warn?.(`[DingTalk] /approve resolve failed: ${getErrorMessage(err)}`);
    +  }
    +  return;                                                // ← 关键:return,不再进 reply 派发
    +}
    + +

    为什么 D2 不能"纯复用上游 /approve dispatcher"

    + + + + + + + + + + + + +
    方案结果
    v1 设想:用户敲 /approve → DingTalk 透传给上游 OpenClaw 的 commands-approve.ts dispatcher❌ 透传必须经过 channel 的 reply 派发 → reply 派发 acquireSessionLock → 死锁
    v2 实施:channel 端 early intercept,自己识别 /approve 格式,直接调 SDK 公开的 resolveApprovalOverGateway✅ 不进 reply 派发,不碰 session lock;上游 store 同样收敛到 resolved
    + +
    + 与上游 commands-approve.ts 的语义对齐 + channel 端的 regex 必须复现上游全部 decision alias(once/always/allow/deny/reject/block), + 这样钉钉用户敲的命令与 Discord/Telegram/Slack 完全一致。 + 上游若未来扩展 alias,channel 端需同步更新(在 §11.2 风险表登记)。 +
    @@ -764,11 +970,32 @@

    7.1 模板字段约定(approval-card-template-v1)

    -

    7.2 三按钮配置(actionId 编码与 D2/D3 对齐)

    -
    ButtonGroup (在 buttonGroupVisible=true 时渲染):
    -  ├─ ✅ 允许一次   actionId = "/approve ${approvalId} allow-once"   style=primary
    -  ├─ ✅ 总是允许   actionId = "/approve ${approvalId} allow-always" style=ghost
    -  └─ ⛔ 拒绝       actionId = "/approve ${approvalId} deny"         style=danger
    +

    7.2 三按钮配置(v2 修订:cardPrivateData 结构化 - D15 落地)

    +

    按钮通过 PR #480 引入的 CardBtn[] 模板字段渲染。三按钮共享 actionId "approval",区分靠 params.d

    +
    // approval-card-render.ts 内部
    +function makeApprovalBtns(approvalId: string): CardBtn[] {
    +  return [
    +    { text: "✅ 允许一次", color: "green",  status: "normal",
    +      event: { type: "sendCardRequest",
    +               params: { actionId: "approval",
    +                         params: { t: "approval", d: "allow-once", id: approvalId } } } },
    +    { text: "✅ 总是允许", color: "blue",   status: "normal",
    +      event: { type: "sendCardRequest",
    +               params: { actionId: "approval",
    +                         params: { t: "approval", d: "allow-always", id: approvalId } } } },
    +    { text: "⛔ 拒绝",     color: "red",    status: "normal",
    +      event: { type: "sendCardRequest",
    +               params: { actionId: "approval",
    +                         params: { t: "approval", d: "deny", id: approvalId } } } },
    +  ];
    +}
    +// 实际放进 cardParamMap:
    +//   cardParamMap.btns = JSON.stringify(makeApprovalBtns(approvalId))
    +//   cardParamMap.hasAction = "true"  (终态时改为 "false")
    +

    + 注意:DingTalk 平台会在回调里给每个按钮的 actionId 末尾追加 button index("approval" → "approval0"/"approval1"/"approval2"), + 回调解析使用 startsWith("approval") + params.d 取 decision 即可,详见 §1.2 与 §6.3。 +

    7.3 状态机

               ┌─ user click allow-once  ──► RESOLVED (allow-once)
    @@ -950,10 +1177,10 @@ 

    8. 错误处理矩阵

    偶发不一致;上游 store 仍是真相 - parseExecApprovalCommandText 失败 - actionId 被人手改坏 / 编码 bug - WARN 日志 + ack 平台 + 卡片不变(保留按钮) - 用户感觉点击没反应;可通过 /approve 命令兜底 + parseApprovalFromCardPrivateData 失败 + cardPrivateData 缺失 / actionIds[0] 不是 "approval" 前缀 / params 结构损坏 + WARN 日志 + ack 平台 + 卡片不变(保留按钮);continue 走原 callback 既有路径(feedback / btn_stop 等) + 非 approval 按钮回调不受影响;approval 按钮异常时可通过 /approve 命令兜底 权限校验失败 @@ -1007,7 +1234,9 @@

    9.1 测试文件布局

    tests/unit/approval-card-template.test.tscardParamMap 构造(含 stringify 规则)~8 tests/unit/approval-card-render.test.tspending/resolved/expired/canceled × exec/plugin = 8 个矩阵~16 tests/unit/approval-target-resolver.test.tsorigin 解析(含 turnSourceChannel=null)、DM 列表构造~10 - tests/unit/approval-callback-handler.test.tsactionId parse、权限校验、resolve 调用、各错误分支~18 + tests/unit/approval-callback-handler.test.tscardPrivateData 结构化 parse(含 button index 后缀)、权限校验、resolveApprovalOverGateway 调用、各错误分支;以 §1.2 的真机回调样本作 fixture~20 + tests/unit/card-callback-service.test.ts(扩展既有)D16 改动:analyzeCardCallbackcardPrivateData 含 actionIds + params;既有 feedback / btn_stop 用例不受影响+6 + tests/unit/inbound-handler-approve-intercept.test.ts§6.8 早期 intercept:群里带 @mention 前缀、私聊、各 decision alias、malformed 命令、resolveApprovalOverGateway 调用~12 tests/unit/approval-store.test.tsregister/get/remove/findByAccountId/finalizeAll~8 tests/unit/approval-cancel.test.tsstopClient 触发 finalize~4 tests/unit/approval-fallback-render.test.tsmarkdown 兜底文案、exec/plugin 分支~6 @@ -1051,42 +1280,53 @@

    9.4 覆盖率目标

    10. 实施阶段

    按"先上链路、再上 UX、最后部署模板"分 3 阶段。每阶段独立可合并,前一阶段不阻塞后一阶段对应的 PR review。

    +

    阶段 0 · 前置依赖(必须先满足)

    +
    + D17:阶段 1 PR 提交前必须完成的事 +
      +
    • 上游 openclaw >= 2026.4.7:才有 ChannelApprovalNativeRuntimeAdapter 契约(openclaw/src/infra/approval-handler-runtime-types.ts:216-235)与 resolveApprovalOverGateway 公开 API(openclaw/plugin-sdk/approval-gateway-runtime)。 +
      实施 PR-1 时 package.jsonpeerDependencies.openclaw 同步 bump,写入 release notes 作为 BREAKING change
    • +
    • 本仓库 PR #480 必须先合并:才有 AI Card v2 模板的 CardBtn[]sendCardRequest 回调格式支持。本设计的按钮渲染(§7.2)依赖这两个能力
    • +
    • approval-card 模板上传:在 open-dev.dingtalk.com/fe/card 导入 docs/assets/card-template-approval-v1.json,发布为预置统一模板,拿到 templateId(用于阶段 2 替换占位常量)
    • +
    +
    +

    阶段 1 · 接口骨架与权限链路(PR-1)

    • 新增 src/approval/ 目录与全部 9 个文件骨架
    • 实现 approval-config.tsapproval-store.tsapproval-target-resolver.tsapproval-capability.ts(不含 native runtime 完整实现)
    • src/channel.ts 挂上 approvalCapability,但 nativeRuntime 暂留 undefined,capability 仅生效 authorizeActorAction / resolveApproveCommandBehavior 等权限部分
    • +
    • src/inbound-handler.ts/approve 早期 intercept(§6.8,D2 落地)——这部分 PR-1 就要做,因为它就是 Feishu-同档体验的最后一公里
    • +
    • package.json peerDependency bump 到 openclaw >= 2026.4.7
    • schema、配置文档(草稿)
    • -
    • 测试:approval-configapproval-storeapproval-target-resolverapproval-capability
    • -
    • 这阶段交付后:用户已经能在钉钉里手敲 /approve <id> <decision> 完成审批(权限校验生效)。Feishu 同档能力。
    • +
    • 测试:approval-configapproval-storeapproval-target-resolverapproval-capabilityapprove-command-early-intercept
    • +
    • 这阶段交付后:用户已经能在钉钉里手敲 /approve <id> <decision> 完成审批(权限校验生效 + 早期 intercept 绕过 session lock)。Feishu 同档能力。

    阶段 2 · 完整 native runtime(PR-2)

      -
    • 实现 approval-card-template.ts(先用占位 templateId,注释 TODO)、approval-card-render.tsapproval-fallback-render.tsapproval-callback-handler.tsapproval-native-runtime.tsapproval-cancel.ts
    • -
    • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支
    • -
    • 在阶段 1 的 capability 里挂上 nativeRuntime
    • -
    • 测试:剩余 6 个 unit 文件 + 2 个 integration 文件
    • -
    • 这阶段交付后:完整三按钮卡片 UX 可用,但 templateId 仍是占位——本地 mock 测试已能验证全链路;真实部署需阶段 3
    • +
    • 实现 approval-card-template.ts(用阶段 0 拿到的正式 templateId 写入常量)、approval-card-render.ts(按 §7.2 出 cardPrivateData 结构化 btns)、approval-fallback-render.tsapproval-callback-handler.ts(用 parseApprovalFromCardPrivateData)、approval-native-runtime.tsapproval-cancel.ts
    • +
    • 修改 src/card-callback-service.ts(D16):CardCallbackAnalysiscardPrivateData 字段,analyzeCardCallback 抽 params 并附到 analysis
    • +
    • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支(在 feedback / btn_stop 之前)
    • +
    • 在阶段 1 的 capability 里挂上 nativeRuntime(含 5 子 adapter;interactions 的 clearPendingActions / cancelDelivered 有实质实现)
    • +
    • 测试:剩余 6 个 unit 文件 + 2 个 integration 文件;新增 callback-handler 与 cardPrivateData 解析的 test(基于 §1.2 的真机回调样本作 fixture)
    • +
    • 这阶段交付后:完整三按钮卡片 UX 在真机可用——templateId 已为正式发布版;可走真机回归(参照 skills/dingtalk-real-device-testing/SKILL.md
    -

    阶段 3 · 模板上传与发布(PR-3)

    +

    阶段 3 · 用户文档与回归收尾(PR-3)

      -
    • 登录 open-dev.dingtalk.com/fe/card,把 docs/assets/card-template-approval-v1.json 导入卡片搭建器
    • -
    • 调试模板布局(按 §7 mockup 对齐)
    • -
    • 发布为统一预置模板,获得正式 templateId
    • -
    • 替换 approval-card-template.ts 里的占位常量为真实 templateId
    • -
    • 真机回归(参照 skills/dingtalk-real-device-testing/SKILL.md
    • -
    • 更新用户文档 docs/user/features/exec-approval.md
    • -
    • 这阶段交付后:用户安装即用,零部署摩擦
    • +
    • 更新用户文档 docs/user/features/exec-approval.md(配置示例、approver 名单、UX 截图)
    • +
    • 更新 README 与 release notes(特别说明 peerDependency BREAKING)
    • +
    • 补真机回归记录到 docs/artifacts/(与现有 v2 卡片真机回归同模式)
    • +
    • 这阶段交付后:feature 正式宣告 production-ready
    分阶段的好处
      -
    • PR-1 可独立 merge,立刻让"在钉钉里用 /approve 命令"成为可能(Feishu-同档 baseline)
    • -
    • PR-2 引入大量代码但完全自给自足(不依赖模板上传),测试可用 mock templateId 全覆盖
    • -
    • PR-3 只动配置常量与文档,code-review 量极小,便于聚焦真机回归
    • +
    • PR-1 可独立 merge,立刻让"在钉钉里用 /approve 命令"成为可能(Feishu-同档 baseline);权限校验生效,安全已闭环
    • +
    • PR-2 引入大量代码但全部聚焦 native runtime;阶段 0 模板已发布,可直接进真机回归
    • +
    • PR-3 只动文档与回归记录,code-review 量极小,便于聚焦写作质量
    • 任何阶段回滚都不破坏前阶段已交付能力
    @@ -1115,9 +1355,14 @@

    11.2 已知风险

    阶段 3 在发布后做真机回归;fallback 路径保证不出现 approval 静默消失 - callback userId 字段语义假设(认为是 staffId) - 若实际是 unionId → 权限校验全部失败 - 真机回归阶段必查;若不符则在 callback-handler 加一次 unionId→staffId 解析(用现有 getUnionIdByStaffId 反向 API),命中后缓存 + 上游 commands-approve.ts 扩展 decision alias + 钉钉 channel 端 §6.8 的 regex 写死支持 once/always/allow/deny/reject/block;若上游新增 alias,channel 端用户敲新 alias 会被 channel 拒为 "malformed" + 在 §6.8 regex 旁加显式注释引用上游文件路径;CI 加一个测试断言上游 alias 集合不超出 channel 端支持范围(参考 openclaw/src/auto-reply/reply/commands-approve.ts alias 列表) + + + DingTalk 平台未来变更 cardPrivateData 字段命名 + 所有 approval 按钮点击解析失败 → 卡片按钮点了无反应 + parseApprovalFromCardPrivateData 失败时降级到 actionId.startsWith("approval") 简单匹配 + 从 actionId 末尾索引推 decision(兜底但精度差);同时上游 monitor warn 上报,触发 hotfix 机器人 DM approver 失败(企业权限) @@ -1153,7 +1398,13 @@

    上游 OpenClaw 核心类型与运行时

  • openclaw/src/infra/exec-approvals.ts — ExecApprovalRequest / Resolved
  • openclaw/src/infra/plugin-approvals.ts — PluginApprovalRequest / Resolved
  • openclaw/src/plugin-sdk/approval-delivery-helpers.ts:30-261 — createApproverRestrictedNativeApprovalCapability 工厂
  • -
  • openclaw/src/auto-reply/reply/commands-approve.ts/approve 命令解析与 dispatcher
  • +
  • openclaw/src/auto-reply/reply/commands-approve.ts/approve 命令解析与 dispatcher(channel 端 §6.8 regex 需保持与此处 alias 集合对齐)
  • +
  • openclaw/plugin-sdk/approval-gateway-runtimeresolveApprovalOverGateway 公开 API(v2026.4.7+ 引入;本设计的按钮 / 命令两条路径都调它)
  • + + +

    本设计的对照实现(必读)

    +
      +
    • PR #489 · feat(approval): exec/plugin approval card with command session dispatch(@BrilliantWang 并行实施)—— 揭示 4 处关键事实约束:session lock 死锁(§6.8)、cardPrivateData 结构化 payload(§1.2)、peerDependency bump(D17)、PR #480 依赖(阶段 0);本设计对其取舍:保留 approver schema、保留 finalizeActiveApprovalCardsForAccount、保留 markdown 兜底、保留 both surface

    对标 channel 实现

    From 898b4b2aa50469f189b66793bb41013141bc25f3 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Mon, 18 May 2026 22:31:55 +0800 Subject: [PATCH 03/44] =?UTF-8?q?docs(spec):=20v3=20=E7=BB=BC=E5=90=88=20g?= =?UTF-8?q?ap#01=20=E2=80=94=20PR#489=20=E9=AA=A8=E6=9E=B6=20+=20=E6=9C=AC?= =?UTF-8?q?=E8=AE=BE=E8=AE=A1=E6=9D=83=E9=99=90/=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=BE=B9=E7=95=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 按用户拍板的"#489 工程骨架 + 我们的目标边界"重新对齐: 采纳 PR#489(D18/D4 v1/D9 修订/D2 强化): - D18 无本地 approval-store——依赖上游 core activeEntries Map 管 pending 生命周期;transport.deliverPending 返回的 entry 由 core 自动带回给 updateEntry,channel 端不复制一份状态 - D4 v1 仅 origin-only(approver-DM 投递推迟到 v2 且 config-gated)。 原因:DM 双投递依赖机器人主动 DM 的企业权限、staffId 可达性、失败 兜底,v1 直接上风险大 - D9 模板 ID 保留 env 覆盖能力——v2 写"纯常量无 env"过于绝对,真机调试 / 私有部署仍需 env 灵活性,与现有 DINGTALK_CARD_TEMPLATE_ID 同模式 - D2 命令早期 intercept 强化为不可妥协的实施约束(session lock 死锁) 保留本设计(D7/§6.6/§9/§10): - approver schema + authorizeActorAction:PR#489 漏掉的安全闭环 - 非 approver 点击拒绝 + 私聊提示 - 测试矩阵 + 分阶段实施 放弃/推迟(D13/v2 future): - finalize-on-stop(依赖 store 故删除) - 默认 origin+DM 双投递 - interactions sub-adapter(v1 仅 4 子 adapter;v2 启用 DM 后再加) 模块层面:删除 approval-store.ts / approval-cancel.ts,新增 approval-command-intercept.ts;从 9 文件减到 8 文件,业务代码 ~1170 行 → ~830 行。测试 ~115 case → ~110 case。 各章节修订面:决策表新增 D18/D19,D4/D9/D13 重写;§3.1 拓扑图、§3.2 模块表、§3.3 接触面、§4.1 SDK 工厂参数表、§4.2 schema、§5 全部 sub- adapter 实现、§6.1 target resolver、§6.2 投递场景、§6.3 click flow、 §6.4 同步流(单卡)、§6.6 边界场景、§6.7 兜底、§7.8 mockup 标 v2、§8 错误矩阵、§9.1/9.3 测试、§10 阶段 1/2 + 新增 v2 future 节、§11.1/11.2、 §12 PR#489 引用同步对齐。 PR#489 当前 CONFLICTING(基于 4 月旧 main),不可直接合并;本设计需基于 当前 v3.6.3 main 重新实现,§12 已注明。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 527 ++++++++++-------- 1 file changed, 297 insertions(+), 230 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index 7f8273a4..eaca6458 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -41,8 +41,11 @@

    1. 总览

    -

    1.1 范围明确不做的事

    +

    1.1 v1 范围明确不做的事

      +
    • 不做 approver-DM 投递(D4 v3 修订)——v1 仅 origin-only,DM 双投递推迟到 v2 且 config-gated
    • +
    • 不引入本地 approval-store(D18 v3 新增)——依赖上游 core 的 activeEntries,channel 不复制 pending state
    • +
    • 不做停机 finalize-on-stop(D13 v3 推迟)——D18 删除 store 后失去枚举能力;遗留卡片在用户点击时降级为"已过期/已关闭"
    • 不抽通用 action dispatcher / registry —— peer 三家都没做,approval 走自己的回调前缀分支,与 feedback_up/down / btn_stop 在 TOPIC_CARD listener 中同级并列
    • 不动 btn_stop 与 feedback 既有路径(向后兼容,零回归风险)
    • 不为 markdown 模式做"教用户打 /approve 命令"的主路径文案——卡片是唯一推广 UX
    • @@ -141,9 +144,11 @@

      2. 已确认的决策清单

      D4 - Surface 默认值 - both + notifyOriginWhenDmOnly=true - 三家完全一致 + Surface 默认值(v3 修订) + v1:origin-only——审批卡片仅发到 agent 所在的 DingTalk 会话;clicker 必须是 approver。 +
      v2 future:在 config-gated 前提下增加 approver-DM 投递(channels.dingtalk.execApprovals.target: "dm" | "both"),需先解决机器人主动 DM 的企业权限 / staffId 可达性 / 失败兜底问题。 +
      原因:DM 双投递依赖机器人主动发任意用户的能力,受企业权限与 oToMessages 调用频率限制;v1 直接上双投递风险大。CLI 触发场景(无 origin)在 v1 通过 /approve 文本命令兜底。 + 对齐 PR #489 v1 D5 @@ -171,9 +176,11 @@

      2. 已确认的决策清单

      D9 - 卡片模板 - 新建 approval 专用模板(独立 templateId),纯常量内置,无 env 覆盖;JSON 源 commit 到 docs/assets/ - —(DingTalk 特化) + 卡片模板(v3 修订) + 新建 approval 专用模板(独立 templateId);内置默认常量 + 保留 env 覆盖能力DINGTALK_APPROVAL_CARD_TEMPLATE_ID)。 +
      默认值确保用户开箱即用,env 覆盖给真机调试、模板迭代、私有部署留出空间——与现有 DINGTALK_CARD_TEMPLATE_ID 同模式。 +
      JSON 源 commit 到 docs/assets/card-template-approval-v1.json + 对齐现有 AI Card 惯例 D10 @@ -195,9 +202,11 @@

      2. 已确认的决策清单

      D13 - 停机取消 - 复刻现有 finalizeActiveCardsForAccount 模式,stopClient 时把所有 pending approval 卡片刷成"已取消(通道断开)" - —(DingTalk 特化) + 停机取消(v3 推迟到 v2) + v1 不做——本来要复刻 finalizeActiveCardsForAccount 模式,但 D18 删除了本地 approval-store 后 channel 端不再有 pending entries 的枚举能力。 +
      v1 行为:stopClient 时遗留 approval 卡片保留按钮态;用户点击 → resolveApprovalOverGateway → 上游若 entry 已失效则返回 not-found → 卡片可降级为静默或 best-effort update。 +
      v2 future:若上游 SDK 暴露 activeEntries 查询 API,或 channel 引入轻量 outTrackId 集合(仅供 stop-time 清理用,非完整 store),再实现 finalize-on-stop + 对齐 PR #489(PR #489 也不做停机 finalize) D14 @@ -220,8 +229,21 @@

      2. 已确认的决策清单

      D17 前置依赖 - peerDependency 必须 bump 到 openclaw >= 2026.4.7(才有 ChannelApprovalNativeRuntimeAdapter 契约与 resolveApprovalOverGateway 公开 API);真机验证需要本仓库 PR #480(AI Card v2 / CardBtn[] + sendCardRequest 回调格式) 先合并 - 对齐 PR #489 实施约束 + peerDependency 必须 bump 到 openclaw >= 2026.4.7(才有 ChannelApprovalNativeRuntimeAdapter 契约与 resolveApprovalOverGateway 公开 API);真机验证需要本仓库 PR #480(AI Card v2 / CardBtn[] + sendCardRequest 回调格式) 先合并;本仓库当前 main 已到 v3.6.3,PR #489 基于 4 月旧 main 且 CONFLICTING,不可直接合并——本设计是基于当前 main 与上游 openclaw 的重新整理实现 + 对齐 PR #489 + 当前 main 约束 + + + D18 + 本地 store(v3 新增) + 不引入本地 approval-store.ts。channel 端不复制一份 pending entries——上游 ChannelApprovalNativeRuntimeAdapter 已在 core 的 activeEntries Map 管理生命周期,transport.deliverPending 返回的 entry(含 outTrackId / conversationId / accountId)会被 core 自动带回给 transport.updateEntry({ entry, payload, phase }),channel 直接从 entry 读字段即可。 +
      影响:D13 停机 finalize 在 v1 不做;重启行为是"点击旧按钮 → 上游返回 not-found → 卡片刷成已过期/已关闭"(详见 §6.6 重启场景) + 对齐 PR #489 + + + D19 + 实施基调(v3 新增) + 本设计是 PR #489 工程骨架 + 本设计的权限/测试/分阶段边界的综合,不是任意一边的完整复制。具体取舍见上方 v3 callout 表格 + — @@ -230,10 +252,24 @@

      2. 已确认的决策清单

      本设计的版本演进
      • v1(2026-05-18 初版):基于上游 SDK + peer 三家对标做的纸面设计,D2 写"纯复用 /approve dispatcher",按钮 payload 用 Telegram 风格字符串字面量
      • -
      • v2(2026-05-18 修订):对比 PR #489(@BrilliantWang 并行实施)+ 真机回调 payload 实证,确认 4 处必须改的事实约束 → 新增 D15/D16/D17,D2 重写。下文 §1.2 / §3.3 / §5.2 / §6.3 / §6.8 / §7.2 / §8 / §10 / §11 已全部对齐 v2 状态
      • -
      • userId 字段身份核实:用户在 v2 修订过程中确认 callback userId === staffId(不是 unionId),所以 §4.2 approver schema(staffId 名单)保持不变
      • +
      • v2(2026-05-18 修订):对比 PR #489(@BrilliantWang 并行实施)+ 真机回调 payload 实证,新增 D15/D16/D17,D2 重写
      • +
      • v3(2026-05-18 综合):明确取舍——采用 PR #489 工程骨架(D18/D19)保留本设计的权限/测试/分阶段边界。v1 范围缩到 origin-only,DM 双投递降级为 v2 future-work,本地 approval-store 删除(依赖 core activeEntries),模板 ID 保留 env 覆盖能力。详见下方 D18/D19 与各章节修订
      • +
      • userId 字段身份核实:v2 已确认 callback userId === staffId(不是 unionId),所以 §4.2 approver schema(staffId 名单)保持不变
      + +
      + v3 的设计哲学:PR #489 骨架 + 我们的边界 + + + + + + + + +
      类别来源具体决策
      采纳 PR #489工程骨架D18 无本地 approval-store;D4 v1 origin-only;D2 命令早期 intercept;D15 cardPrivateData 编码;按钮回调直接调 resolveApprovalOverGateway
      保留本设计目标边界D7 approver schema;D5 非 approver 点击拒绝;§9 测试矩阵;§10 分阶段;createApproverRestrictedNativeApprovalCapability 工厂用法
      放弃 / 推迟D13(停机 finalize,依赖 store 故删除);D4 v1 不做 DM 投递;D9 模板 ID 保留 env 覆盖(不再"纯常量")
      PR #489 不能直接合基于 4 月旧 main,CONFLICTING;当前 main 已到 3.6.3,runtime/package 结构变化;需按当前 main 与上游 openclaw 重整实现(不是 cherry-pick 489)
      +
      @@ -261,17 +297,18 @@

      3.1 上下游分工

      ▼ ┌──────────────────── openclaw-channel-dingtalk (本仓库) ────────────────────┐ │ │ -│ src/approval/ ── 新增 domain 目录 │ +│ src/approval/ ── 新增 domain 目录(v3:8 个文件,比 v2 少 store/cancel) │ │ ├─ approval-capability.ts ApprovalCapability 单例装配 │ -│ ├─ approval-native-runtime.ts 5 子 adapter 入口(lazy load) │ -│ ├─ approval-card-template.ts 模板 ID 常量 + cardParamMap helper │ +│ ├─ approval-native-runtime.ts 4 子 adapter 入口(含可选 5th obs) │ +│ ├─ approval-card-template.ts 模板 ID(const+env)+ helper │ │ ├─ approval-card-render.ts pending / resolved / expired 渲染 │ │ ├─ approval-fallback-render.ts markdown 兜底(仅 error-recovery) │ -│ ├─ approval-target-resolver.ts origin / DM target 解析 │ -│ ├─ approval-callback-handler.ts TOPIC_CARD 回调入口 approve: 分支 │ -│ ├─ approval-config.ts execApprovals.* schema 读写 │ -│ ├─ approval-store.ts 内存 pendingEntry (outTrackId↔id) │ -│ └─ approval-cancel.ts stopClient 时 finalize 所有 pending │ +│ ├─ approval-target-resolver.ts v1: 仅 origin;v2: + DM │ +│ ├─ approval-callback-handler.ts TOPIC_CARD 卡片按钮回调入口 │ +│ ├─ approval-command-intercept.ts /approve 命令早期 intercept(v3) │ +│ └─ approval-config.ts execApprovals.* schema 读写 │ +│ │ +│ 上游 core 管 pending 生命周期(activeEntries Map);channel 不引入本地 store │ │ │ 改造点(增量、向后兼容) │ │ ├─ src/channel.ts 新增 approvalCapability 字段 │ @@ -323,37 +360,34 @@

      3.2 模块单一职责表

      approval-target-resolver.ts - resolveOriginTarget + resolveApproverDmTargets,从 request 与 config 提取 DingTalk target + v1:仅 resolveOriginTarget(从 turnSourceTo 还原 DingTalk target,保留 user:/group: 前缀)。v2 future:增加 resolveApproverDmTargets normalizeDingTalkTarget, config - ~120 + ~80(v1) approval-callback-handler.ts - TOPIC_CARD 收到 "/approve …" 前缀的 actionId 时:parse → authorize → resolve gateway → ack - gateway primitive, capability auth, store + TOPIC_CARD 收到 cardPrivateData.actionIds[0]"approval" 前缀的回调时:parseApprovalFromCardPrivateData → authorize → resolveApprovalOverGateway → ack。不依赖本地 store(D18),entry 数据由 core activeEntriestransport.updateEntry 回灌 + capability auth, parseApprovalFromCardPrivateData, SDK gateway runtime ~150 approval-config.ts - 纯读 helper:getExecApprovalsConfig / listExecApprovers / isExecAuthorizedSender / resolveNativeDeliveryMode + 纯读 helper:getExecApprovalsConfig / listExecApprovers / isExecAuthorizedSender / resolveNativeDeliveryMode(v1 永远返回 "channel") config 模块 ~90 - approval-store.ts - per-process Map:approvalId ↔ entry,支持 register/get*/remove/finalizeAll - — - ~80 - - - approval-cancel.ts - finalizeActiveApprovalCardsForAccount(accountId, reason):扫 store + PUT update 为"已取消(通道断开)" - approval-store, card-service + approval-command-intercept.ts
      (v3 新增模块) + 导出 tryInterceptApproveCommand({ cfg, text, senderId, log }):regex 匹配 /approve <id> <decision> → alias 归一 → resolveApprovalOverGateway。由 src/inbound-handler.ts 入口早期调用(§6.8) + SDK gateway runtime ~70 -

      合计新增 ~1170 行业务代码;测试代码预计 ~2200 行(按 2× 比例,含 fixture)。

      +

      + v3 删除:approval-store.ts(D18:依赖 core activeEntries)、approval-cancel.ts(无 store 无法枚举 pending;D13 推迟到 v2)。
      + 合计新增约 ~830 行业务代码(v2 是 ~1170 行,少 ~340 行);测试代码预计 ~1700 行。 +

      3.3 与现有代码的接触面

      @@ -429,11 +463,11 @@

      4.1 用 SDK 工厂的输入清单

      - - - - - + + + + + @@ -454,15 +488,14 @@

      4.2 配置 schema(D7 落地)

      approvers: # staffId 列表,支持 dingtalk:/dd:/ding: 前缀 - "staff001" - "dingtalk:staff002" # ← 等价于 "staff002" - target: both # both | dm | channel(默认 both) - ttlMs: 600000 # 可选 channel-level TTL hint;不传时跟随上游默认(建议留空) + # target: channel # v1 写死 channel(origin-only);v2 才支持 channel | dm | both + # ttlMs: 600000 # v2 future:可选 channel-level TTL hint # 多账号 override accounts: acme: execApprovals: approvers: ["staff100"] # 完全替换 channel-level 名单 - target: dm # 此账号偏好 DM-only # 全局 fallback(peer 惯例;与 dingtalk 无关,由上游统一处理) commands: @@ -535,57 +568,50 @@

      5.3 transport

      - + - - + + - - + + - - + +
      isExecAuthorizedSenderisExecAuthorizedSender({ cfg, accountId, senderId }),clicker staffId 在名单中
      isPluginAuthorizedSender可选默认 = isExecAuthorizedSender(同一份 approver 名单管两类)
      isNativeDeliveryEnabled检查 execApprovals.enabled !== false && hasApproversauto 视为 true)
      resolveNativeDeliveryModeexecApprovals.target"both" | "dm" | "channel"),默认 "both"
      requireMatchingTurnSourceChannel可选false(允许 CLI/WebUI 触发的 approval 也通过 DingTalk 投递)
      resolveOriginTarget可选详见 §6.1:从 turnSourceTo + turnSourceThreadId 还原 DingTalk target
      resolveApproverDmTargets可选详见 §6.1:approver staffId 列表 → 每个 staffId 一条 { to: "user:<staffId>" }
      notifyOriginWhenDmOnly可选true(D4)
      resolveNativeDeliveryModev1:永远返回 "channel"(origin-only)。
      v2:读 execApprovals.target"channel" | "dm" | "both"),默认 "channel"
      requireMatchingTurnSourceChannel可选v1: true(origin-only 必须有 dingtalk turn source;CLI 触发场景靠 /approve 命令兜底)。
      v2: 改 false 后才有 DM 兜底
      resolveOriginTarget必须(v1)详见 §6.1:从 turnSourceTo + turnSourceThreadId 还原 DingTalk target(保留 user:/group: 前缀)
      resolveApproverDmTargetsv1 不实现v2 future:approver staffId 列表 → 每个一条 { to: "user:<staffId>" };v1 设为 undefined 即可
      notifyOriginWhenDmOnly可选v1: false(无 DM 路径,字段无意义)。v2 改 true
      nativeRuntime可选createLazyChannelApprovalNativeRuntimeAdapter({ load, isConfigured, shouldHandle, eventKinds: ["exec","plugin"] }),详见 §5
      describeExecApprovalSetup可选返回中文配置指南字符串,引导用户填 channels.dingtalk.execApprovals.approvers
      prepareTarget({ plannedTarget, request, view, pendingPayload })plannedTarget.tonormalizeApprovalTargetTo(§6.1)确保带 user:/group: 前缀;返回 { target: { to: normalizedTo, threadId: null }, dedupeKey: \`${accountId}:${normalizedTo}\` }。DingTalk 无 thread 概念,threadId 永远 null。plannedTarget.tonormalizeApprovalTargetTo(§6.1)确保带 user:/group: 前缀;返回 { target: { to: normalizedTo, isGroup: normalizedTo.startsWith("group:"), accountId }, dedupeKey: \`dingtalk:${accountId}:${normalizedTo}\` }。DingTalk 无 thread 概念,threadId 永远 null。
      deliverPending({ preparedTarget, request, view, pendingPayload })根据 preparedTarget.target.to 的前缀分支:
      group:cid_xxxcard-service.createAndDeliver({ openConversationId: stripPrefix(target.to), ... })
      user:<staffId>card-service.createAndDeliver({ userIds: [stripPrefix(target.to)], ... })
      统一参数:outTrackId = \`approval-${request.id}-${hash(target.to)}\`(hash 避免 outTrackId 含冒号导致 PUT 路径解析问题),templateId = BUILTIN_APPROVAL_CARD_TEMPLATE_IDcardParamMap 来自 buildPendingPayloadcallbackType: "STREAM"
      成功:approval-store.register({ approvalId, outTrackId, accountId, target: preparedTarget.target, deliveredAt: Date.now(), kind: request.kind }),返回 entry。
      失败:observe.onDeliveryError + 调 approval-fallback-render 发 markdown/text 兜底(仍写一条 fallback entry 到 store 留痕)
      deliverPending({ cfg, accountId, preparedTarget, request, pendingPayload })根据 preparedTarget.target.isGroup 分支调 card-service.createAndDeliver
      • group → { openConversationId: stripPrefix(target.to), ... }
      • user → { userIds: [stripPrefix(target.to)], ... }
      统一参数:outTrackId = \`approval_${request.id}_${hash(target.to)}\`templateId = resolveApprovalTemplateId()(const 或 env 覆盖),cardParamMap 来自 buildPendingPayloadcallbackType: "STREAM"。 +
      成功:直接返回 entry { approvalId: request.id, outTrackId, conversationId: stripPrefix(target.to), isGroup, accountId }——不写本地 store(D18),core 会自动把这个 entry 缓存到 activeEntries 并在后续 updateEntry 调用时带回。 +
      失败:调 observe.onDeliveryError + approval-fallback-render 发 markdown 兜底;返回 null 让 core 知道未成功投递
      updateEntry({ entry, payload, phase })updateCardVariables(entry.outTrackId, payload.cardParamMap, token)(即 PUT /v1.0/card/instances)。phase=expired/resolved 时同时 approval-store.remove(entry.approvalId)updateEntry({ cfg, accountId, entry, payload, phase })updateCardVariables(entry.outTrackId, payload.cardParamMap, token)(PUT /v1.0/card/instances)。entry 由 core 从 activeEntries 取出回传,不需要查任何本地 store。phase=expired/resolved 时 core 自动从 activeEntries 移除该 entry
      deleteEntry({ entry, phase })not used(D14: 永远 update,从不 delete)。占位实现:仅 approval-store.removedeleteEntry({ cfg, accountId, entry, phase })not used(D14:永远 update,从不 delete)。v1 不实现(SDK 允许 deleteEntry 缺省)
      -

      5.4 interactions(可选,但本设计实现)

      +

      5.4 interactions(v3 修订:v1 不实现,作为 v2 future)

      +
      + v1 不实现 interactions sub-adapter。原因: +
        +
      • bindPending / unbindPending:DingTalk 按钮 actionId 编码已写进 cardParamMap,平台侧无需额外 binding
      • +
      • clearPendingActions / cancelDelivered:在 v3 origin-only + 无本地 store 模式下,这些钩子的语义价值不明显——终态由 updateEntry({ phase: "resolved"/"expired" }) 已经把按钮一起 update 掉
      • +
      + v2 future:若引入 DM 投递与多消息状态同步,再实现 clearPendingActions / cancelDelivered。 +
      + +

      5.5 observe(v1 实现 onDeliveryError + onDelivered 用于日志)

      - - - - - - - - - - + + - - - - - -
      方法DingTalk 实现
      bindPending({ entry, request, view, pendingPayload })no-op(DingTalk 端按钮的 actionId 编码已经在 buildPendingPayload 写进 cardParamMap,平台侧无需额外 binding)。返回 null 表示无 binding
      unbindPending({ entry, binding, request })no-op
      clearPendingActions({ entry, phase })updateCardVariables(entry.outTrackId, { buttonGroupVisible: false }, token),仅隐藏按钮;statusFooter 不动(由 updateEntry 负责)onDelivered({ entry, request })INFO 日志 [DingTalk][Approval] delivered approval=<id> outTrackId=<...>
      cancelDelivered({ entry, request })updateCardVariables 把 statusFooter 改成"❌ 已取消",buttonGroupVisible=false
      - -

      5.5 observe(可选,本设计仅实现 onDeliveryError 用于日志)

      - - - - + - - + +
      方法DingTalk 实现
      onDeliveryError({ error, plannedTarget, request })结构化日志 [DingTalk][Approval][DeliveryError] approvalId=<id> target=<to> error=<msg>WARN 日志 [DingTalk][Approval][DeliveryError] approval=<id> target=<to> error=<msg>在此调用 markdown 兜底——兜底在 deliverPending 错误分支同步触发,避免重复发消息
      onDuplicateSkipped / onDelivered仅 debug 日志onDuplicateSkippedv1 不实现——origin-only 模式下 dedupe 不会真发生(只有一个 target)
      @@ -617,50 +643,64 @@

      resolveOriginTarget

      // - 输入若为裸 staffId → 加 "user:" 前缀 // - 与 sendProactiveTextOrMarkdown 内部 stripTargetPrefix 的反向约定保持一致
    -

    resolveApproverDmTargets

    -
    approvers = listExecApprovers({ cfg, accountId })  // staffId[] after normalize
    -return approvers.map(staffId => ({
    -  to: `user:${staffId}`,                  // 永远带 user: 前缀
    -  threadId: null,
    -}))
    +

    resolveApproverDmTargets(v1 不实现)

    +
    + v1 不实现该 resolver——SDK 工厂调用 createApproverRestrictedNativeApprovalCapability 时该字段传 undefined。 + 上游 approval-runtime 看到 supportsApproverDmSurface: falseresolveNativeDeliveryMode: "channel" 后不会触发 DM 路径。 + v2 future 实现时会按 approvers.map(staffId => ({ to: \`user:${staffId}\`, threadId: null })) 模式输出。 +
    -

    6.2 投递 pending(origin + DM 双路径 + 去重)

    -
    场景:用户在钉钉群 cid_xxx 里 @ agent,agent 跑命令需要批准。
    +        

    6.2 投递 pending(v3:仅 origin)

    +
    场景 A:用户在钉钉群 cid_xxx 里 @ agent,agent 跑命令需要批准。
            approvers = ["staffA", "staffB"]
            触发用户也是 staffA(在群里)
     
    -approval-runtime 投递计划:
    -  ├─ origin   = { to: "group:cid_xxx" }
    -  ├─ DM[0]    = { to: "user:staffA" }
    -  └─ DM[1]    = { to: "user:staffB" }
    +approval-runtime 投递计划(D4 v1: origin-only):
    +  └─ origin = { to: "group:cid_xxx" }     ← 只有这一条
    +
    +调 prepareTarget:
    +  └─ origin → dedupeKey = "dingtalk:default:group:cid_xxx"
    +              target    = { to: "group:cid_xxx", isGroup: true, accountId: "default" }
    +
    +调 deliverPending:
    +  └─ group:cid_xxx → createAndDeliver({
    +        outTrackId: "approval_abc123_",
    +        openConversationId: "cid_xxx",
    +        cardData.cardParamMap: { btns: , content: "...", hasAction: "true" },
    +        callbackType: "STREAM",
    +      })
    +  └─ 返回 entry = { approvalId:"abc123", outTrackId, conversationId:"cid_xxx",
    +                    isGroup:true, accountId:"default" }
    +  └─ core 把 entry 缓存到 activeEntries(channel 端无本地 store)
    +
    +→ 群里 staffA 和 staffB 都能在群消息流看到审批卡片
    +  staffA / staffB 任一点击都能批(因都在 approvers 名单;§6.3 权限校验)
    +  群里其它非 approver 点击 → 拒绝提示(§6.6)
    +
    +———————————————————————————————————————————————————————————————————
    +
    +场景 B:用户 staffA 直接跟 agent DM 触发 exec:
    +       origin = { to: "user:staffA" }
     
    -依次调 prepareTarget:
    -  ├─ origin    → dedupeKey = "default:group:cid_xxx"
    -  ├─ DM[0]     → dedupeKey = "default:user:staffA"
    -  └─ DM[1]     → dedupeKey = "default:user:staffB"
    -  → 三个 dedupeKey 全不同 → 三条都投
    +调 prepareTarget:
    +  └─ origin → dedupeKey = "dingtalk:default:user:staffA"
    +              target    = { to: "user:staffA", isGroup: false, accountId: "default" }
     
    -依次调 deliverPending:
    -  ├─ group:cid_xxx → createAndDeliver outTrackId="approval-abc123-group:cid_xxx"  ✓
    -  ├─ user:staffA   → createAndDeliver outTrackId="approval-abc123-user:staffA"    ✓
    -  └─ user:staffB   → createAndDeliver outTrackId="approval-abc123-user:staffB"    ✓
    +调 deliverPending:
    +  └─ user:staffA → createAndDeliver({ userIds: ["staffA"], ... }) ✓
     
    -approval-store 写 3 条 entry,所有 entry.approvalId="abc123"
    +→ staffA 私聊里看到审批卡片,自己点自己批(D5 self-approval 允许)
     
     ———————————————————————————————————————————————————————————————————
     
    -变体场景:用户 staffA 直接跟 agent DM 触发 exec:
    -       origin   = { to: "user:staffA" }
    -       DM[0]    = { to: "user:staffA" }   ← 与 origin 相同
    -       DM[1]    = { to: "user:staffB" }
    +场景 C:用户从 CLI 跑 codex 触发 exec(turnSourceChannel 为空 / 不是 dingtalk):
    +       resolveOriginTarget 返回 null(v1 没有 DM fallback)
     
    -prepareTarget:
    -  ├─ origin → dedupeKey = "default:user:staffA"
    -  ├─ DM[0]  → dedupeKey = "default:user:staffA"  ⚠ 重复!
    -  └─ DM[1]  → dedupeKey = "default:user:staffB"
    +→ v1 channel 端不投任何 approval 卡片
    +→ 用户需要自己在钉钉里手敲 /approve   完成审批(§6.8)
    +   approval id 用户从 CLI 的 OpenClaw 输出里复制
     
    -上游 SDK 见 dedupeKey 重复 → DM[0] 自动跳过(onDuplicateSkipped 日志)
    -最终只投 2 条:staffA 一张(origin 角色)+ staffB 一张
    +v2 future:CLI 触发场景启用 DM 投递,让 approver 在私聊里直接点按钮

    6.3 点击 approve → 上游 resolve(核心交互链路)

    用户在卡片上点"允许一次"
    @@ -695,20 +735,18 @@ 

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ 2. (v2 不再走 parseExecApprovalCommandText——按钮路径走 cardPrivateData 解析; │ 命令路径走 §6.8 早期 intercept 的 regex;两条路径最终都调 resolveApprovalOverGateway) │ - │ 3. 查 store 找 entry - │ entry = approval-store.getByApprovalId("abc123") - │ if (!entry) { - │ updateCardVariables(payload.outTrackId, - │ { statusFooter: "⏰ 已过期或已关闭", - │ buttonGroupVisible: false }, token) - │ return { handled: true, reason: "not-found" } - │ } + │ 3. (v3 删除"查 store 找 entry"步骤) + │ channel 端不维护本地 store(D18);entry 数据由 core + │ activeEntries 经 transport.updateEntry 回灌。本步直接进权限校验。 + │ approval 是否已过期/被 resolve 等状态判定由上游 + │ resolveApprovalOverGateway 内部处理(失败时抛 already-resolved / + │ not-found 等具名错误)。 │ - │ 4. 权限校验 + │ 4. 权限校验(approval kind 暂用 "exec"——上游接口当前不区分) │ authorized = capability.authorizeActorAction({ │ cfg, accountId, senderId: analysis.userId, │ action: "approve", - │ approvalKind: entry.kind }) + │ approvalKind: "exec" }) // v1:exec/plugin 共用同一份 approvers │ if (!authorized.authorized) { │ sendProactiveTextOrMarkdown( │ config, @@ -721,13 +759,22 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ 5. 调上游 SDK 公开 API 回写(v2026.4.7+) │ import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime" │ - │ await resolveApprovalOverGateway({ - │ cfg, - │ approvalId: parsed.approvalId, - │ decision: parsed.decision, // "allow-once" | "allow-always" | "deny" - │ senderId: analysis.userId, // staffId - │ clientDisplayName: "DingTalk", - │ }) + │ try { + │ await resolveApprovalOverGateway({ + │ cfg, + │ approvalId: parsed.approvalId, + │ decision: parsed.decision, // "allow-once" | "allow-always" | "deny" + │ senderId: analysis.userId, // staffId + │ clientDisplayName: "DingTalk", + │ }) + │ } catch (err) { + │ // 已过期 / 已被批 / 不存在 → 上游会抛 already-resolved / not-found + │ // 等具名错误。catch 后兜底 update 卡片为 "⏰ 已过期或已关闭", + │ // 让用户看到明确反馈而不是按钮"点了无反应" + │ await updateCardVariables(payload.outTrackId, { + │ status: "⏰ 已过期或已关闭", hasAction: "false", btns: "[]", + │ }, token).catch(() => {}) + │ } │ // SDK 内部根据 approval kind 自动 dispatch 到 │ // exec.approval.resolve / plugin.approval.resolve │ // channel 端不需要关心 kind 区分 @@ -739,45 +786,54 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ │ 6. 兜底立即 update 本机这张(防止上游事件回环延迟) │ updateCardVariables(payload.outTrackId, - │ buildResolvedCardParamMap(...), token) + │ buildResolvedCardParamMap(parsed.decision, analysis.userId), + │ token) + │ // 即使上游 updateEntry 还没触发,用户也能立刻看到终态卡片 │ │ 7. 日志 + return { handled: true, reason: "resolved" } │ ├─ 分支命中 → 跳过既有 handleCardAction └─ finally: socketCallBackResponse(messageId, { success: true }) (ack 平台)
    -

    6.4 上游 resolve 后的卡片状态同步

    -

    当任意一张卡片被点击 → 上游 store 标记为 resolved → approval-handler-runtime 会对**所有 entry**(origin + 所有 DM)调一次 transport.updateEntry({ phase: "resolved" })。这保证所有人看到的卡片都同步更新。

    - -
    approval abc123 被 staffA 在群里点了"允许一次"
    -
    -       ┌─────────────────────────┐
    -       │ upstream approval store │
    -       │  abc123 → resolved      │
    -       │  decision = allow-once  │
    -       │  resolvedBy = staffA    │
    -       └────────────┬────────────┘
    -                    │ 触发
    -                    ▼
    -       presentation.buildResolvedResult()
    +        

    6.4 上游 resolve 后的卡片状态同步(v3:单卡)

    +

    v1 origin-only 模式下,每个 approval 只有 1 张卡片(origin 会话里那张)。点击 → 上游 store 标记 resolved → approval-handler-runtime 对这 1 张 entry 调 transport.updateEntry({ phase: "resolved" })。channel 端 callback handler 自己也会做一次 best-effort update(§6.3 step 6),所以视觉上看到终态来源有两条:

    + +
    approval abc123 被 staffA 在群 cid_xxx 点了"允许一次"
    +
    +       ┌──────────────────────────────────────┐
    +       │ channel callback handler(§6.3 step 6)│
    +       │   立即 PUT updateCardVariables       │  ← 第一次 update(同步,无网络往返延迟)
    +       │   status="✅ 已批准", hasAction=false  │     用户即时看到终态
    +       └──────────────────┬───────────────────┘
    +                          │
    +                          ▼ (resolveApprovalOverGateway 异步返回)
    +       ┌─────────────────────────────────────┐
    +       │ upstream approval store              │
    +       │   abc123 → resolved                  │
    +       │   decision = allow-once               │
    +       │   resolvedBy = staffA                 │
    +       └──────────────────┬──────────────────┘
    +                          │ 触发
    +                          ▼
    +       presentation.buildResolvedResult({ view, entry })
            returns { kind: "update", payload: {
    -         statusFooter: "✅ 已批准 by @staffA (allow-once)",
    -         buttonGroupVisible: false,
    +         phase: "resolved",
    +         decision: "allow-once",
    +         // 可附带 resolvedBy 显示给用户
            }}
    -                    │
    -                    │  approval-runtime 遍历所有 entry
    -                    ▼
    -       ┌─────────────────────────────────────────────┐
    -       │ for entry of store.findByApprovalId(abc123) │
    -       │   transport.updateEntry({ entry, payload })  │
    -       │   → PUT /v1.0/card/instances                 │
    -       └─────────────────────────────────────────────┘
    -                    │
    -       ┌────────────┼────────────┐
    -       ▼            ▼            ▼
    -   group 卡片    DM-A 卡片    DM-B 卡片
    -   都被 PUT 成   都被 PUT 成   都被 PUT 成
    -   "✅ 已批准"   "✅ 已批准"   "✅ 已批准"
    + │ + ▼ + transport.updateEntry({ cfg, accountId, entry, payload, phase }) + → PUT /v1.0/card/instances ← 第二次 update(幂等,与第一次结果相同) + → status="✅ 已批准 by @staffA (allow-once)", hasAction=false + + 两次 update 内容一致,无视觉差异;幂等覆盖 OK。 + core 在 phase=resolved 后从 activeEntries 移除该 entry。 + +——————————————————————————————————————————————————————————————————— + +v2 future(DM 投递启用后):core 会对**所有 entry**(origin + 每个 DM) +都调 updateEntry,所有卡片同步刷成相同终态。

    6.5 用户点 deny / allow-always 的差异

    @@ -793,8 +849,9 @@

    6.6 失败 / 边界场景

    用户重复点击(按钮看起来还在但已 resolved)

      -
    1. 上游 exec.approval.resolve 返回 already-resolved 错误
    2. -
    3. callback-handler 捕获错误,调 updateCardVariables 把卡片刷成终态(即使 UI 还没刷过来也立即修正)
    4. +
    5. callback-handler 调 resolveApprovalOverGateway
    6. +
    7. 上游返回 already-resolved 类的具名错误(catch 到)
    8. +
    9. catch 分支调 updateCardVariables 把卡片刷成"⏰ 已过期或已关闭"(即使 UI 还没刷过来也立即修正)
    10. 对用户:第二次点击看起来等同于第一次"已生效",无打扰提示
    @@ -806,34 +863,31 @@

    非 approver 点击

  • 日志 [DingTalk][Approval][Denied] approvalId=<id> clicker=<userId>
  • -

    Channel 重启后用户点旧卡片

    +

    Channel 重启后用户点旧卡片(v3:no-store 模式下的行为)

      -
    1. approval-store 内存清空 → getByApprovalId 返 null
    2. -
    3. callback-handler 进 not-found 分支
    4. -
    5. 卡片刷成"⏰ 已过期或已关闭",按钮隐藏
    6. -
    7. 用户体感:按钮"点了一下变灰,没批成"——对长时间下线降级,可接受
    8. +
    9. channel 端无本地 store(D18),但 entry 信息从平台回调的 outTrackId 仍能拿到
    10. +
    11. callback-handler 解析 cardPrivateData 拿到 approvalId + decision → 直接调 resolveApprovalOverGateway
    12. +
    13. 上游 core 在重启后 activeEntries 也清空了,resolveApprovalOverGateway 返回 not-found
    14. +
    15. callback-handler 进 catch 分支 → updateCardVariables(outTrackId, { status: "⏰ 已过期或已关闭", hasAction: "false", btns: "[]" })
    16. +
    17. 用户体感:按钮点了一下卡片刷成"已过期",明确反馈——对长时间下线降级,可接受

    上游过期事件触达

    upstream timer 触发 approval.expired event
    -  ├─ presentation.buildExpiredResult() → { kind: "update", payload: {
    -  │     statusFooter: "⏰ 已过期(未在 10 分钟内响应)",
    -  │     buttonGroupVisible: false,
    +  ├─ presentation.buildExpiredResult({ entry, view }) → { kind: "update", payload: {
    +  │     phase: "expired",
       │   }}
    -  └─ transport.updateEntry({ phase: "expired" }) for all entries
    -       → 所有卡片同步更新为过期态
    -       → approval-store.remove(approvalId)
    - -

    Channel stopClient(账号停用 / gateway 重启)

    -
    gateway.stopClient(accountId)
    -  ├─ finalizeActiveCardsForAccount(accountId)        ── 现有 AI Card 收尾
    -  └─ finalizeActiveApprovalCardsForAccount(accountId)  ← 新增
    -        for entry of approval-store.findByAccountId(accountId):
    -          updateCardVariables(entry.outTrackId, {
    -            statusFooter: "❌ 已取消(钉钉通道已断开)",
    -            buttonGroupVisible: false,
    -          }, token).catch(noop)
    -          approval-store.remove(entry.approvalId)
    + └─ transport.updateEntry({ entry, payload, phase: "expired" }) + → entry 由 core 从 activeEntries 取出回传(无需 channel 本地 store) + → PUT /v1.0/card/instances 把 status 改为 "⏰ 已过期",hasAction=false + → core 从 activeEntries 移除该 entry + +

    Channel stopClient(账号停用 / gateway 重启)—— v3 推迟到 v2

    +
    + v1 不实现 stop-time finalize(D13 v3 推迟)。原因:D18 删除本地 store 后 channel 端无法枚举 pending entries。 + v1 行为:停机时遗留 approval 卡片保留按钮态;用户点击 → §6.6"Channel 重启后用户点旧卡片"路径 → 卡片刷成"⏰ 已过期或已关闭"。 + v2 future:若 SDK 暴露 activeEntries 查询 API,或 channel 引入轻量 outTrackId Set(仅供 stop-time 清理用,非完整 entry store),再实现 finalize。 +

    6.7 createAndDeliver 失败 → markdown 兜底(error-recovery)

    deliverPending(target)
    @@ -857,11 +911,11 @@ 

    6.7 createAndDeliver 失败 → markdown 兜底(error-recovery)

    │ ├─ sendProactiveTextOrMarkdown(target, markdownText, ...) │ - ├─ 仍写一条降级 entry 到 store(无 outTrackId,type="fallback") - │ 这样后续 expired/resolved 事件还能 try-best 触达—— - │ 虽然 markdown 消息不能 update,但至少不会留下孤儿状态 - │ - └─ return fallback-entry
    + └─ return null(让 core 知道 deliverPending 未成功) + v3 不写 fallback entry——无本地 store; + 上游 core 看 null 即认为该 target 投递失败, + 不会再触发后续 updateEntry。 + 用户可用 markdown 消息里的 /approve 命令完成审批。
    兜底路径的设计取舍 @@ -1116,7 +1170,8 @@

    7.7 Mockup(expired · 已过期)

    -

    7.8 Mockup(plugin approval · pending · DM 私聊)

    +

    7.8 Mockup(plugin approval · pending · DM 私聊)—— v2 future

    +

    v1 不支持 approver-DM 投递(D4 v3 修订);本 mockup 保留作为 v2 future 设计参考。v1 中 plugin approval 卡片同样发到 origin 会话(不是 DM);DM 私聊场景仅在用户跟 agent 1:1 触发时出现(origin == DM[user])。

    OpenClaw · 私聊
    @@ -1201,22 +1256,16 @@

    8. 错误处理矩阵

    - - - - - - - - - - + + + + - - - - + + + +
    点击后卡片直接刷成过期态
    approval-store entry 缺失(点击时)channel 重启后内存清空同上"not-found"路径同上
    DM 投递权限不足机器人对某 approver 无法主动 DM(企业权限问题)onDeliveryError 日志 + 该 target 走 markdown 兜底(可能也失败);其它 target 不受影响未配置 DM 权限的 approver 收不到通知;运维需排查turnSourceChannel 不是 dingtalk(CLI 触发)v1 origin-only,无 DM 兜底availability.shouldHandle 返回 false → 上游 approval-runtime 不调用 DingTalk 投递路径;用户在钉钉里用 /approve 命令完成(§6.8)需用户主动复制 approval id;v2 future 自动 DM 给 approver
    同一 approver 既在 origin 群里又收 DM正常场景SDK dedupeKey 自动去重(§6.2 变体);同一 target 只投一次不会重复打扰multi-approver 在 origin 群里竞争点击正常场景(v1 仅 1 张卡片,多 approver 同群)第一个点击的成功 → 上游标记 resolved → 第二个点击触发 already-resolved → catch 分支 update 为已过期/已关闭第二个点击者看到卡片立即刷成终态,明确反馈
    @@ -1237,35 +1286,36 @@

    9.1 测试文件布局

    tests/unit/approval-callback-handler.test.tscardPrivateData 结构化 parse(含 button index 后缀)、权限校验、resolveApprovalOverGateway 调用、各错误分支;以 §1.2 的真机回调样本作 fixture~20 tests/unit/card-callback-service.test.ts(扩展既有)D16 改动:analyzeCardCallbackcardPrivateData 含 actionIds + params;既有 feedback / btn_stop 用例不受影响+6 tests/unit/inbound-handler-approve-intercept.test.ts§6.8 早期 intercept:群里带 @mention 前缀、私聊、各 decision alias、malformed 命令、resolveApprovalOverGateway 调用~12 - tests/unit/approval-store.test.tsregister/get/remove/findByAccountId/finalizeAll~8 - tests/unit/approval-cancel.test.tsstopClient 触发 finalize~4 tests/unit/approval-fallback-render.test.tsmarkdown 兜底文案、exec/plugin 分支~6 tests/unit/approval-capability.test.tsSDK 工厂参数装配正确、capability 单例~6 - tests/unit/approval-native-runtime.test.ts5 子 adapter 集成 mock~14 - tests/integration/approval-end-to-end.test.ts模拟 createApprovalRequest → 投递 → 点击 → resolve 回写 → 卡片刷新;含 dedupeKey、both-surface、self-approval~10 - tests/integration/approval-channel-stop.test.tsstopClient 触发的 finalize 链路~3 + tests/unit/approval-native-runtime.test.ts4 子 adapter(availability/presentation/transport/observe)集成 mock;含 v1 origin-only 路径~14 + tests/integration/approval-end-to-end.test.ts模拟 createApprovalRequest → 投递(仅 origin)→ 点击 → resolve 回写 → 卡片刷新;含 self-approval、multi-approver 竞争点击、非 approver 拒绝~10 -

    合计 ~115 case;按现有测试目录 1 case ≈ 18-25 行估算,测试代码约 2000-2800 行。

    +

    + v3 删除:approval-store.test.ts(无本地 store)、approval-cancel.test.ts(无 finalize-on-stop)、approval-channel-stop.test.ts(同上)。 + 合计 ~110 case(v2 是 ~115);测试代码约 1700-2400 行。 +

    9.2 Mock 策略

    • 所有 DingTalk HTTP(createAndDeliver / updateCardVariables / sendProactiveTextOrMarkdown):vi.mock("../../src/http-client")vi.mock("../../src/auth"),不打真实 API
    • 上游 SDK 工厂:vi.mock("openclaw/plugin-sdk") 注入 spy,验证传入参数完整
    • 上游 gateway resolve:vi.mock 抽象的 invoke,断言 method + payload
    • -
    • approval-store:测试用 fresh instance,避免跨 case 污染(现有 vitest clearMocks/restoreMocks/mockReset 全局开启)
    • +
    • 测试用 fresh instance / clearMocks 避免跨 case 污染(现有 vitest clearMocks/restoreMocks/mockReset 全局开启)
    -

    9.3 关键 integration 场景

    +

    9.3 关键 integration 场景(v3:仅 origin)

      -
    1. 群 + 双 approver DM:发起 → 3 卡片投出 → 群里点 approve → 3 卡片同步刷新
    2. -
    3. self-approval 在 DM:approver 自己 DM 发起 exec → 仅投 1 卡(dedupe)→ 自己点 → 通过
    4. -
    5. 非 approver 点击:投 1 卡 → 非 approver 用户点 → 收私聊拒绝 → 卡片不变
    6. -
    7. 过期:投卡 → mock 上游 expired event → 所有卡片刷成过期
    8. -
    9. stopClient:投 2 卡 → 调 stopClient → 2 卡都刷成"已取消"
    10. -
    11. createAndDeliver 失败 fallback:mock HTTP 错 → 走 markdown 兜底 → 验证 sendProactiveTextOrMarkdown 调用与文案
    12. -
    13. 重复点击:第一次成功 → mock 上游返 already-resolved → 卡片仍刷成终态,不再调 resolve
    14. +
    15. 群里 multi-approver 点击:发起 → 群里发 1 卡 → approverA 点 approve → 卡片刷成终态;approverB 第二次点击 → already-resolved → 卡片再刷一次(幂等)
    16. +
    17. self-approval 在 DM:approver 自己 DM 发起 exec → 投 1 卡到自己私聊 → 自己点 → 通过
    18. +
    19. 非 approver 点击:投 1 卡 → 非 approver 用户点 → 收私聊拒绝 → 卡片不变(按钮保留)
    20. +
    21. 过期:投卡 → mock 上游 expired event → core 调 updateEntry → 卡片刷成过期
    22. +
    23. createAndDeliver 失败 fallback:mock HTTP 错 → deliverPending 返 null → 同步调 sendProactiveTextOrMarkdown 发 markdown 兜底(含 /approve 命令模板)
    24. +
    25. 用户敲 /approve 命令:群里 @bot /approve abc once → inbound-handler 早期 intercept → resolveApprovalOverGateway → 不进 reply 派发(不触发 session lock)
    26. 未配置 approver:execApprovals 缺省 → isConfigured=false → shouldHandle=false → 上游不会调 DingTalk 路径
    27. +
    28. turnSourceChannel 非 dingtalk(CLI 触发):shouldHandle=false → 跳过 DingTalk 投递;用户在钉钉里手敲 /approve 完成审批
    29. +
    30. channel 重启后点旧按钮:mock resolveApprovalOverGateway 抛 not-found → catch 分支 update 卡片为"⏰ 已过期或已关闭"

    9.4 覆盖率目标

    @@ -1293,23 +1343,23 @@

    阶段 0 · 前置依赖(必须先满足)

    阶段 1 · 接口骨架与权限链路(PR-1)

      -
    • 新增 src/approval/ 目录与全部 9 个文件骨架
    • -
    • 实现 approval-config.tsapproval-store.tsapproval-target-resolver.tsapproval-capability.ts(不含 native runtime 完整实现)
    • -
    • src/channel.ts 挂上 approvalCapability,但 nativeRuntime 暂留 undefined,capability 仅生效 authorizeActorAction / resolveApproveCommandBehavior 等权限部分
    • -
    • src/inbound-handler.ts/approve 早期 intercept(§6.8,D2 落地)——这部分 PR-1 就要做,因为它就是 Feishu-同档体验的最后一公里
    • +
    • 新增 src/approval/ 目录与全部 8 个文件骨架(v3:删除 store/cancel,新增 command-intercept)
    • +
    • 实现 approval-config.tsapproval-target-resolver.ts(仅 origin)、approval-capability.ts(不含 nativeRuntime 完整实现)、approval-command-intercept.ts
    • +
    • src/channel.ts 挂上 approvalCapabilitynativeRuntime 暂留 undefined;capability 仅生效 authorizeActorAction / resolveApproveCommandBehavior 等权限部分
    • +
    • src/inbound-handler.ts/approve 早期 intercept(§6.8,D2 落地)—— PR-1 就要做,因为这是 Feishu-同档体验的最后一公里
    • package.json peerDependency bump 到 openclaw >= 2026.4.7
    • schema、配置文档(草稿)
    • -
    • 测试:approval-configapproval-storeapproval-target-resolverapproval-capabilityapprove-command-early-intercept
    • +
    • 测试:approval-configapproval-target-resolverapproval-capabilityapproval-command-intercept + inbound-handler-approve-intercept
    • 这阶段交付后:用户已经能在钉钉里手敲 /approve <id> <decision> 完成审批(权限校验生效 + 早期 intercept 绕过 session lock)。Feishu 同档能力。

    阶段 2 · 完整 native runtime(PR-2)

      -
    • 实现 approval-card-template.ts(用阶段 0 拿到的正式 templateId 写入常量)、approval-card-render.ts(按 §7.2 出 cardPrivateData 结构化 btns)、approval-fallback-render.tsapproval-callback-handler.ts(用 parseApprovalFromCardPrivateData)、approval-native-runtime.tsapproval-cancel.ts
    • +
    • 实现 approval-card-template.ts(const 默认 + env 覆盖,DINGTALK_APPROVAL_CARD_TEMPLATE_ID)、approval-card-render.ts(按 §7.2 出 cardPrivateData 结构化 btns)、approval-fallback-render.tsapproval-callback-handler.ts(用 parseApprovalFromCardPrivateData)、approval-native-runtime.ts(4 子 adapter;interactions 不实现)
    • 修改 src/card-callback-service.ts(D16):CardCallbackAnalysiscardPrivateData 字段,analyzeCardCallback 抽 params 并附到 analysis
    • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支(在 feedback / btn_stop 之前)
    • -
    • 在阶段 1 的 capability 里挂上 nativeRuntime(含 5 子 adapter;interactions 的 clearPendingActions / cancelDelivered 有实质实现)
    • -
    • 测试:剩余 6 个 unit 文件 + 2 个 integration 文件;新增 callback-handler 与 cardPrivateData 解析的 test(基于 §1.2 的真机回调样本作 fixture)
    • +
    • 在阶段 1 的 capability 里挂上 nativeRuntime(4 子 adapter;v1 不实现 interactions)
    • +
    • 测试:剩余 4 个 unit 文件 + 1 个 integration 文件;新增 callback-handler 与 cardPrivateData 解析的 test(基于 §1.2 的真机回调样本作 fixture)
    • 这阶段交付后:完整三按钮卡片 UX 在真机可用——templateId 已为正式发布版;可走真机回归(参照 skills/dingtalk-real-device-testing/SKILL.md
    @@ -1330,6 +1380,15 @@

    阶段 3 · 用户文档与回归收尾(PR-3)

  • 任何阶段回滚都不破坏前阶段已交付能力
  • + +

    v2 future · 后续可加但 v1 明确不做的事

    +
      +
    • approver-DM 投递(D4 升级):resolveApproverDmTargets 实现 + resolveNativeDeliveryMode 支持 config 配置 "channel" | "dm" | "both";前置需解决机器人主动 DM 的企业权限、staffId 可达性、失败兜底
    • +
    • finalize-on-stop(D13 升级):引入轻量 activeApprovalOutTrackIdSet(只存 outTrackId 不存完整 entry)或等待上游 SDK 暴露 activeEntries 查询 API;停机时把所有 pending 卡片 update 为"❌ 已取消(通道断开)"
    • +
    • interactions sub-adapter(仅在 DM 启用后有实际收益):实现 clearPendingActions / cancelDelivered,让多消息状态同步更精确
    • +
    • 主动 rebind on restart(D12 升级):channel 持久化 (approvalId → outTrackId) 映射,重启后从上游查 pending list 主动刷新卡片
    • +
    • 真机 plugin approval 验证:当前 §7.8 mockup 是 v1 不投递的 DM 场景;v2 启用 DM 后做 plugin approval 真机回归
    • +
    @@ -1338,11 +1397,14 @@

    11. 非目标与已知风险

    11.1 明确不在本 spec 范围

      +
    • approver-DM 投递(D4 v3):v1 仅 origin-only;v2 future 启用
    • +
    • 本地 approval store(D18 v3):依赖 core activeEntries
    • +
    • 停机 finalize(D13 v3 推迟):v1 不实现;v2 future 视上游 API 而定
    • 通用 action dispatcher(gap 文档 #01 sub-1, sub-2):peer 三家都没做,approval 按前缀走自己的分支已经够清晰
    • 抽象 interactive payload → AI Card 自动渲染(gap 文档 #01 sub-4):approval 是固定 3 按钮,不需要 runtime 任意组件渲染;任意组件需登录卡片平台核实清单后单独立项
    • -
    • 统一交互状态模型(gap 文档 #01 sub-6):本 spec 已为 approval 实现 pending/resolved/expired/canceled 状态机;将之普适到其它交互需要更多用例验证,留待 #12 message tool action surface
    • -
    • per-account 配置 surface 降级(D4 D 选项):当前 schema 已支持 execApprovals.target 字段,但默认值固定为 both,不主推切换
    • +
    • 统一交互状态模型(gap 文档 #01 sub-6):v1 仅为 approval 实现 pending/resolved/expired 状态机;将之普适到其它交互需要更多用例验证,留待 #12 message tool action surface
    • 主动 rebind on restart(D12 B 选项):留待 v2
    • +
    • interactions sub-adapter:v1 不实现,仅在 DM 启用后才有实际收益

    11.2 已知风险

    @@ -1375,9 +1437,14 @@

    11.2 已知风险

    "upstream store 是真相"原则;不重试 update;下次相关事件会覆盖 - 多账号场景下 approval-store 单例如何按账号隔离 - 潜在跨账号污染 - store 内 entry 强制带 accountId;所有查询 API 必须传 accountId;测试覆盖跨账号场景 + 多账号场景下 entry 隔离 + 潜在跨账号污染(core activeEntries 由 SDK 管,channel 不直接干预) + transport.deliverPending 返回的 entry 强制带 accountIdupdateEntry 调用方也按 accountId 路由;测试覆盖跨账号场景 + + + v1 origin-only 在 CLI 触发场景下无 DM 兜底 + 用户必须主动到钉钉里手敲 /approve <id> <decision> 才能批准 + v1:在 OpenClaw CLI 输出里清晰打印 approval id 与命令模板;v2:启用 DM 投递自动到达 approver @@ -1404,7 +1471,7 @@

    上游 OpenClaw 核心类型与运行时

    本设计的对照实现(必读)

      -
    • PR #489 · feat(approval): exec/plugin approval card with command session dispatch(@BrilliantWang 并行实施)—— 揭示 4 处关键事实约束:session lock 死锁(§6.8)、cardPrivateData 结构化 payload(§1.2)、peerDependency bump(D17)、PR #480 依赖(阶段 0);本设计对其取舍:保留 approver schema、保留 finalizeActiveApprovalCardsForAccount、保留 markdown 兜底、保留 both surface
    • +
    • PR #489 · feat(approval): exec/plugin approval card with command session dispatch(@BrilliantWang 并行实施,OPEN 未合)—— v3 综合的来源。采纳 PR #489 工程骨架:D2 命令早期 intercept、D15 cardPrivateData payload、D17 peerDep + PR #480 依赖、D18 无本地 store、D4 v1 origin-only。保留本设计的边界:D7 approver schema、§6.6 非 approver 拒绝、§9 测试矩阵、§10 分阶段。放弃/推迟:D13 finalize-on-stop(v2)、approver-DM 投递(v2)、纯常量无 env 的模板(D9 保留 env)。注意 PR #489 基于 4 月旧 main 已 CONFLICTING,不可直接合并——本设计需基于当前 main 重新实现

    对标 channel 实现

    From 0a0e3ceb7fd0240fa3d9c5ae9285337a4ee6014c Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Mon, 18 May 2026 22:40:02 +0800 Subject: [PATCH 04/44] =?UTF-8?q?docs(spec):=20v3.1=20=E4=BF=AE=E8=AE=A2?= =?UTF-8?q?=20=E2=80=94=20=E7=AC=AC=E4=BA=8C=E8=BD=AE=20review=20=E5=8F=8D?= =?UTF-8?q?=E9=A6=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 8 处修订(按 review 顺序): 主要问题: 1. §6.8 /approve early intercept 漏 approver 校验(resolveApprovalOverGateway 只连 gateway 不校验 senderId)。修复:intercept 内必须自调 capability.authorizeActorAction,不通过则给发起者私聊"无权批准"。否则 任何能发 /approve 的人都能批准,与按钮路径形成安全漏洞。 2. §6.3 step 4 不再写死 approvalKind="exec"。改为按 approvalId.startsWith("plugin:") 派发;§11.2 加 unprefixed plugin id 边界风险条目。 3. 顶部 page-subtitle / 总览段 / 核心原则 callout 同步 v3 状态——删除 "origin+DM 两端"、"5 子 adapter 全部实现"、"无环境变量配置"等与 D4/D9/ D18 矛盾的旧表述。 4. §5.1 shouldHandle 收紧为四连判:isConfigured && turnSourceChannel === "dingtalk" && turnSourceTo 可解析 && hasApprovers。原"为空也处理"与 v1 origin-only 直接冲突。 5. §5.3 transport 类型对齐:prepareTarget 用 plannedTarget.target.to(不 是裸 plannedTarget.to);deliverPending 明确"transport 内部自闭环 catch + fallback + return null",不在 catch 内调 observe.onDeliveryError(那 是 runtime 捕获 throw 后才触发的钩子)。 6. §10 PR-1 措辞修正——原"PR-1 后用户已能在钉钉手敲 /approve 完成审批" 过度承诺;PR-1 无 nativeRuntime/render,approval id 可见性仍依赖外部 界面(WebUI/CLI),完整 UX 在 PR-2。 小修正: - §6.7 删 store.register(entry) 残留(D18 冲突) - §6.3 ASCII 把 actionId 形式由旧 "/approve abc allow-once" 改为真机实测 的 cardPrivateData {actionIds:["approval0"], params:{...}} decision payload 结构 - §6.8 alias 显式列范围(8 个别名 + 两种顺序),不再宣称"完整支持" - 所有 catch 文案 "⏰ 已过期或已关闭" → "ℹ️ 已处理或已过期"(更准确, 覆盖 already-resolved / not-found / expired 三种情况);§6.3 step 5 catch 内明确 return,避免后续 step 6 用 resolved 终态覆盖 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 182 +++++++++++++----- 1 file changed, 134 insertions(+), 48 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index eaca6458..212d2da7 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -12,7 +12,7 @@

    Gap #01 · DingTalk Native Approval 设计方案

    - 为 DingTalk Channel 接入 OpenClaw 的 原生审批能力(exec approval + plugin approval),全量实现 ChannelApprovalCapability 与 5 个子 native runtime adapter。审批以 AI 卡片三按钮形式投放,按钮 payload 用 DingTalk sendCardRequest 的 cardPrivateData.params 结构化字段编码 decision/approvalId(v2 修订;v1 曾设想用 /approve 命令字面量,被真机回调证伪)。按钮点击与 /approve 文本命令两条路径都收敛到上游公开 API resolveApprovalOverGateway。 + 为 DingTalk Channel 接入 OpenClaw 的 原生审批能力(exec approval + plugin approval)。v1 实现 ChannelApprovalCapability 与 native runtime 4 个 sub-adapter(availability/presentation/transport/observe;interactions 推迟到 v2)。审批 v1 仅投递到 origin 会话(agent 触发的钉钉群/私聊),approver DM 双投递推迟到 v2 且 config-gated。按钮 payload 用 DingTalk sendCardRequest 的 cardPrivateData.params 结构化字段编码 decision/approvalId;按钮点击与 /approve 文本命令两条路径都收敛到上游公开 API resolveApprovalOverGateway

    P0 · 核心缺口 @@ -27,17 +27,18 @@

    Gap #01 · DingTalk Native Approval 设计方案

    1. 总览

    - DingTalk Channel 当前 src/channel.ts:22-127 没有声明 approvalCapability,所以 OpenClaw 的 exec / plugin 审批流程在钉钉用户那里完全走不通——agent 想跑命令时只能在 WebUI / 终端 UI 里批,从钉钉群里发起的 agent 任务等于 dead-end。本设计填补这一空缺,让钉钉群(origin)与 approver 私聊(DM)两端都能直接收到三按钮卡片,点击即可完成审批。 + DingTalk Channel 当前 src/channel.ts:22-127 没有声明 approvalCapability,所以 OpenClaw 的 exec / plugin 审批流程在钉钉用户那里完全走不通——agent 想跑命令时只能在 WebUI / 终端 UI 里批,从钉钉群里发起的 agent 任务等于 dead-end。本设计填补这一空缺:v1 让 agent 触发审批的钉钉会话(群或私聊)直接收到三按钮卡片,approver 点击即可完成;DM 双投递推迟到 v2。

    本设计的核心原则
      -
    • 不做一半:exec + plugin 两类 approval 一次到位,5 个 native runtime sub-adapter 全部实现
    • -
    • 与 peer 对齐:所有可对照决策(surface / approver schema / ID 不短化 / slash 命令复用上游)都跟 Discord/Telegram/Slack 一致,不为 DingTalk 发明独有约定
    • +
    • exec + plugin 一次到位:两类 approval 都通过同一套 native runtime 处理(按 approvalId 前缀派发)
    • +
    • v1 范围有边界:origin-only 投递、4 子 adapter(不含 interactions)、无本地 store;DM 投递 / finalize-on-stop / interactions 推迟到 v2。本边界由 D4 / D13 / D18 锚定,下文所有章节遵循 v1 边界
    • +
    • 与 peer 对齐:approver schema / ID 不短化 / slash 命令文本格式都跟 Discord/Telegram/Slack 一致;origin-only 边界与 PR #489 v1 一致
    • 钉钉特性最大化:审批永远用 DingTalk 互动卡片(独立模板)渲染,与 messageType: card | markdown 配置无关
    • -
    • 上游约定为权威:按钮点击解码后回写到 exec.approval.resolve / plugin.approval.resolve gateway method,与用户手敲 /approve <id> <decision> 走同一条解析链
    • -
    • 用户零部署摩擦:审批卡片模板 ID 内置常量,无环境变量配置;只需把 approver staffId 列表写进 channels.dingtalk.execApprovals.approvers 即可启用
    • +
    • 上游约定为权威:按钮点击解码后通过 SDK 公开 API resolveApprovalOverGateway 回写;用户手敲 /approve 因 session lock 死锁需早期 intercept,但同样收敛到 resolveApprovalOverGateway
    • +
    • 用户低部署摩擦:审批卡片模板 ID 内置默认常量,DINGTALK_APPROVAL_CARD_TEMPLATE_ID env 可覆盖(与现有 AI Card 同模式);只需把 approver staffId 列表写进 channels.dingtalk.execApprovals.approvers 即可启用
    @@ -133,7 +134,7 @@

    2. 已确认的决策清单

    D2 Slash 命令 - 命令字面量与上游一致(/approve <id> <decision>),但 channel 端必须在 inbound-handler 入口早期 intercept,直接调 resolveApprovalOverGateway——不走正常 inbound dispatch 路径
    原因(v1 实施踩坑确认):Plugin Approval 的 waitDecision 阻塞在 dispatchReply 内并持有 DingTalk 自己的 session lock,若 /approve 走 normal pipeline 会 session lock 死锁,120s 超时失败。
    decision 别名按上游 commands-approve.ts 完整支持(once/always/allow/deny/reject/block) + 命令字面量与上游一致(/approve <id> <decision>),但 channel 端必须在 inbound-handler 入口早期 intercept,直接调 resolveApprovalOverGateway——不走正常 inbound dispatch 路径
    原因(v1 实施踩坑确认):Plugin Approval 的 waitDecision 阻塞在 dispatchReply 内并持有 DingTalk 自己的 session lock,若 /approve 走 normal pipeline 会 session lock 死锁,120s 超时失败。
    权限校验必须由 channel 自行执行capability.authorizeActorAction),resolveApprovalOverGateway 只连 gateway 不校验 approver(Issue 1 修订)。
    decision alias v3.1 显式支持范围:allow-once|once|allow → allow-once;allow-always|always → allow-always;deny|reject|block → deny;同时接两种顺序 /approve <id> <decision>/approve <decision> <id>。若上游引入新 alias 或新顺序需同步更新(§11.2 风险表登记) 对齐 PR #489 揭示的运行时约束 @@ -254,6 +255,15 @@

    2. 已确认的决策清单

  • v1(2026-05-18 初版):基于上游 SDK + peer 三家对标做的纸面设计,D2 写"纯复用 /approve dispatcher",按钮 payload 用 Telegram 风格字符串字面量
  • v2(2026-05-18 修订):对比 PR #489(@BrilliantWang 并行实施)+ 真机回调 payload 实证,新增 D15/D16/D17,D2 重写
  • v3(2026-05-18 综合):明确取舍——采用 PR #489 工程骨架(D18/D19)保留本设计的权限/测试/分阶段边界。v1 范围缩到 origin-only,DM 双投递降级为 v2 future-work,本地 approval-store 删除(依赖 core activeEntries),模板 ID 保留 env 覆盖能力。详见下方 D18/D19 与各章节修订
  • +
  • v3.1(2026-05-18 review 反馈):第二轮 spec 审计修订 8 处—— + (1) §6.8 /approve early intercept 漏 approver 校验(resolveApprovalOverGateway 不校验 senderId),必须自带 authorizeActorAction; + (2) §6.3 step 4 不再写死 approvalKind: "exec",改为按 approvalId.startsWith("plugin:") 派发; + (3) 顶部 page-subtitle / 总览 / 核心原则 同步 v3 状态(去除 "DM 两端"、"5 子 adapter"、"无 env"等过时表述); + (4) §5.1 shouldHandle 收紧为 turnSourceChannel === "dingtalk" && turnSourceTo 可解析 && hasApprovers; + (5) §5.3 prepareTarget 取值修正为 plannedTarget.target.to;deliverPending 明确 transport 内部自闭环(不在 catch 内调 observe.onDeliveryError); + (6) §10 PR-1 措辞改"resolve 通道生效,approval id 可见性依赖外部界面,完整 UX 在 PR-2"; + (7) §6.7 删除 store.register 残留; + (8) §6.3 / §6.6 / §8 / §9 等所有 already-resolved 文案改"ℹ️ 已处理或已过期"并明确 catch 后 return 避免覆盖;§6.8 alias 范围显式列清;§11.2 风险表新增 kind 派发边界条目
  • userId 字段身份核实:v2 已确认 callback userId === staffId(不是 unionId),所以 §4.2 approver schema(staffId 名单)保持不变
  • @@ -527,7 +537,12 @@

    5.1 availability

    shouldHandle({ cfg, accountId, request }) - 三连判:(1) isConfigured 为 true;(2) request 的 turnSourceChannel 为空 OR 为 "dingtalk";(3) approver 名单非空 + 四连判(v1 origin-only 严格判定): +
    (1) isConfigured 为 true; +
    (2) request.turnSourceChannel === "dingtalk"(非 dingtalk 触发的 approval 跳过;CLI 触发由用户在钉钉里手敲 /approve 兜底); +
    (3) request.turnSourceTo 非空且可被 normalizeApprovalTargetTo 解析(保证有 origin target); +
    (4) listExecApprovers().length > 0(有 approver 名单才有 channel-side 权限校验意义)。 +
    v2 改为 requireMatchingTurnSourceChannel: false 后才放开 (2),让 CLI 触发也能走 DM 兜底 @@ -568,13 +583,15 @@

    5.3 transport

    prepareTarget({ plannedTarget, request, view, pendingPayload }) - 对 plannedTarget.tonormalizeApprovalTargetTo(§6.1)确保带 user:/group: 前缀;返回 { target: { to: normalizedTo, isGroup: normalizedTo.startsWith("group:"), accountId }, dedupeKey: \`dingtalk:${accountId}:${normalizedTo}\` }。DingTalk 无 thread 概念,threadId 永远 null。 + 从 plannedTarget.target.to(注意:参数是嵌套的 plannedTarget.target.{to, threadId},不是裸 plannedTarget.to)取出原始 target string,调 normalizeApprovalTargetTo(§6.1)确保带 user:/group: 前缀。 +
    返回 { target: { to: normalizedTo, isGroup: normalizedTo.startsWith("group:"), accountId }, dedupeKey: \`dingtalk:${accountId}:${normalizedTo}\` }。DingTalk 无 thread 概念,threadId 永远 null。 deliverPending({ cfg, accountId, preparedTarget, request, pendingPayload }) 根据 preparedTarget.target.isGroup 分支调 card-service.createAndDeliver
    • group → { openConversationId: stripPrefix(target.to), ... }
    • user → { userIds: [stripPrefix(target.to)], ... }
    统一参数:outTrackId = \`approval_${request.id}_${hash(target.to)}\`templateId = resolveApprovalTemplateId()(const 或 env 覆盖),cardParamMap 来自 buildPendingPayloadcallbackType: "STREAM"
    成功:直接返回 entry { approvalId: request.id, outTrackId, conversationId: stripPrefix(target.to), isGroup, accountId }——不写本地 store(D18),core 会自动把这个 entry 缓存到 activeEntries 并在后续 updateEntry 调用时带回。 -
    失败:调 observe.onDeliveryError + approval-fallback-render 发 markdown 兜底;返回 null 让 core 知道未成功投递 +
    失败(observe 语义说明):上游 runtime 是捕获 deliverPending 抛错后才调 observe.onDeliveryError。本设计选择"transport 内部自闭环":catch 错误后内部直接 log + 调 approval-fallback-render 发 markdown 兜底 + return null再 throw(避免 runtime 重复 log,也避免 observe 收到错误后误以为 fallback 未发)。 +
    不要在 catch 内再显式调 observe.onDeliveryError——observe 是 runtime 触发的钩子,自己内部调它会破坏 SDK 契约 updateEntry({ cfg, accountId, entry, payload, phase }) @@ -706,10 +723,14 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    用户在卡片上点"允许一次"
     
     t=0   DingTalk Stream 平台推送 TOPIC_CARD 回调
    -        payload.cardPrivateData.actionIds = ["/approve abc123 allow-once"]
    -        payload.userId                    = "staffA"
    +        payload.content/value (内嵌 JSON, 真机实测 §1.2):
    +          { cardPrivateData: {
    +              actionIds: ["approval0"],          ← 平台自动追加 button index
    +              params: { t:"approval", d:"allow-once", id:"abc123" }   ← decision payload
    +            } }
    +        payload.userId                    = "staffA"  (staffId)
             payload.spaceId / spaceType       = "cid_xxx" / "group"
    -        payload.outTrackId                = "approval-abc123-group:cid_xxx"
    +        payload.outTrackId                = "approval_abc123_"
     
     t=1   src/gateway/channel-gateway.ts:330 listener 触发
             ├─ messageId = res.headers.messageId
    @@ -735,18 +756,23 @@ 

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ 2. (v2 不再走 parseExecApprovalCommandText——按钮路径走 cardPrivateData 解析; │ 命令路径走 §6.8 早期 intercept 的 regex;两条路径最终都调 resolveApprovalOverGateway) │ - │ 3. (v3 删除"查 store 找 entry"步骤) - │ channel 端不维护本地 store(D18);entry 数据由 core - │ activeEntries 经 transport.updateEntry 回灌。本步直接进权限校验。 - │ approval 是否已过期/被 resolve 等状态判定由上游 - │ resolveApprovalOverGateway 内部处理(失败时抛 already-resolved / - │ not-found 等具名错误)。 + │ 3. 推导 approval kind(按 approvalId 前缀派发) + │ approvalKind = parsed.approvalId.startsWith("plugin:") + │ ? "plugin" + │ : "exec" + │ // 上游 approval id 当前约定:plugin approval 带 "plugin:" 前缀; + │ // exec approval 不带前缀。若上游未来引入 unprefixed plugin id, + │ // 这里需补 allowPluginFallback 配置(见 §11.2 风险表) │ - │ 4. 权限校验(approval kind 暂用 "exec"——上游接口当前不区分) + │ (v3 不再"查本地 store 找 entry"——D18 删除了 store; + │ approval 是否已过期/已被 resolve 等状态判定由上游 + │ resolveApprovalOverGateway 在 step 5 内部处理,失败时抛具名错误) + │ + │ 4. 权限校验(v1:exec / plugin 共用同一份 approvers 名单) │ authorized = capability.authorizeActorAction({ │ cfg, accountId, senderId: analysis.userId, │ action: "approve", - │ approvalKind: "exec" }) // v1:exec/plugin 共用同一份 approvers + │ approvalKind }) // ← 按前缀派发,不写死 │ if (!authorized.authorized) { │ sendProactiveTextOrMarkdown( │ config, @@ -768,27 +794,33 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ clientDisplayName: "DingTalk", │ }) │ } catch (err) { - │ // 已过期 / 已被批 / 不存在 → 上游会抛 already-resolved / not-found - │ // 等具名错误。catch 后兜底 update 卡片为 "⏰ 已过期或已关闭", - │ // 让用户看到明确反馈而不是按钮"点了无反应" + │ // 上游抛 already-resolved(已被批)/ not-found(不存在或已清理) + │ // / expired 等具名错误。catch 后用中性文案"已处理或已过期"—— + │ // 不写死"已过期"以免误导(也可能是 already-resolved) │ await updateCardVariables(payload.outTrackId, { - │ status: "⏰ 已过期或已关闭", hasAction: "false", btns: "[]", + │ status: "ℹ️ 已处理或已过期", hasAction: "false", btns: "[]", │ }, token).catch(() => {}) + │ return { handled: true, reason: "resolve-failed" } + │ // ↑ 关键:必须 return,不再走 step 6 的 resolved 终态 update, + │ // 否则会用 "✅ 已批准" 覆盖刚刚的 "已处理或已过期",产生 + │ // 视觉矛盾 │ } │ // SDK 内部根据 approval kind 自动 dispatch 到 │ // exec.approval.resolve / plugin.approval.resolve - │ // channel 端不需要关心 kind 区分 + │ // (channel 端的 step 3 kind 推导只用于本地权限校验) │ - │ (上游 resolve 触发 approval-handler-runtime 调 + │ (上游 resolve 成功后会异步触发 approval-handler-runtime 调 │ presentation.buildResolvedResult → │ transport.updateEntry({ phase: "resolved" }) → - │ 所有 3 张卡片都被更新为终态,包含本机这张) + │ v1 origin-only 即只刷本机这张卡片,与下面 step 6 内容一致, + │ 幂等覆盖) │ - │ 6. 兜底立即 update 本机这张(防止上游事件回环延迟) + │ 6. 仅 step 5 成功(无 catch return)时执行:本机立即 update │ updateCardVariables(payload.outTrackId, │ buildResolvedCardParamMap(parsed.decision, analysis.userId), │ token) - │ // 即使上游 updateEntry 还没触发,用户也能立刻看到终态卡片 + │ // 即使上游 updateEntry 异步事件还没回来,用户也能立刻看到 + │ // 终态卡片;step 5 失败路径已 return,本步不会重复执行 │ │ 7. 日志 + return { handled: true, reason: "resolved" } │ @@ -851,7 +883,7 @@

    用户重复点击(按钮看起来还在但已 resolved)

    1. callback-handler 调 resolveApprovalOverGateway
    2. 上游返回 already-resolved 类的具名错误(catch 到)
    3. -
    4. catch 分支调 updateCardVariables 把卡片刷成"⏰ 已过期或已关闭"(即使 UI 还没刷过来也立即修正)
    5. +
    6. catch 分支调 updateCardVariables 把卡片刷成"ℹ️ 已处理或已过期"(即使 UI 还没刷过来也立即修正)
    7. 对用户:第二次点击看起来等同于第一次"已生效",无打扰提示
    @@ -868,7 +900,7 @@

    Channel 重启后用户点旧卡片(v3:no-store 模式下的行为)channel 端无本地 store(D18),但 entry 信息从平台回调的 outTrackId 仍能拿到
  • callback-handler 解析 cardPrivateData 拿到 approvalId + decision → 直接调 resolveApprovalOverGateway
  • 上游 core 在重启后 activeEntries 也清空了,resolveApprovalOverGateway 返回 not-found
  • -
  • callback-handler 进 catch 分支 → updateCardVariables(outTrackId, { status: "⏰ 已过期或已关闭", hasAction: "false", btns: "[]" })
  • +
  • callback-handler 进 catch 分支 → updateCardVariables(outTrackId, { status: "ℹ️ 已处理或已过期", hasAction: "false", btns: "[]" })
  • 用户体感:按钮点了一下卡片刷成"已过期",明确反馈——对长时间下线降级,可接受
  • @@ -885,7 +917,7 @@

    上游过期事件触达

    Channel stopClient(账号停用 / gateway 重启)—— v3 推迟到 v2

    v1 不实现 stop-time finalize(D13 v3 推迟)。原因:D18 删除本地 store 后 channel 端无法枚举 pending entries。 - v1 行为:停机时遗留 approval 卡片保留按钮态;用户点击 → §6.6"Channel 重启后用户点旧卡片"路径 → 卡片刷成"⏰ 已过期或已关闭"。 + v1 行为:停机时遗留 approval 卡片保留按钮态;用户点击 → §6.6"Channel 重启后用户点旧卡片"路径 → 卡片刷成"ℹ️ 已处理或已过期"。 v2 future:若 SDK 暴露 activeEntries 查询 API,或 channel 引入轻量 outTrackId Set(仅供 stop-time 清理用,非完整 entry store),再实现 finalize。
    @@ -893,11 +925,13 @@

    6.7 createAndDeliver 失败 → markdown 兜底(error-recovery)

    deliverPending(target)
       ├─ card-service.createAndDeliver(...)
       │
    -  ├─ 成功 → store.register(entry) → return entry
    +  ├─ 成功 → return entry(D18:不写本地 store;core 自动缓存到 activeEntries)
       │
       └─ 失败(网络 / API 4xx/5xx / 模板未发布)
            │
    -       ├─ observe.onDeliveryError({ error, plannedTarget, request })
    +       ├─ 内部 log.warn (注意:不调 observe.onDeliveryError——那是 runtime
    +       │    捕获 throw 后才会触发的钩子;本设计选择 transport 自闭环 catch
    +       │    + fallback + return null,runtime 不会再走 observe)
            │
            ├─ approval-fallback-render.buildMarkdownFallback(request)
            │  →  内容包含:
    @@ -947,22 +981,62 @@ 

    解决方案:在 handleDingTalkMessage 入口最早处直接 : extractedContent.text.trim(); if (/^\/approve\b/i.test(textForApproveCheck)) { - const m = textForApproveCheck.match( - /^\/approve\s+(\S+)\s+(allow-once|allow-always|allow|once|always|deny|reject|block)\s*$/i, - ); + // 支持两种顺序:/approve <id> <decision> 与 /approve <decision> <id> + // alias 表与上游 commands-approve.ts 对齐: + // allow-once / once / allow → "allow-once" + // allow-always / always → "allow-always" + // deny / reject / block → "deny" + const decisionPattern = "(?:allow-once|allow-always|allow|once|always|deny|reject|block)"; + const m = + textForApproveCheck.match( + new RegExp(`^/approve\\s+(\\S+)\\s+(${decisionPattern})\\s*$`, "i"), + ) ?? + textForApproveCheck.match( + new RegExp(`^/approve\\s+(${decisionPattern})\\s+(\\S+)\\s*$`, "i"), + ); if (!m) { log?.warn?.("[DingTalk] /approve malformed — usage: /approve <id> <decision>"); return; } - const approvalId = m[1]; - // 与上游 commands-approve.ts 的 alias 表对齐——保持跨 channel 体验一致 - const rawDecision = m[2].toLowerCase(); + // 判定哪个 group 是 decision(顺序两种之一) + const [, g1, g2] = m; + const isDecisionFirst = new RegExp(`^${decisionPattern}$`, "i").test(g1); + const approvalId = isDecisionFirst ? g2 : g1; + const rawDecision = (isDecisionFirst ? g1 : g2).toLowerCase(); const decision: "allow-once" | "allow-always" | "deny" = rawDecision === "allow" || rawDecision === "once" ? "allow-once" : rawDecision === "always" ? "allow-always" : rawDecision === "reject" || rawDecision === "block" ? "deny" : (rawDecision as "allow-once" | "allow-always" | "deny"); + // === Issue 1 修订:channel 端必须自己做 approver 权限校验 === + // resolveApprovalOverGateway 只负责连接 gateway 与回写, + // 不会按 senderId 校验 approver 名单。若不在这里拦截, + // 任何能发 /approve 消息的人都可批准。 + const approvalKind = approvalId.startsWith("plugin:") ? "plugin" : "exec"; + const authResult = dingtalkApprovalCapability.authorizeActorAction?.({ + cfg, + accountId: account.accountId, + senderId, // 入站 senderStaffId + action: "approve", + approvalKind, + }); + if (authResult && !authResult.authorized) { + log?.info?.( + `[DingTalk] /approve denied — sender=${senderId} not in approvers ` + + `(approvalId=${approvalId})`, + ); + // 回一条文本,让发起者明确知道被拒(而不是静默) + try { + await dispatchReplyText( + `⛔ 你不在 approver 名单,无权批准此请求(${approvalId})`, + ); + } catch { + // 回复失败不影响主流程 + } + return; + } + log?.info?.(`[DingTalk] /approve intercept id=${approvalId} decision=${decision}`); try { const { resolveApprovalOverGateway } = await import( @@ -981,6 +1055,13 @@

    解决方案:在 handleDingTalkMessage 入口最早处直接 return; // ← 关键:return,不再进 reply 派发 }

    +
    + Issue 1 修订:早期 intercept 必须自带 approver 权限校验
    + v3.0 草稿里 intercept 直接调 resolveApprovalOverGateway 漏掉了 approver 校验——上游 resolveApprovalOverGateway 只是 gateway adapter(连接 gateway 并调用 exec.approval.resolve / plugin.approval.resolve),不会按 senderId 做 approver 名单校验。 + 所以 channel 端必须自己先调 capability.authorizeActorAction,否则任何能发 /approve 的用户都能批准(与 §6.6"非 approver 点击"按钮路径形成不对称漏洞)。 + 上述代码已修正——按钮回调 (§6.3 step 4) 与命令早期 intercept (§6.8) 两条路径都走同一 authorizeActorAction 检查。 +
    +

    为什么 D2 不能"纯复用上游 /approve dispatcher"

    @@ -1252,7 +1333,7 @@

    8. 错误处理矩阵

    - + @@ -1315,7 +1396,7 @@

    9.3 关键 integration 场景(v3:仅 origin)

  • 用户敲 /approve 命令:群里 @bot /approve abc once → inbound-handler 早期 intercept → resolveApprovalOverGateway → 不进 reply 派发(不触发 session lock)
  • 未配置 approver:execApprovals 缺省 → isConfigured=false → shouldHandle=false → 上游不会调 DingTalk 路径
  • turnSourceChannel 非 dingtalk(CLI 触发):shouldHandle=false → 跳过 DingTalk 投递;用户在钉钉里手敲 /approve 完成审批
  • -
  • channel 重启后点旧按钮:mock resolveApprovalOverGateway 抛 not-found → catch 分支 update 卡片为"⏰ 已过期或已关闭"
  • +
  • channel 重启后点旧按钮:mock resolveApprovalOverGateway 抛 not-found → catch 分支 update 卡片为"ℹ️ 已处理或已过期"
  • 9.4 覆盖率目标

    @@ -1350,7 +1431,7 @@

    阶段 1 · 接口骨架与权限链路(PR-1)

  • package.json peerDependency bump 到 openclaw >= 2026.4.7
  • schema、配置文档(草稿)
  • 测试:approval-configapproval-target-resolverapproval-capabilityapproval-command-intercept + inbound-handler-approve-intercept
  • -
  • 这阶段交付后:用户已经能在钉钉里手敲 /approve <id> <decision> 完成审批(权限校验生效 + 早期 intercept 绕过 session lock)。Feishu 同档能力。
  • +
  • 这阶段交付后:DingTalk 端具备 /approve 命令的 resolve 通道——权限校验(approver 名单)+ 早期 intercept 绕过 session lock 都生效。但 approval id 的可见性仍依赖外部界面(用户需从 OpenClaw WebUI / CLI / 日志拿到 id 后才能在钉钉里手敲)。完整钉钉内"看见 → 点按钮"的端到端用户体验在 PR-2 才落地。Feishu approval-auth 同档。
  • 阶段 2 · 完整 native runtime(PR-2)

    @@ -1374,7 +1455,7 @@

    阶段 3 · 用户文档与回归收尾(PR-3)

    分阶段的好处
      -
    • PR-1 可独立 merge,立刻让"在钉钉里用 /approve 命令"成为可能(Feishu-同档 baseline);权限校验生效,安全已闭环
    • +
    • PR-1 可独立 merge:DingTalk 端 resolve 通道与 approver 权限校验生效;用户从外部界面拿到 approval id 后可在钉钉里手敲 /approve 完成(与 Feishu approval-auth 同档;完整 UX 在 PR-2)
    • PR-2 引入大量代码但全部聚焦 native runtime;阶段 0 模板已发布,可直接进真机回归
    • PR-3 只动文档与回归记录,code-review 量极小,便于聚焦写作质量
    • 任何阶段回滚都不破坏前阶段已交付能力
    • @@ -1417,9 +1498,14 @@

      11.2 已知风险

    - - - + + + + + + + + From c071ea37184cc1cc4b9a2b85d51c9d08c9ca263e Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Mon, 18 May 2026 22:50:24 +0800 Subject: [PATCH 05/44] =?UTF-8?q?docs(spec):=20v3.2=20=E4=BF=AE=E8=AE=A2?= =?UTF-8?q?=20=E2=80=94=20=E7=AC=AC=E4=B8=89=E8=BD=AE=20review=20+=20?= =?UTF-8?q?=E5=BC=95=E5=85=A5=E7=BB=9F=E4=B8=80=20resolver=20=E6=8A=BD?= =?UTF-8?q?=E8=B1=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 6 处事实约束 + 1 处架构重构: 事实约束: 1. 5→4 sub-adapter 残留全部清理(页眉决策表 D1、§3.1 拓扑图、§3.2 模块表) 2. §8 错误矩阵 createAndDeliver 失败行改"transport 自闭环 log + fallback", 与 §5.3 deliverPending 的"内部 catch + return null"语义一致;不再宣称 调 observe.onDeliveryError(那是 runtime 捕获 throw 才触发的钩子) 3. §6.8 /approve unauthorized 分支明确用 sendProactiveTextOrMarkdown (channel 直接路径),禁用 dispatchReplyText——后者会经过 SDK reply dispatcher 重新触发 session lock,正是 early intercept 要避免的坑 5. PR #480 状态从"必须先合并"改为"已合并 (MERGED) 需复用契约" 6. D17 + 阶段 0 SDK 基线扩成"三件套":peerDep + pnpm-lock.yaml + tsconfig path 优先级;说明 node_modules/openclaw 当前仍 2026.3.28 type-check 会 卡住的具体原因 架构重构(统一 resolver 抽象): 4. 新增 D20/D21 两条决策,引入两个新模块: - approval-resolver.ts:单点收敛 kind 推导 / authorize / allowPluginFallback / resolveMethod / resolveApprovalOverGateway 调用 / 错误分类 - approval-command-parser.ts:纯解析 /approve 两种顺序 + 8 alias,含上游 commands-approve.ts alias 对照测试 - approval-callback-handler.ts 与 approval-command-intercept.ts 都收敛到 调 resolver.resolveApproval;权限策略 / plugin fallback / 错误文案不分叉 - kind 推导按上游 Slack/Telegram allowPluginFallback 经验: plugin: 前缀 → resolveMethod plugin; 无前缀 + 两边授权 → resolveMethod exec + allowPluginFallback true; 无前缀 + 仅 plugin → resolveMethod plugin; 无前缀 + 仅 exec → resolveMethod exec; 都未授权 → unauthorized 各章节修订面:决策表新增 D20/D21;§3.1 拓扑图加 resolver/parser;§3.2 模块表 加两行 + 重写 callback-handler/command-intercept;§6.3 step 3-7 全部收敛到 resolver;§6.8 inbound 内联代码大幅瘦身(薄壳调 intercept→parser→resolver) + 新增 intercept 内部伪代码;§9.1 测试加 resolver/parser 两个文件 + intercept 独立 test,case 数从 ~110 升到 ~135;§10 阶段 1 强调 resolver 在 PR-1 就建好。 模块数:8 → 9;业务代码 ~830 行 → ~970 行(多 ~140 行换来跨路径行为一致 + 单点修改 + 测试聚焦)。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 368 ++++++++++-------- 1 file changed, 198 insertions(+), 170 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index 212d2da7..1f7e602b 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -128,7 +128,7 @@

    2. 已确认的决策清单

    - + @@ -229,9 +229,14 @@

    2. 已确认的决策清单

    - - - + + + @@ -246,6 +251,25 @@

    2. 已确认的决策清单

    + + + + + + + + + + + +
    方案结果
    上游 resolve 返回 not-found approvalId 在上游已过期/清除(如 channel 长时间下线)callback-handler 更新卡片为"⏰ 已过期或已关闭"callback-handler 更新卡片为"ℹ️ 已处理或已过期" 点击后卡片直接刷成过期态
    阶段 3 在发布后做真机回归;fallback 路径保证不出现 approval 静默消失
    上游 commands-approve.ts 扩展 decision alias钉钉 channel 端 §6.8 的 regex 写死支持 once/always/allow/deny/reject/block;若上游新增 alias,channel 端用户敲新 alias 会被 channel 拒为 "malformed"在 §6.8 regex 旁加显式注释引用上游文件路径;CI 加一个测试断言上游 alias 集合不超出 channel 端支持范围(参考 openclaw/src/auto-reply/reply/commands-approve.ts alias 列表)上游 commands-approve.ts 扩展 decision alias / 命令顺序钉钉 channel 端 §6.8 的 regex 写死支持 allow-once|once|allow / allow-always|always / deny|reject|block 共 8 个别名 + 两种顺序;若上游新增 alias 或新顺序,channel 端用户敲新形式会被 channel 拒为 "malformed"在 §6.8 regex 旁加显式注释引用上游文件路径;CI 加一个测试断言上游 alias 集合不超出 channel 端支持范围(参考 openclaw/src/auto-reply/reply/commands-approve.ts alias 列表);或考虑改为复用上游 parser(如果上游导出 pure parse 函数则无须重写)
    approval kind 按前缀派发的边界(Issue 2 修订相关)approvalId.startsWith("plugin:") 派发 exec/plugin;若上游未来引入 unprefixed plugin id 或新 kind,本派发会把 plugin 当成 exec(权限校验仍正确,但 metrics/log/未来 plugin-specific authorization 可能出错)CI 加一个上游 id 命名约定的契约测试;若上游新增 kind,本派发函数 + capability.authorizeActorAction 调用方都需同步更新;考虑设计 allowPluginFallback 配置位以兜底
    DingTalk 平台未来变更 cardPrivateData 字段命名
    D1 实现范围Full native runtime(5 个 sub-adapter 全做)Native runtime v1:4 个 sub-adapter(availability / presentation / transport / observe),interactions 推迟到 v2。详见 §5 Discord / Slack / Telegram
    D17前置依赖peerDependency 必须 bump 到 openclaw >= 2026.4.7(才有 ChannelApprovalNativeRuntimeAdapter 契约与 resolveApprovalOverGateway 公开 API);真机验证需要本仓库 PR #480(AI Card v2 / CardBtn[] + sendCardRequest 回调格式) 先合并;本仓库当前 main 已到 v3.6.3,PR #489 基于 4 月旧 main 且 CONFLICTING,不可直接合并——本设计是基于当前 main 与上游 openclaw 的重新整理实现对齐 PR #489 + 当前 main 约束前置依赖(v3.2 修订:完整 SDK 基线,不仅是 peerDep)SDK 基线 = peerDependency + 实际 lockfile + 本地 openclaw 副本三者一致。 +
    1) package.json peerDependencies.openclaw bump 到 >= 2026.4.7(才有 ChannelApprovalNativeRuntimeAdapter 契约与 resolveApprovalOverGateway 公开 API); +
    2) 同步更新 lockfile——当前 node_modules/openclaw 仍是 2026.3.28,tsconfig.json 又优先读 ./node_modules/openclaw/dist/plugin-sdk/*.d.ts,旧类型里没有 approvalCapability / nativeRuntime / resolveApprovalOverGateway。若不一并 bump,type-check 会失败; +
    3) 替代方案:在 tsconfig.json 调整 path 优先级让 ../openclaw/src/plugin-sdknode_modules/openclaw/dist 之上(开发期),用于 monorepo / linked checkout 场景; +
    4) PR #480 已合并(CardBtn[] + sendCardRequest 已在当前 main 可用,无需阻塞); +
    5) PR #489 基于 4 月旧 main 且 CONFLICTING,不可直接合并——本设计是基于当前 main 与上游 openclaw 的重新整理实现
    对齐 PR #489 + 当前 main 实际工程约束
    D18 本设计是 PR #489 工程骨架 + 本设计的权限/测试/分阶段边界的综合,不是任意一边的完整复制。具体取舍见上方 v3 callout 表格
    D20统一 resolver 抽象(v3.2 新增)把"approvalKind 推导 + 权限校验 + allowPluginFallback / resolveMethod + 调 resolveApprovalOverGateway + 错误分类"统一封装到 approval-resolver.ts,由按钮回调(§6.3)/approve 早期 intercept(§6.8)两条路径都调用。 +
    命令解析单独抽到 approval-command-parser.ts,与上游 commands-approve.ts 的 alias / 顺序保持对照测试。 +
    优势:权限策略 / plugin fallback / 错误文案三件事不会在两条路径分叉;新加 v2 surface 时只改 resolver 一处
    对齐 Slack/Telegram 上游 allowPluginFallback 经验
    D21approval kind 推导规则(v3.2 修订 D20 子规则)3 段判断(按 Slack/Telegram 模式): +
    (1) approvalId.startsWith("plugin:"){ resolveMethod: "plugin" } +
    (2) 无前缀 + exec 与 plugin 都授权 → { resolveMethod: "exec", allowPluginFallback: true }(让 resolveApprovalOverGateway 在 exec store 找不到时回退尝试 plugin store) +
    (3) 无前缀 + 仅 plugin 授权 → { resolveMethod: "plugin" } +
    (4) 无前缀 + 仅 exec 授权 → { resolveMethod: "exec" } +
    (5) 都未授权 → 拒绝(私聊提示 + 不调 gateway)
    对齐上游 Slack/Telegram 当前做法
    @@ -264,6 +288,13 @@

    2. 已确认的决策清单

    (6) §10 PR-1 措辞改"resolve 通道生效,approval id 可见性依赖外部界面,完整 UX 在 PR-2"; (7) §6.7 删除 store.register 残留; (8) §6.3 / §6.6 / §8 / §9 等所有 already-resolved 文案改"ℹ️ 已处理或已过期"并明确 catch 后 return 避免覆盖;§6.8 alias 范围显式列清;§11.2 风险表新增 kind 派发边界条目 +
  • v3.2(2026-05-18 第三轮 review):6 处事实约束 + 1 处重构—— + (1) 5→4 sub-adapter 残留(页眉 / 决策表 / 拓扑图 / 模块表)全部清理; + (2) §8 错误矩阵改"transport 自闭环"语义(与 §5.3 deliverPending 一致),不再宣称调 observe.onDeliveryError; + (3) §6.8 /approve 未授权回复改用 sendProactiveTextOrMarkdown 直接路径(明确禁用 dispatchReplyText,避免再次踩 session lock); + (4) 引入统一 resolver 抽象(D20/D21):新增 approval-resolver.ts(kind 推导 + 授权 + allowPluginFallback + gateway 调用 + 错误分类)和 approval-command-parser.ts(纯解析);按钮回调与命令 intercept 都通过 resolver 单点收敛;对齐 Slack/Telegram resolveMethod + allowPluginFallback 经验; + (5) D17 / 阶段 0 把"PR #480 必须先合"改为"已合并需复用契约"(PR #480 实际为 MERGED); + (6) D17 / 阶段 0 扩成"SDK 基线三件套"——peerDependency + pnpm-lock.yaml + tsconfig path 都要管,否则 type-check 卡住
  • userId 字段身份核实:v2 已确认 callback userId === staffId(不是 unionId),所以 §4.2 approver schema(staffId 名单)保持不变
  • @@ -291,7 +322,9 @@

    3.1 上下游分工

    │ │ │ exec-approval / plugin-approval store ─┐ (createApprovalRequest) │ │ │ │ -│ approval-handler-runtime ────────────► nativeRuntime 5 子 adapter │ +│ approval-handler-runtime ────────────► nativeRuntime v1: 4 子 adapter │ +│ (availability/presentation/ │ +│ transport/observe; interactions v2)│ │ (src/infra/approval-handler-runtime.ts) ▲ (DingTalk channel 实现) │ │ │ │ │ resolve gateway ◄───────────────────────┤ 调上游回写 │ @@ -307,18 +340,25 @@

    3.1 上下游分工

    ▼ ┌──────────────────── openclaw-channel-dingtalk (本仓库) ────────────────────┐ │ │ -│ src/approval/ ── 新增 domain 目录(v3:8 个文件,比 v2 少 store/cancel) │ +│ src/approval/ ── 新增 domain 目录(v3.2:9 个文件,含统一 resolver 抽象)│ │ ├─ approval-capability.ts ApprovalCapability 单例装配 │ -│ ├─ approval-native-runtime.ts 4 子 adapter 入口(含可选 5th obs) │ +│ ├─ approval-native-runtime.ts 4 子 adapter(availability/ │ +│ │ presentation/transport/observe) │ │ ├─ approval-card-template.ts 模板 ID(const+env)+ helper │ │ ├─ approval-card-render.ts pending / resolved / expired 渲染 │ │ ├─ approval-fallback-render.ts markdown 兜底(仅 error-recovery) │ │ ├─ approval-target-resolver.ts v1: 仅 origin;v2: + DM │ -│ ├─ approval-callback-handler.ts TOPIC_CARD 卡片按钮回调入口 │ -│ ├─ approval-command-intercept.ts /approve 命令早期 intercept(v3) │ +│ ├─ approval-resolver.ts ★ 统一:kind推导 + 授权 + fallback │ +│ │ + resolveMethod + gateway 调用 │ +│ │ + 错误分类 │ +│ ├─ approval-command-parser.ts ★ 解析 /approve 两种顺序 + alias │ +│ ├─ approval-callback-handler.ts TOPIC_CARD 入口 → approval-resolver │ +│ ├─ approval-command-intercept.ts /approve 早期 intercept → parser │ +│ │ → approval-resolver │ │ └─ approval-config.ts execApprovals.* schema 读写 │ │ │ │ 上游 core 管 pending 生命周期(activeEntries Map);channel 不引入本地 store +│ 按钮回调与 /approve intercept 两条入口全部 → approval-resolver 单一收敛点 │ │ │ 改造点(增量、向后兼容) │ │ ├─ src/channel.ts 新增 approvalCapability 字段 │ @@ -374,29 +414,48 @@

    3.2 模块单一职责表

    normalizeDingTalkTarget, config ~80(v1) + + approval-resolver.ts
    ★ v3.2 新增(D20 落地) + 统一收敛点。导出 resolveApproval({ cfg, accountId, approvalId, decision, senderId, log }): +
    (1) 按 §D21 推导 { resolveMethod, allowPluginFallback } + 调 isExecAuthorizedSender / isPluginAuthorizedSender 做权限校验; +
    (2) 未授权 → return { ok: false, reason: "unauthorized" }(caller 决定怎么提示); +
    (3) 调 resolveApprovalOverGateway({ cfg, approvalId, decision, senderId, clientDisplayName: "DingTalk", resolveMethod, allowPluginFallback }); +
    (4) catch 分类错误:{ ok: false, reason: "already-resolved" | "not-found" | "gateway-error", error }; +
    成功返回 { ok: true }。两条入口(callback + intercept)共享同一行为 + approval-config, SDK gateway runtime + ~140 + + + approval-command-parser.ts
    ★ v3.2 新增 + 纯解析(无副作用)。导出 parseApproveCommand(text: string): { approvalId, decision } | null:支持 /approve <id> <decision>/approve <decision> <id> 两种顺序 + 8 个 alias(allow-once / once / allow / allow-always / always / deny / reject / block)。 +
    测试以上游 openclaw/src/auto-reply/reply/commands-approve.ts 的 alias 集合做对照,确保跨 channel 体验一致 + —(纯函数) + ~60 + approval-callback-handler.ts - TOPIC_CARD 收到 cardPrivateData.actionIds[0]"approval" 前缀的回调时:parseApprovalFromCardPrivateData → authorize → resolveApprovalOverGateway → ack。不依赖本地 store(D18),entry 数据由 core activeEntriestransport.updateEntry 回灌 - capability auth, parseApprovalFromCardPrivateData, SDK gateway runtime - ~150 + TOPIC_CARD 入口(前缀 "approval" 命中):parseApprovalFromCardPrivateData{ approvalId, decision } → 调 approval-resolver.resolveApproval(权限 / fallback / gateway 都在内)→ 按 result.ok 决定终态 update(resolved)或 catch update(ℹ️ 已处理或已过期)→ ack 平台 + parseApprovalFromCardPrivateData, approval-resolver + ~110 approval-config.ts - 纯读 helper:getExecApprovalsConfig / listExecApprovers / isExecAuthorizedSender / resolveNativeDeliveryMode(v1 永远返回 "channel") + 纯读 helper:getExecApprovalsConfig / listExecApprovers / isExecAuthorizedSender / isPluginAuthorizedSender(v1 默认同 exec)/ resolveNativeDeliveryMode(v1 永远返回 "channel") config 模块 - ~90 + ~110 - approval-command-intercept.ts
    (v3 新增模块) - 导出 tryInterceptApproveCommand({ cfg, text, senderId, log }):regex 匹配 /approve <id> <decision> → alias 归一 → resolveApprovalOverGateway。由 src/inbound-handler.ts 入口早期调用(§6.8) - SDK gateway runtime - ~70 + approval-command-intercept.ts + 导出 tryInterceptApproveCommand({ cfg, accountId, text, senderId, log }):调 parseApproveCommand(text) → 命中后调 approval-resolver.resolveApproval。未授权时用 sendProactiveTextOrMarkdown(不是 dispatcher,避免 session lock)回私聊提示。由 src/inbound-handler.ts 入口早期调用(§6.8) + approval-command-parser, approval-resolver, sendProactiveTextOrMarkdown + ~80

    - v3 删除:approval-store.ts(D18:依赖 core activeEntries)、approval-cancel.ts(无 store 无法枚举 pending;D13 推迟到 v2)。
    - 合计新增约 ~830 行业务代码(v2 是 ~1170 行,少 ~340 行);测试代码预计 ~1700 行。 + v3.2 模块结构:resolver(D20)单一收敛点——按钮回调(callback-handler)+ 命令(command-intercept via parser)都进 resolver,权限 / fallback / 错误分类不分叉。
    + v3 删除:approval-store.ts(D18)、approval-cancel.ts(D13 推迟)。
    + v3.2 合计新增约 ~970 行业务代码(v3 是 ~830 行,多 ~140 行的 resolver;换来跨路径行为一致 + 单点修改);测试代码预计 ~1900 行

    3.3 与现有代码的接触面

    @@ -756,73 +815,50 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ 2. (v2 不再走 parseExecApprovalCommandText——按钮路径走 cardPrivateData 解析; │ 命令路径走 §6.8 早期 intercept 的 regex;两条路径最终都调 resolveApprovalOverGateway) │ - │ 3. 推导 approval kind(按 approvalId 前缀派发) - │ approvalKind = parsed.approvalId.startsWith("plugin:") - │ ? "plugin" - │ : "exec" - │ // 上游 approval id 当前约定:plugin approval 带 "plugin:" 前缀; - │ // exec approval 不带前缀。若上游未来引入 unprefixed plugin id, - │ // 这里需补 allowPluginFallback 配置(见 §11.2 风险表) + │ 3. 调统一 resolver(D20:所有 kind 推导 / 授权 / fallback / + │ gateway 调用 / 错误分类都在内部) + │ result = await approvalResolver.resolveApproval({ + │ cfg, + │ accountId: account.accountId, + │ approvalId: parsed.approvalId, + │ decision: parsed.decision, + │ senderId: analysis.userId, // staffId(D5:自批准允许) + │ log, + │ }) │ - │ (v3 不再"查本地 store 找 entry"——D18 删除了 store; - │ approval 是否已过期/已被 resolve 等状态判定由上游 - │ resolveApprovalOverGateway 在 step 5 内部处理,失败时抛具名错误) + │ 4. 按 result 分支 + │ if (!result.ok) { + │ switch (result.reason) { + │ case "unauthorized": + │ // 非 approver 点了按钮 → 私聊提示 + 卡片保留(按钮不变) + │ await sendProactiveTextOrMarkdown( + │ dingtalkConfig, + │ `user:${analysis.userId}`, + │ "⛔ 你不在 approver 名单,无权批准此请求", + │ { accountId, log }) + │ return { handled: true, reason: "unauthorized" } │ - │ 4. 权限校验(v1:exec / plugin 共用同一份 approvers 名单) - │ authorized = capability.authorizeActorAction({ - │ cfg, accountId, senderId: analysis.userId, - │ action: "approve", - │ approvalKind }) // ← 按前缀派发,不写死 - │ if (!authorized.authorized) { - │ sendProactiveTextOrMarkdown( - │ config, - │ `user:${analysis.userId}`, // 带 user: 前缀,明确为 oto 投递 - │ "⛔ 你不在 approver 名单,无权批准此请求", - │ { accountId, log }) - │ return { handled: true, reason: "unauthorized" } + │ case "already-resolved": + │ case "not-found": + │ case "gateway-error": + │ // 兜底卡片刷成中性终态(不写死"已过期",避免误导) + │ await updateCardVariables(payload.outTrackId, { + │ status: "ℹ️ 已处理或已过期", + │ hasAction: "false", + │ btns: "[]", + │ }, token).catch(() => {}) + │ return { handled: true, reason: result.reason } + │ } │ } │ - │ 5. 调上游 SDK 公开 API 回写(v2026.4.7+) - │ import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime" - │ - │ try { - │ await resolveApprovalOverGateway({ - │ cfg, - │ approvalId: parsed.approvalId, - │ decision: parsed.decision, // "allow-once" | "allow-always" | "deny" - │ senderId: analysis.userId, // staffId - │ clientDisplayName: "DingTalk", - │ }) - │ } catch (err) { - │ // 上游抛 already-resolved(已被批)/ not-found(不存在或已清理) - │ // / expired 等具名错误。catch 后用中性文案"已处理或已过期"—— - │ // 不写死"已过期"以免误导(也可能是 already-resolved) - │ await updateCardVariables(payload.outTrackId, { - │ status: "ℹ️ 已处理或已过期", hasAction: "false", btns: "[]", - │ }, token).catch(() => {}) - │ return { handled: true, reason: "resolve-failed" } - │ // ↑ 关键:必须 return,不再走 step 6 的 resolved 终态 update, - │ // 否则会用 "✅ 已批准" 覆盖刚刚的 "已处理或已过期",产生 - │ // 视觉矛盾 - │ } - │ // SDK 内部根据 approval kind 自动 dispatch 到 - │ // exec.approval.resolve / plugin.approval.resolve - │ // (channel 端的 step 3 kind 推导只用于本地权限校验) - │ - │ (上游 resolve 成功后会异步触发 approval-handler-runtime 调 - │ presentation.buildResolvedResult → - │ transport.updateEntry({ phase: "resolved" }) → - │ v1 origin-only 即只刷本机这张卡片,与下面 step 6 内容一致, - │ 幂等覆盖) - │ - │ 6. 仅 step 5 成功(无 catch return)时执行:本机立即 update - │ updateCardVariables(payload.outTrackId, + │ 5. result.ok === true:立即 update 本机这张为 resolved 终态 + │ await updateCardVariables(payload.outTrackId, │ buildResolvedCardParamMap(parsed.decision, analysis.userId), │ token) - │ // 即使上游 updateEntry 异步事件还没回来,用户也能立刻看到 - │ // 终态卡片;step 5 失败路径已 return,本步不会重复执行 + │ // 即使上游 updateEntry 异步事件还没回来,用户也能立刻看到终态。 + │ // 上游事件回来后会再做一次同样内容的 update,幂等覆盖 OK。 │ - │ 7. 日志 + return { handled: true, reason: "resolved" } + │ 6. 日志 + return { handled: true, reason: "resolved" } │ ├─ 分支命中 → 跳过既有 handleCardAction └─ finally: socketCallBackResponse(messageId, { success: true }) (ack 平台)
    @@ -975,91 +1011,73 @@

    解决方案:在 handleDingTalkMessage 入口最早处直接 // (放在 command dispatch 之后是为了让 dedup / self-filter / content extract // 等基础设施先 run;放在 reply 派发之前是关键——避免 session lock 死锁。) -// ---- Early /approve bypass: resolveApprovalOverGateway ---- +// ---- Early /approve bypass:collapsed into approval-resolver ---- +// 关键:parse 与 resolve 都走专用模块(D20),不在 inbound-handler 内堆 200 行 const textForApproveCheck = !isDirect ? extractedContent.text.replace(/^(?:@\S+\s+)*/u, "").trim() // 群里剥前导 @mention : extractedContent.text.trim(); if (/^\/approve\b/i.test(textForApproveCheck)) { - // 支持两种顺序:/approve <id> <decision> 与 /approve <decision> <id> - // alias 表与上游 commands-approve.ts 对齐: - // allow-once / once / allow → "allow-once" - // allow-always / always → "allow-always" - // deny / reject / block → "deny" - const decisionPattern = "(?:allow-once|allow-always|allow|once|always|deny|reject|block)"; - const m = - textForApproveCheck.match( - new RegExp(`^/approve\\s+(\\S+)\\s+(${decisionPattern})\\s*$`, "i"), - ) ?? - textForApproveCheck.match( - new RegExp(`^/approve\\s+(${decisionPattern})\\s+(\\S+)\\s*$`, "i"), - ); - if (!m) { - log?.warn?.("[DingTalk] /approve malformed — usage: /approve <id> <decision>"); - return; - } - // 判定哪个 group 是 decision(顺序两种之一) - const [, g1, g2] = m; - const isDecisionFirst = new RegExp(`^${decisionPattern}$`, "i").test(g1); - const approvalId = isDecisionFirst ? g2 : g1; - const rawDecision = (isDecisionFirst ? g1 : g2).toLowerCase(); - const decision: "allow-once" | "allow-always" | "deny" = - rawDecision === "allow" || rawDecision === "once" ? "allow-once" - : rawDecision === "always" ? "allow-always" - : rawDecision === "reject" || rawDecision === "block" ? "deny" - : (rawDecision as "allow-once" | "allow-always" | "deny"); - - // === Issue 1 修订:channel 端必须自己做 approver 权限校验 === - // resolveApprovalOverGateway 只负责连接 gateway 与回写, - // 不会按 senderId 校验 approver 名单。若不在这里拦截, - // 任何能发 /approve 消息的人都可批准。 - const approvalKind = approvalId.startsWith("plugin:") ? "plugin" : "exec"; - const authResult = dingtalkApprovalCapability.authorizeActorAction?.({ + const { tryInterceptApproveCommand } = await import("./approval/approval-command-intercept"); + const intercepted = await tryInterceptApproveCommand({ cfg, accountId: account.accountId, - senderId, // 入站 senderStaffId - action: "approve", - approvalKind, + text: textForApproveCheck, + senderId, // 入站 senderStaffId + log, }); - if (authResult && !authResult.authorized) { - log?.info?.( - `[DingTalk] /approve denied — sender=${senderId} not in approvers ` + - `(approvalId=${approvalId})`, - ); - // 回一条文本,让发起者明确知道被拒(而不是静默) - try { - await dispatchReplyText( - `⛔ 你不在 approver 名单,无权批准此请求(${approvalId})`, - ); - } catch { - // 回复失败不影响主流程 - } - return; + if (intercepted) { + // intercepted=true 表示这条消息确实是 /approve; + // 不管 resolve 成功失败,都已在 intercept 内部处理(含权限拒绝私聊提示)。 + return; // ← 关键:return,不进 reply 派发 + } + // 若 intercepted=false(命令格式不符合 /approve),继续走正常 inbound pipeline +} + + +

    approval-command-intercept.ts 内部职责(伪代码)

    +
    export async function tryInterceptApproveCommand({
    +  cfg, accountId, text, senderId, log,
    +}): Promise<boolean> {
    +  const parsed = parseApproveCommand(text);          // approval-command-parser
    +  if (!parsed) {
    +    log?.warn?.("[DingTalk] /approve malformed");
    +    return true;                                      // 是 /approve 但格式错;仍 return true
    +                                                      // 让 inbound 不再走 reply 派发
       }
     
    -  log?.info?.(`[DingTalk] /approve intercept id=${approvalId} decision=${decision}`);
    -  try {
    -    const { resolveApprovalOverGateway } = await import(
    -      "openclaw/plugin-sdk/approval-gateway-runtime"
    -    );
    -    await resolveApprovalOverGateway({
    -      cfg,
    -      approvalId,
    -      decision,
    -      senderId,                                          // 入站消息的 senderStaffId
    -      clientDisplayName: "DingTalk",
    -    });
    -  } catch (err) {
    -    log?.warn?.(`[DingTalk] /approve resolve failed: ${getErrorMessage(err)}`);
    +  const result = await approvalResolver.resolveApproval({  // D20 统一收敛
    +    cfg, accountId,
    +    approvalId: parsed.approvalId,
    +    decision:   parsed.decision,
    +    senderId,
    +    log,
    +  });
    +
    +  if (!result.ok && result.reason === "unauthorized") {
    +    // ⚠️ 必须用 sendProactiveTextOrMarkdown(channel 直接路径),
    +    //    绝不能调 SDK dispatchReply——否则又触发 session lock 死锁
    +    //    (正是早期 intercept 要避免的同一个坑)
    +    await sendProactiveTextOrMarkdown(
    +      getConfig(cfg, accountId),
    +      `user:${senderId}`,                            // user: 前缀走 oto
    +      `⛔ 你不在 approver 名单,无权批准此请求(${parsed.approvalId})`,
    +      { accountId, log },
    +    ).catch(() => {});                              // 提示失败不影响主流程
    +  }
    +  // 其它失败(already-resolved / not-found / gateway-error)由用户在卡片侧/
    +  // 下次操作时自然感知;命令路径无原卡片可 update,所以仅 log 不发新消息
    +  if (!result.ok) {
    +    log?.info?.(`[DingTalk] /approve resolver returned ${result.reason}`);
       }
    -  return;                                                // ← 关键:return,不再进 reply 派发
    +  return true;
     }
    -
    - Issue 1 修订:早期 intercept 必须自带 approver 权限校验
    - v3.0 草稿里 intercept 直接调 resolveApprovalOverGateway 漏掉了 approver 校验——上游 resolveApprovalOverGateway 只是 gateway adapter(连接 gateway 并调用 exec.approval.resolve / plugin.approval.resolve),不会按 senderId 做 approver 名单校验。 - 所以 channel 端必须自己先调 capability.authorizeActorAction,否则任何能发 /approve 的用户都能批准(与 §6.6"非 approver 点击"按钮路径形成不对称漏洞)。 - 上述代码已修正——按钮回调 (§6.3 step 4) 与命令早期 intercept (§6.8) 两条路径都走同一 authorizeActorAction 检查。 +
    + v3.2 统一抽象(D20)的好处
    + 按钮回调(§6.3 step 3)与 /approve 命令(§6.8)现在共享同一个 approval-resolver.resolveApproval() 入口。 + 权限校验 / approvalKind 推导 / allowPluginFallback(§D21)/ resolveApprovalOverGateway 调用 / 错误分类——五件事不会在两条路径上分叉。 + v2 future 加 DM 投递或新 surface 时,只改 resolver 一处即可让所有入口都受益。

    为什么 D2 不能"纯复用上游 /approve dispatcher"

    @@ -1297,13 +1315,13 @@

    8. 错误处理矩阵

    createAndDeliver 网络/HTTP 失败 钉钉 API 5xx、超时、429 - onDeliveryError 日志 + 走 §6.7 markdown 兜底 + transport 自闭环:deliverPending 内部 catch + WARN 日志 + 调 approval-fallback-render 发 markdown 兜底 + return null(D5 §5.3)。调 observe.onDeliveryError——那是 runtime 捕获 throw 才触发的钩子,本设计选 transport 内部处理就不能同时让 runtime observe 介入 收到 markdown 消息,含 /approve 命令模板 approval-card 模板未发布 templateId 在 DingTalk 侧不存在 - 同上(错误归一化为 createAndDeliver 失败)+ ERROR 级日志 + 同上(错误归一化为 createAndDeliver 失败)+ ERROR 级日志(transport 内部 log,不 throw) 降级到 markdown;运维需看日志修复 @@ -1364,9 +1382,12 @@

    9.1 测试文件布局

    tests/unit/approval-card-template.test.tscardParamMap 构造(含 stringify 规则)~8 tests/unit/approval-card-render.test.tspending/resolved/expired/canceled × exec/plugin = 8 个矩阵~16 tests/unit/approval-target-resolver.test.tsorigin 解析(含 turnSourceChannel=null)、DM 列表构造~10 - tests/unit/approval-callback-handler.test.tscardPrivateData 结构化 parse(含 button index 后缀)、权限校验、resolveApprovalOverGateway 调用、各错误分支;以 §1.2 的真机回调样本作 fixture~20 + tests/unit/approval-resolver.test.ts
    ★ v3.2D20/D21 核心:kind 推导 4 情况(plugin: 前缀 / 无前缀 + 两边授权 / 仅 plugin 授权 / 仅 exec 授权)、未授权返 unauthorized、resolveApprovalOverGateway 调用参数(含 resolveMethod 与 allowPluginFallback)、错误分类(already-resolved / not-found / gateway-error);mock SDK gateway~18 + tests/unit/approval-command-parser.test.ts
    ★ v3.2两种顺序 × 8 个 alias = 16 个合法 case + 5 个 malformed case + 上游 commands-approve.ts alias 集合对照断言~12 + tests/unit/approval-callback-handler.test.tscardPrivateData 结构化 parse(含 button index 后缀)、调 resolver、按 result 分支处理(resolved/unauthorized/已处理或已过期);以 §1.2 的真机回调样本作 fixture(不重复测 resolver 内部逻辑——那由 resolver test 覆盖)~14 tests/unit/card-callback-service.test.ts(扩展既有)D16 改动:analyzeCardCallbackcardPrivateData 含 actionIds + params;既有 feedback / btn_stop 用例不受影响+6 - tests/unit/inbound-handler-approve-intercept.test.ts§6.8 早期 intercept:群里带 @mention 前缀、私聊、各 decision alias、malformed 命令、resolveApprovalOverGateway 调用~12 + tests/unit/approval-command-intercept.test.tsparser 命中 → 调 resolver;未授权 → sendProactiveTextOrMarkdown(mock);resolve-failed 仅 log;非 /approve 命令 return false;不重复测 parser / resolver 内部~8 + tests/unit/inbound-handler-approve-intercept.test.ts§6.8 在 inbound-handler 的接入:群里带 @mention 前缀、私聊、return 后不进 reply 派发(验证 session lock 不被触发)~8 tests/unit/approval-fallback-render.test.tsmarkdown 兜底文案、exec/plugin 分支~6 tests/unit/approval-capability.test.tsSDK 工厂参数装配正确、capability 单例~6 tests/unit/approval-native-runtime.test.ts4 子 adapter(availability/presentation/transport/observe)集成 mock;含 v1 origin-only 路径~14 @@ -1375,7 +1396,8 @@

    9.1 测试文件布局

    v3 删除:approval-store.test.ts(无本地 store)、approval-cancel.test.ts(无 finalize-on-stop)、approval-channel-stop.test.ts(同上)。 - 合计 ~110 case(v2 是 ~115);测试代码约 1700-2400 行。 + v3.2 新增:approval-resolver.test.ts(核心单点收敛覆盖)+ approval-command-parser.test.ts(含上游 alias 对照)+ approval-command-intercept.test.ts;callback-handler.test 缩 case 数因不再重复测 resolver 内逻辑。 + 合计 ~135 case(v3 是 ~110);测试代码约 2000-2700 行

    9.2 Mock 策略

    @@ -1415,33 +1437,39 @@

    阶段 0 · 前置依赖(必须先满足)

    D17:阶段 1 PR 提交前必须完成的事
      -
    • 上游 openclaw >= 2026.4.7:才有 ChannelApprovalNativeRuntimeAdapter 契约(openclaw/src/infra/approval-handler-runtime-types.ts:216-235)与 resolveApprovalOverGateway 公开 API(openclaw/plugin-sdk/approval-gateway-runtime)。 -
      实施 PR-1 时 package.jsonpeerDependencies.openclaw 同步 bump,写入 release notes 作为 BREAKING change
    • -
    • 本仓库 PR #480 必须先合并:才有 AI Card v2 模板的 CardBtn[]sendCardRequest 回调格式支持。本设计的按钮渲染(§7.2)依赖这两个能力
    • +
    • SDK 基线三件套同时到位(v3.2 关键补充——peerDep 只是冰山一角): +
        +
      1. package.jsonpeerDependencies.openclaw bump 到 >= 2026.4.7(写入 release notes 作为 BREAKING change)——才有 ChannelApprovalNativeRuntimeAdapter 契约(openclaw/src/infra/approval-handler-runtime-types.ts:216-235)与 resolveApprovalOverGateway 公开 API(openclaw/plugin-sdk/approval-gateway-runtime
      2. +
      3. pnpm-lock.yaml 同步更新——当前 node_modules/openclaw 仍是 2026.3.28,tsconfig.json 优先读 ./node_modules/openclaw/dist/plugin-sdk/*.d.ts,旧类型里没有 approvalCapability / nativeRuntime / resolveApprovalOverGateway;若不一并 bump, pnpm run type-check 会失败。pnpm install 后必须确认 node_modules/openclaw/package.json version >= 2026.4.7
      4. +
      5. 验证 tsconfig.json path 解析顺序——开发期 monorepo / linked checkout 场景可临时调高 ../openclaw/src/plugin-sdk 在 paths 中的优先级做 unblock,但 release 前必须切回 lockfile 路径以保证发布包正确
      6. +
    • +
    • 本仓库 PR #480 已合并(MERGED;2026-05-18 核实):AI Card v2 模板的 CardBtn[]sendCardRequest 回调格式已在当前 main 可用。实施时需复用 / 确认当前 main 上的 CardBtn 类型与 sendCardRequest 契约(路径与字段命名)与本 spec §1.2 / §7.2 一致;如有偏差以当前 main 为准并回头修订 spec
    • approval-card 模板上传:在 open-dev.dingtalk.com/fe/card 导入 docs/assets/card-template-approval-v1.json,发布为预置统一模板,拿到 templateId(用于阶段 2 替换占位常量)
    -

    阶段 1 · 接口骨架与权限链路(PR-1)

    +

    阶段 1 · 接口骨架 + 统一 resolver + 命令链路(PR-1)

      -
    • 新增 src/approval/ 目录与全部 8 个文件骨架(v3:删除 store/cancel,新增 command-intercept)
    • -
    • 实现 approval-config.tsapproval-target-resolver.ts(仅 origin)、approval-capability.ts(不含 nativeRuntime 完整实现)、approval-command-intercept.ts
    • +
    • 新增 src/approval/ 目录骨架(v3.2:9 个文件)
    • +
    • 实现核心收敛点(D20):approval-resolver.tsapproval-command-parser.ts
    • +
    • 实现支撑模块:approval-config.tsapproval-target-resolver.ts(仅 origin)、approval-capability.ts(不含 nativeRuntime 完整实现)、approval-command-intercept.ts(薄壳,调 parser + resolver)
    • src/channel.ts 挂上 approvalCapabilitynativeRuntime 暂留 undefined;capability 仅生效 authorizeActorAction / resolveApproveCommandBehavior 等权限部分
    • src/inbound-handler.ts/approve 早期 intercept(§6.8,D2 落地)—— PR-1 就要做,因为这是 Feishu-同档体验的最后一公里
    • -
    • package.json peerDependency bump 到 openclaw >= 2026.4.7
    • +
    • SDK 基线三件套(D17 v3.2):peerDependency bump + pnpm-lock.yaml 更新 + 确认 node_modules/openclaw 版本 / tsconfig path 优先级
    • schema、配置文档(草稿)
    • -
    • 测试:approval-configapproval-target-resolverapproval-capabilityapproval-command-intercept + inbound-handler-approve-intercept
    • -
    • 这阶段交付后:DingTalk 端具备 /approve 命令的 resolve 通道——权限校验(approver 名单)+ 早期 intercept 绕过 session lock 都生效。但 approval id 的可见性仍依赖外部界面(用户需从 OpenClaw WebUI / CLI / 日志拿到 id 后才能在钉钉里手敲)。完整钉钉内"看见 → 点按钮"的端到端用户体验在 PR-2 才落地。Feishu approval-auth 同档。
    • +
    • 测试:approval-resolver(含 §D21 kind 推导 / allowPluginFallback / 错误分类全分支)、approval-command-parser(含上游 alias 对照断言)、approval-configapproval-target-resolverapproval-capabilityapproval-command-interceptinbound-handler-approve-intercept
    • +
    • 这阶段交付后:DingTalk 端具备 /approve 命令的 resolve 通道——权限校验(approver 名单)+ 早期 intercept 绕过 session lock 都生效;resolver 统一抽象就位,PR-2 加按钮回调时只挂一根新线。但 approval id 的可见性仍依赖外部界面(用户需从 OpenClaw WebUI / CLI / 日志拿到 id 后才能在钉钉里手敲)。完整钉钉内"看见 → 点按钮"的端到端用户体验在 PR-2 才落地。Feishu approval-auth 同档。

    阶段 2 · 完整 native runtime(PR-2)

      -
    • 实现 approval-card-template.ts(const 默认 + env 覆盖,DINGTALK_APPROVAL_CARD_TEMPLATE_ID)、approval-card-render.ts(按 §7.2 出 cardPrivateData 结构化 btns)、approval-fallback-render.tsapproval-callback-handler.ts(用 parseApprovalFromCardPrivateData)、approval-native-runtime.ts(4 子 adapter;interactions 不实现)
    • +
    • 实现 approval-card-template.ts(const 默认 + env 覆盖,DINGTALK_APPROVAL_CARD_TEMPLATE_ID)、approval-card-render.ts(按 §7.2 出 cardPrivateData 结构化 btns)、approval-fallback-render.ts
    • +
    • 实现 approval-callback-handler.ts(用 parseApprovalFromCardPrivateData → 调阶段 1 已有的 approval-resolver.resolveApproval——零新增权限逻辑)、approval-native-runtime.ts(4 子 adapter;interactions 不实现)
    • 修改 src/card-callback-service.ts(D16):CardCallbackAnalysiscardPrivateData 字段,analyzeCardCallback 抽 params 并附到 analysis
    • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支(在 feedback / btn_stop 之前)
    • 在阶段 1 的 capability 里挂上 nativeRuntime(4 子 adapter;v1 不实现 interactions)
    • -
    • 测试:剩余 4 个 unit 文件 + 1 个 integration 文件;新增 callback-handler 与 cardPrivateData 解析的 test(基于 §1.2 的真机回调样本作 fixture)
    • -
    • 这阶段交付后:完整三按钮卡片 UX 在真机可用——templateId 已为正式发布版;可走真机回归(参照 skills/dingtalk-real-device-testing/SKILL.md
    • +
    • 测试:剩余 4 个 unit 文件 + 1 个 integration 文件;新增 callback-handler 与 cardPrivateData 解析的 test(基于 §1.2 的真机回调样本作 fixture);resolver 的 unit test 在 PR-1 已完成,PR-2 仅加 callback 入口集成测试覆盖"按钮 → resolver"链路
    • +
    • 这阶段交付后:完整三按钮卡片 UX 在真机可用——templateId 已为正式发布版;按钮回调与命令 intercept 走同一 resolver 行为一致;可走真机回归(参照 skills/dingtalk-real-device-testing/SKILL.md

    阶段 3 · 用户文档与回归收尾(PR-3)

    @@ -1515,7 +1543,7 @@

    11.2 已知风险

    机器人 DM approver 失败(企业权限) 该 approver 收不到 DM;origin 仍可见 - onDeliveryError 日志;用户文档明示需配置工作通知权限 + transport 内部 WARN 日志;用户文档明示需配置工作通知权限 updateCardVariables 在频繁 approve/expire 时遇上 race From 7b5ea23a65cc48f2871be2d1fa8fdc7e3c63b38f Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Mon, 18 May 2026 23:10:29 +0800 Subject: [PATCH 06/44] =?UTF-8?q?docs(spec):=20v3.3=20=E4=BF=AE=E8=AE=A2?= =?UTF-8?q?=20=E2=80=94=20approval=20=E6=8C=82=E5=9C=A8=E5=8E=9F=20agent?= =?UTF-8?q?=20card=20=E4=B8=8A=20+=20markdown=20=E8=B7=AF=E5=BE=84?= =?UTF-8?q?=E7=8B=AC=E7=AB=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户拍板的架构 pivot——卡片模式下不要新建独立审批卡片(太突兀), 按钮挂到 agent 正在 stream 的那张 AI Card 上;markdown 模式走 /approve 文字命令路径。 D 级决策变化: - D9 删除"新建 approval 专用模板",复用 PR #480 已合并的 AI Card v2 + CardBtn[] 能力。移除 BUILTIN_APPROVAL_CARD_TEMPLATE_ID 常量、env、 card-template-approval-v1.json 源、阶段 0 模板上传任务。 - D10 路由策略改为按 card-run-registry 实际状态决定(不读 messageType config),cover 所有 runtime 降级场景:messageType=card 但 createAndDeliver 失败、plugin approval 无 reply card、approval 来得太晚 card 已 FINISHED 等。 - D14 终态展示改为"对原 agent card 做字段级 patch",agent card 自身状态机 不被 approval 触碰。 - D18 entry 结构改为 { approvalId, accountId, mode: "card"|"markdown", outTrackId?: string }(outTrackId 仅 card 路径有值,指向原 agent card)。 - D22 新增:agent-card-coalesce 模型(按钮注入原 card 的 patch 模型)。 - D23 新增:approval 期间隐藏 btn_stop,resolved 后恢复(让用户继续可中止 agent)。 模块变化: - 删除 approval-card-template.ts - 新增 approval-card-locator.ts(按 sessionKey 查 card-run-registry,决定 card vs markdown 路由) - approval-card-render.ts → approval-card-patcher.ts(在原 agent card 上做 按钮注入/清理 patch,而非完整卡片渲染) - approval-fallback-render.ts → approval-markdown-render.ts(markdown 路径 主路径,不再叫 fallback) 数据流变化: - §5.3 transport.prepareTarget 内嵌 card-locator 查询,输出 route 字段 - transport.deliverPending 按 route 分支:card → patcher PUT 更新 agent card; markdown → sendProactiveTextOrMarkdown 发独立消息 - transport.updateEntry 按 entry.mode 分支:card 用 patcher 改原 card; markdown no-op(钉钉消息不能 edit) - §6.2 投递场景重写为 5 个:群聊 card / 私聊 card / 配置 card 但降级 / plugin approval 无 card / CLI 触发 turnSource 非 dingtalk - §6.4 同步流改为按 entry.mode 分支 - §6.7 失败处理重写:card 失败不降级 markdown(避免双消息) §7 卡片设计: - 7.1 改为"复用 AI Card v2 字段映射",含终态指示位三候选(contentKey append / blockList system block / 新增 approvalStatus) - 7.3 加双状态机协同说明(approval lifecycle vs card lifecycle) - 7.4-7.8 旧 mockup 加 v3.3 重画提醒,PR-2 真机回归补新 mockup 阶段调整: - 阶段 0 删除模板上传任务,替换为 AI Card v2 字段能力确认 - 阶段 1 增加 approval-card-locator 实现(markdown 路径也需 locator 返 null) - 阶段 2 渲染层改用 patcher + markdown-render 测试: - 新增 approval-card-locator.test.ts、approval-card-patcher.test.ts、 approval-markdown-render.test.ts - 删除 approval-card-template.test.ts、approval-fallback-render.test.ts 模块从 9 → 10;业务代码 ~970 → ~900 行(patch 比 render 简单); 测试代码预计 ~1800 行。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 489 ++++++++++++------ 1 file changed, 320 insertions(+), 169 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index 1f7e602b..f0f7ca09 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -12,7 +12,11 @@

    Gap #01 · DingTalk Native Approval 设计方案

    - 为 DingTalk Channel 接入 OpenClaw 的 原生审批能力(exec approval + plugin approval)。v1 实现 ChannelApprovalCapability 与 native runtime 4 个 sub-adapter(availability/presentation/transport/observe;interactions 推迟到 v2)。审批 v1 仅投递到 origin 会话(agent 触发的钉钉群/私聊),approver DM 双投递推迟到 v2 且 config-gated。按钮 payload 用 DingTalk sendCardRequest 的 cardPrivateData.params 结构化字段编码 decision/approvalId;按钮点击与 /approve 文本命令两条路径都收敛到上游公开 API resolveApprovalOverGateway。 + 为 DingTalk Channel 接入 OpenClaw 的 原生审批能力(exec approval + plugin approval)。v1 实现 ChannelApprovalCapability 与 native runtime 4 个 sub-adapter(availability/presentation/transport/observe;interactions 推迟到 v2)。审批 v1 仅投递到 origin 会话(agent 触发的钉钉群/私聊),approver DM 双投递推迟到 v2。 +
    v3.3 关键架构:approval 不创建独立卡片,而是按 card-run-registry 实际状态分两路由—— + (card 路径) 若 agent 正在 AI Card 流式输出,把 3 个 approval 按钮挂到原 agent reply card 上(PUT 更新 cardParamMap.btns); + (markdown 路径) 若没有 active card(markdown 模式、card 创建失败降级、plugin approval 无 reply card 等),发独立 markdown 消息含 /approve <id> <decision> 命令模板。 + 按钮点击与 /approve 文本命令两条入口都收敛到 approval-resolver 单一抽象,再调上游 resolveApprovalOverGateway

    P0 · 核心缺口 @@ -27,7 +31,7 @@

    Gap #01 · DingTalk Native Approval 设计方案

    1. 总览

    - DingTalk Channel 当前 src/channel.ts:22-127 没有声明 approvalCapability,所以 OpenClaw 的 exec / plugin 审批流程在钉钉用户那里完全走不通——agent 想跑命令时只能在 WebUI / 终端 UI 里批,从钉钉群里发起的 agent 任务等于 dead-end。本设计填补这一空缺:v1 让 agent 触发审批的钉钉会话(群或私聊)直接收到三按钮卡片,approver 点击即可完成;DM 双投递推迟到 v2。 + DingTalk Channel 当前 src/channel.ts:22-127 没有声明 approvalCapability,所以 OpenClaw 的 exec / plugin 审批流程在钉钉用户那里完全走不通——agent 想跑命令时只能在 WebUI / 终端 UI 里批,从钉钉群里发起的 agent 任务等于 dead-end。本设计填补这一空缺:v1 让 agent 触发审批的钉钉会话(群或私聊)以最自然的方式呈现 approval——AI Card 流式模式下把 3 按钮挂到 agent 正在说话的那张卡片上;markdown 模式或无 active card 时发独立 markdown 消息含 /approve 命令模板。approver 点按钮或敲命令均可完成。

    @@ -36,9 +40,9 @@

    1. 总览

  • exec + plugin 一次到位:两类 approval 都通过同一套 native runtime 处理(按 approvalId 前缀派发)
  • v1 范围有边界:origin-only 投递、4 子 adapter(不含 interactions)、无本地 store;DM 投递 / finalize-on-stop / interactions 推迟到 v2。本边界由 D4 / D13 / D18 锚定,下文所有章节遵循 v1 边界
  • 与 peer 对齐:approver schema / ID 不短化 / slash 命令文本格式都跟 Discord/Telegram/Slack 一致;origin-only 边界与 PR #489 v1 一致
  • -
  • 钉钉特性最大化:审批永远用 DingTalk 互动卡片(独立模板)渲染,与 messageType: card | markdown 配置无关
  • +
  • 钉钉特性最大化(v3.3 修订):审批按 card-run-registry 实际状态分两路由——card 路径在 agent reply card 上挂按钮(不创建新卡,不突兀);markdown 路径发独立文字消息含 /approve 模板。不读 messageType 配置,因为它不反映 runtime 实际降级状态
  • 上游约定为权威:按钮点击解码后通过 SDK 公开 API resolveApprovalOverGateway 回写;用户手敲 /approve 因 session lock 死锁需早期 intercept,但同样收敛到 resolveApprovalOverGateway
  • -
  • 用户低部署摩擦:审批卡片模板 ID 内置默认常量,DINGTALK_APPROVAL_CARD_TEMPLATE_ID env 可覆盖(与现有 AI Card 同模式);只需把 approver staffId 列表写进 channels.dingtalk.execApprovals.approvers 即可启用
  • +
  • 零模板部署摩擦(v3.3 新约束):不再需要为 approval 单独发布卡片模板——card 路径复用 PR #480 已合并的 AI Card v2 模板的 CardBtn[] 能力,markdown 路径无需模板。只需把 approver staffId 列表写进 channels.dingtalk.execApprovals.approvers 即可启用
  • @@ -177,17 +181,20 @@

    2. 已确认的决策清单

    D9 - 卡片模板(v3 修订) - 新建 approval 专用模板(独立 templateId);内置默认常量 + 保留 env 覆盖能力DINGTALK_APPROVAL_CARD_TEMPLATE_ID)。 -
    默认值确保用户开箱即用,env 覆盖给真机调试、模板迭代、私有部署留出空间——与现有 DINGTALK_CARD_TEMPLATE_ID 同模式。 -
    JSON 源 commit 到 docs/assets/card-template-approval-v1.json - 对齐现有 AI Card 惯例 + 卡片模板(v3.3 大改) + 不再新建 approval 专用模板——card 路径直接复用 PR #480 已合并的 AI Card v2 模板的 CardBtn[] 能力。 +
    v3.3 移除:BUILTIN_APPROVAL_CARD_TEMPLATE_ID 常量、DINGTALK_APPROVAL_CARD_TEMPLATE_ID env、docs/assets/card-template-approval-v1.json 源、阶段 0 模板上传任务。 +
    实施时确认 AI Card v2 模板的 cardParamMap.btns + hasAction 字段在当前 main 行为符合 §7.2 描述(默认应该 OK,PR #480 设计就这样) + v3.3 用户拍板:approval 挂到原 card D10 - 渲染策略 - 所有 approval 永远用 approval 卡片,与 messageType 无关;markdown/text 仅作 createAndDeliver 失败时的 error-recovery 兜底 - 三家完全一致 + 渲染策略(v3.3 重写) + 不读 messageType 配置——按 card-run-registry 实际状态 分两路由: +
    card 路径:sessionKey 命中 registry 且 entry.state ∈ {PROCESSING, INPUTING} → 在 entry.outTrackId 上 PUT 注入 approval 按钮 +
    markdown 路径:其它所有情况(无 entry / entry 已 FINISHED/STOPPED/FAILED / entry 被 TTL sweep / messageType=markdown 时本来就没 entry / card 创建失败降级)→ sendProactiveTextOrMarkdown 发独立消息 + /approve 命令模板 +
    messageType 配置完全不参与决策——单一事实源是 registry 实际状态 + v3.3 用户拍板:runtime 实际降级要 cover D11 @@ -211,9 +218,10 @@

    2. 已确认的决策清单

    D14 - 终态展示 - kind: "update"——PUT /v1.0/card/instances 改 statusFooter + 隐藏按钮组,卡片保留 - Slack 同款 + 终态展示(v3.3 修订:原 agent card patch) + card 路径kind: "update"——PUT /v1.0/card/instances 改原 agent card 的 cardParamMap(清除 approval 按钮 + 写终态指示 "✅ 已批准 by @user · allow-once" / "ℹ️ 已处理或已过期" 到 agent card 的某个变量位)。agent card 本身的状态机(PROCESSING/INPUTING/FINISHED)不被 approval 触碰,approval 只对 cardParamMap 做字段级 patch。
    具体写入位置(candidate):append 到 contentKey 末尾、或写到独立变量 approvalStatus、或加 system block——PR-2 实施时按 AI Card v2 模板现有支持决定,可能轻微调整模板。 +
    markdown 路径:v1 不发终态通知消息(markdown 不能 edit,发新消息会刷屏;用户从命令成功的事实自然感知。v2 future 可视用户反馈再加) + v3.3 用户拍板 + Slack patch 模式参考 D15 @@ -240,10 +248,10 @@

    2. 已确认的决策清单

    D18 - 本地 store(v3 新增) - 不引入本地 approval-store.ts。channel 端不复制一份 pending entries——上游 ChannelApprovalNativeRuntimeAdapter 已在 core 的 activeEntries Map 管理生命周期,transport.deliverPending 返回的 entry(含 outTrackId / conversationId / accountId)会被 core 自动带回给 transport.updateEntry({ entry, payload, phase }),channel 直接从 entry 读字段即可。 -
    影响:D13 停机 finalize 在 v1 不做;重启行为是"点击旧按钮 → 上游返回 not-found → 卡片刷成已过期/已关闭"(详见 §6.6 重启场景) - 对齐 PR #489 + 本地 store(v3 新增;v3.3 微调) + 不引入本地 approval-store.ts。channel 端不复制一份 pending entries——上游 ChannelApprovalNativeRuntimeAdapter 已在 core 的 activeEntries Map 管理生命周期。 +
    v3.3 修订transport.deliverPending 返回的 entry 现含 { approvalId, accountId, mode: "card" | "markdown", outTrackId?: string }outTrackId 仅 card 路径有值,指向 原 agent reply card 而非独立 approval card)。core 把整个 entry 带回给 transport.updateEntry,channel 端直接读 entry.mode 分支处理:card 路径 PUT 更新 agent card;markdown 路径 no-op + 对齐 PR #489 + v3.3 双路由 D19 @@ -270,6 +278,26 @@

    2. 已确认的决策清单


    (5) 都未授权 → 拒绝(私聊提示 + 不调 gateway) 对齐上游 Slack/Telegram 当前做法 + + D22 + agent-card-coalesce(v3.3 新增核心) + card 路径下,approval 按钮挂在 原 agent reply card 而非新建独立卡片: +
    (1) transport.prepareTarget 内部调 approval-card-locator.findActiveAgentCard(request),按 request.sessionKeycard-run-registry; +
    (2) 命中且 entry.state ∈ {PROCESSING, INPUTING} → preparedTarget.route="card" 携带 entry.outTrackId;否则 route="markdown"; +
    (3) transport.deliverPending 按 route 分支:card 走 PUT updateCardVariables(outTrackId, { btns: JSON.stringify(approvalButtons), hasAction: "true" });markdown 走 sendProactiveTextOrMarkdown; +
    (4) entry 记下 mode + outTrackId(如有),core 把 entry 带回 updateEntry 时按 mode 分支处理终态 +
    关键不变量:approval 只对 cardParamMap 做字段级 patch,不触碰 agent card 的 lifecycle 状态机(PROCESSING/INPUTING/FINISHED 仍由 agent reply 流控制) + v3.3 用户拍板 + + + D23 + btn_stop 与 approval 按钮共存(v3.3 新增) + approval 期间隐藏 btn_stop(cardParamMap.btns 只含 approval 3 按钮); +
    resolved 后恢复 btn_stop(若 agent card 还在 INPUTING 状态,让用户继续可中止 agent); +
    expired/decision=deny 时恢复 btn_stop 不一定有意义——deny 时 agent 通常会被上游终止,card 进入 STOPPED 状态,按钮自然隐藏。 +
    语义动机:"等批准" 阶段 stop 没语义(agent 卡在等决定),4 按钮也太挤;批准放行后 agent 继续输出,stop 才重新有用 + v3.3 用户拍板 + @@ -288,6 +316,14 @@

    2. 已确认的决策清单

    (6) §10 PR-1 措辞改"resolve 通道生效,approval id 可见性依赖外部界面,完整 UX 在 PR-2"; (7) §6.7 删除 store.register 残留; (8) §6.3 / §6.6 / §8 / §9 等所有 already-resolved 文案改"ℹ️ 已处理或已过期"并明确 catch 后 return 避免覆盖;§6.8 alias 范围显式列清;§11.2 风险表新增 kind 派发边界条目 +
  • v3.3(2026-05-18 第四轮 review,架构 pivot):用户提出"卡片模式下不要新建独立 approval 卡片,挂到原 agent reply card 上;markdown 模式走 /approve 文字命令"。修订面: + (1) 删除 D9 新模板创建/上传,复用 PR #480 已合并的 AI Card v2 + CardBtn[]; + (2) D10 路由策略改为按 card-run-registry 实际状态(不读 messageType,因 runtime 降级不反映); + (3) 新增 D22 agent-card-coalesce(buttons 注入原 card 的 patch 模型); + (4) 新增 D23 按钮共存策略(approval 期间隐藏 btn_stop,resolved 后恢复); + (5) 模块变化:删 approval-card-template;新增 approval-card-locator(查 active card);approval-card-render → approval-card-patcher(按钮注入/清理 patch);approval-fallback-render → approval-markdown-render(markdown 路径主路径,非 fallback); + (6) §5 transport 重写为两路由分支;§6 数据流全部重写;§7 mockup 重画为 agent reply 卡片含按钮; + (7) §10 阶段 0 删除"模板上传",阶段 1 新增 approval-card-locator 实现
  • v3.2(2026-05-18 第三轮 review):6 处事实约束 + 1 处重构—— (1) 5→4 sub-adapter 残留(页眉 / 决策表 / 拓扑图 / 模块表)全部清理; (2) §8 错误矩阵改"transport 自闭环"语义(与 §5.3 deliverPending 一致),不再宣称调 observe.onDeliveryError; @@ -340,13 +376,17 @@

    3.1 上下游分工

    ▼ ┌──────────────────── openclaw-channel-dingtalk (本仓库) ────────────────────┐ │ │ -│ src/approval/ ── 新增 domain 目录(v3.2:9 个文件,含统一 resolver 抽象)│ +│ src/approval/ ── 新增 domain 目录(v3.3:10 个文件,含 card-locator + 双路由) │ ├─ approval-capability.ts ApprovalCapability 单例装配 │ │ ├─ approval-native-runtime.ts 4 子 adapter(availability/ │ │ │ presentation/transport/observe) │ -│ ├─ approval-card-template.ts 模板 ID(const+env)+ helper │ -│ ├─ approval-card-render.ts pending / resolved / expired 渲染 │ -│ ├─ approval-fallback-render.ts markdown 兜底(仅 error-recovery) │ +│ ├─ approval-card-locator.ts ★ v3.3 新增:按 sessionKey 查 │ +│ │ card-run-registry,决定 route │ +│ ├─ approval-card-patcher.ts ★ v3.3 替代 card-template+render: │ +│ │ 在原 agent card 上注入/清除按钮 │ +│ │ + 写入 approval 终态指示 │ +│ ├─ approval-markdown-render.ts ★ v3.3 替代 fallback-render: │ +│ │ markdown 路径主路径(不再"fallback") │ ├─ approval-target-resolver.ts v1: 仅 origin;v2: + DM │ │ ├─ approval-resolver.ts ★ 统一:kind推导 + 授权 + fallback │ │ │ + resolveMethod + gateway 调用 │ @@ -359,17 +399,17 @@

    3.1 上下游分工

    │ │ │ 上游 core 管 pending 生命周期(activeEntries Map);channel 不引入本地 store │ 按钮回调与 /approve intercept 两条入口全部 → approval-resolver 单一收敛点 +│ v3.3 删除:approval-card-template.ts(不再新建模板,复用 AI Card v2) │ │ │ 改造点(增量、向后兼容) │ │ ├─ src/channel.ts 新增 approvalCapability 字段 │ │ ├─ src/config-schema.ts 新增 execApprovalsSchema │ │ ├─ src/gateway/channel-gateway.ts TOPIC_CARD listener 加 approve 分支│ -│ ├─ src/card/card-template.ts 加 BUILTIN_APPROVAL_CARD_TEMPLATE_ID│ -│ └─ src/types.ts 加 ApprovalCardEntry / Decision │ +│ ├─ src/card/card-template.ts v3.3 不修改(复用 AI Card v2) │ +│ └─ src/types.ts 加 ApprovalEntry / Decision │ │ │ │ 资产 │ -│ ├─ docs/assets/card-template-approval-v1.json 低代码 schema │ -│ ├─ docs/assets/card-template-approval-v1-source.md 字段语义说明 │ +│ (v3.3 删除 card-template-approval-v1.json 与 -source.md,复用 AI v2) │ │ └─ docs/user/features/exec-approval.md 用户配置指南 │ │ │ └────────────────────────────────────────────────────────────────────────────┘ @@ -391,22 +431,27 @@

    3.2 模块单一职责表

    ~220 - approval-card-template.ts - BUILTIN_APPROVAL_CARD_TEMPLATE_ID 常量 + buildApprovalCardParamMap() 把 view+phase 转 KV - — - ~120 + approval-card-locator.ts
    ★ v3.3 新增(D22 落地核心) + 导出 findActiveAgentCard({ cfg, accountId, sessionKey }):按 sessionKey 查 card-run-registry,仅在 entry 存在且 entry.card.state ∈ {PROCESSING, INPUTING} 时返回 { outTrackId, sessionKey };否则返回 null(caller 走 markdown 路径)。 +
    关键边界:entry.state 必须在 active 集合内,避免对 FINISHED/STOPPED/FAILED card 错误注入按钮 + card-run-registry(既有模块) + ~60 - approval-card-render.ts - 给 request+phase 输出 cardParamMap(pending/resolved/expired/canceled × exec/plugin) - approval-card-template - ~180 + approval-card-patcher.ts
    ★ v3.3 替代 card-template + card-render + 知道如何在已存在的 agent reply card上做按钮区域 patch: +
    buildApprovalButtons(approvalId): CardBtn[](3 个按钮,编码同 §1.2 D15); +
    applyPendingPatch(outTrackId, approvalId, token):PUT 设置 btns=approvalButtons, hasAction="true"(隐藏 btn_stop,D23); +
    applyResolvedPatch(outTrackId, decision, resolverDisplayName, token):PUT 清按钮 + 写"✅ 已批准 by @user · <decision>" 到 agent card 的某变量位(具体位置 PR-2 实施时决定)+ 若 card 还活则恢复 btn_stop; +
    applyExpiredPatch(outTrackId, token):PUT 清按钮 + 写"ℹ️ 已处理或已过期" + 恢复 btn_stop(如适用) + card-callback-service.updateCardVariables, AI Card v2 CardBtn 类型 + ~150 - approval-fallback-render.ts - 仅 createAndDeliver 失败时调用,生成 markdown/text 兜底正文 - approval-config - ~60 + approval-markdown-render.ts
    ★ v3.3 替代 fallback-render + markdown 路径主路径(不再叫 fallback)。导出 buildExecApprovalMarkdown(request, nowMs): stringbuildPluginApprovalMarkdown(request, nowMs): string:构造含 approval id、命令 preview / tool 描述、过期 hint、3 个 /approve <id> <decision> 复制即用模板的 markdown 文本 + approval-config(读 expire hint 等) + ~90 approval-target-resolver.ts @@ -453,9 +498,13 @@

    3.2 模块单一职责表

    - v3.2 模块结构:resolver(D20)单一收敛点——按钮回调(callback-handler)+ 命令(command-intercept via parser)都进 resolver,权限 / fallback / 错误分类不分叉。
    + v3.3 模块结构: + 路由:approval-card-locator(D22)按 registry 状态选 card vs markdown。 + 渲染:approval-card-patcher(card 路径,在原 agent card 上 patch)+ approval-markdown-render(markdown 路径,主路径)。 + 收敛:approval-resolver(D20)单点处理 kind/auth/fallback/gateway/errors。
    v3 删除:approval-store.ts(D18)、approval-cancel.ts(D13 推迟)。
    - v3.2 合计新增约 ~970 行业务代码(v3 是 ~830 行,多 ~140 行的 resolver;换来跨路径行为一致 + 单点修改);测试代码预计 ~1900 行。 + v3.3 删除:approval-card-template.ts(D9 不再新建模板,复用 AI Card v2)。
    + v3.3 合计新增约 ~900 行业务代码(比 v3.2 的 ~970 行少 ~70,因为没有完整 card-render,patch 比 render 简单);测试代码预计 ~1800 行

    3.3 与现有代码的接触面

    @@ -479,8 +528,8 @@

    3.3 与现有代码的接触面

    src/card/card-template.ts - 新增 BUILTIN_APPROVAL_CARD_TEMPLATE_ID 常量(与 AI Card 常量并列) - 极低 + v3.3 不修改——D9 v3.3 已废弃新建 approval 模板,复用现有 AI Card v2 模板字段 + — src/types.ts @@ -612,8 +661,10 @@

    5.2 presentation

    buildPendingPayload({ request, nowMs, view }) - 调 approval-card-render.buildPendingCardParamMap(request, view),返回 { kind: "card", templateId, cardParamMap }
    cardParamMap 含 btns 字段(JSON-stringified CardBtn[]),三按钮 共享 actionId "approval",靠 params.d 区分(D15): -
    btns = [
    +              v3.3 修订:同时构造 card 路径用的 buttons + markdown 路径用的文本。
    +                
    transport 后续按 preparedTarget.route 选用其一。返回 { approvalId: request.id, buttons: CardBtn[], markdownText: string }。 +
    buttons(card 路径用):3 个 CardBtn,共享 actionId "approval",靠 params.d 区分(D15 编码): +
    buttons = [
       { text:"✅ 允许一次", color:"green",
         event:{type:"sendCardRequest", params:{
           actionId:"approval", params:{t:"approval", d:"allow-once", id:request.id}}}},
    @@ -623,7 +674,8 @@ 

    5.2 presentation

    { text:"⛔ 拒绝", color:"red", event:{type:"sendCardRequest", params:{ actionId:"approval", params:{t:"approval", d:"deny", id:request.id}}}}, -]
    +]
    +
    markdownText(markdown 路径用):由 approval-markdown-render.buildExecApprovalMarkdown(request, nowMs)buildPluginApprovalMarkdown 输出,含 approval id、command/tool preview、3 个 /approve <id> <decision> 复制即用块 buildResolvedResult({ request, resolved, view, entry }) @@ -642,19 +694,27 @@

    5.3 transport

    prepareTarget({ plannedTarget, request, view, pendingPayload }) - 从 plannedTarget.target.to(注意:参数是嵌套的 plannedTarget.target.{to, threadId},不是裸 plannedTarget.to)取出原始 target string,调 normalizeApprovalTargetTo(§6.1)确保带 user:/group: 前缀。 -
    返回 { target: { to: normalizedTo, isGroup: normalizedTo.startsWith("group:"), accountId }, dedupeKey: \`dingtalk:${accountId}:${normalizedTo}\` }。DingTalk 无 thread 概念,threadId 永远 null。 + v3.3 修订:内嵌 card-locator 查询决定路由。 +
    (1) 从 plannedTarget.target.to(嵌套)取 target string,normalizeApprovalTargetTo 确保带 user:/group: 前缀; +
    (2) 调 approval-card-locator.findActiveAgentCard({ cfg, accountId, sessionKey: request.sessionKey }); +
    (3a) 命中 → preparedTarget = { target: { to: normalizedTo, isGroup, accountId }, route: "card", activeCardOutTrackId: found.outTrackId, dedupeKey: \`dingtalk:${accountId}:${normalizedTo}:${found.outTrackId}\` }; +
    (3b) 未命中 → preparedTarget = { target: { to, isGroup, accountId }, route: "markdown", dedupeKey: \`dingtalk:${accountId}:${normalizedTo}:markdown:${request.id}\` }。 +
    DingTalk 无 thread 概念,threadId 永远 null deliverPending({ cfg, accountId, preparedTarget, request, pendingPayload }) - 根据 preparedTarget.target.isGroup 分支调 card-service.createAndDeliver
    • group → { openConversationId: stripPrefix(target.to), ... }
    • user → { userIds: [stripPrefix(target.to)], ... }
    统一参数:outTrackId = \`approval_${request.id}_${hash(target.to)}\`templateId = resolveApprovalTemplateId()(const 或 env 覆盖),cardParamMap 来自 buildPendingPayloadcallbackType: "STREAM"。 -
    成功:直接返回 entry { approvalId: request.id, outTrackId, conversationId: stripPrefix(target.to), isGroup, accountId }——不写本地 store(D18),core 会自动把这个 entry 缓存到 activeEntries 并在后续 updateEntry 调用时带回。 -
    失败(observe 语义说明):上游 runtime 是捕获 deliverPending 抛错后才调 observe.onDeliveryError。本设计选择"transport 内部自闭环":catch 错误后内部直接 log + 调 approval-fallback-render 发 markdown 兜底 + return null再 throw(避免 runtime 重复 log,也避免 observe 收到错误后误以为 fallback 未发)。 -
    不要在 catch 内再显式调 observe.onDeliveryError——observe 是 runtime 触发的钩子,自己内部调它会破坏 SDK 契约 + v3.3 修订:按 preparedTarget.route 分两条路径。 +
    route="card":调 approval-card-patcher.applyPendingPatch(activeCardOutTrackId, request.id, token)——PUT updateCardVariables 注入 3 个 approval 按钮到现有 agent card(D22)+ 隐藏 btn_stop(D23)。成功返回 entry = { approvalId, accountId, mode: "card", outTrackId: activeCardOutTrackId }。 +
    route="markdown":调 approval-markdown-render 构造 markdown 文本(含 /approve 命令模板),sendProactiveTextOrMarkdown(config, target.to, text, ...) 发独立消息。成功返回 entry = { approvalId, accountId, mode: "markdown" }(无 outTrackId)。 +
    失败(两路径通用):transport 内部 catch + WARN log + return null。在 catch 内调 observe.onDeliveryError(runtime 契约不允许 transport 自己触发 observe)。 +
    注:card 路径失败时降级到 markdown 路径——core 看 null 即标 entry 失败,避免一次 approval 投出两条消息 updateEntry({ cfg, accountId, entry, payload, phase }) - 调 updateCardVariables(entry.outTrackId, payload.cardParamMap, token)(PUT /v1.0/card/instances)。entry 由 core 从 activeEntries 取出回传,不需要查任何本地 store。phase=expired/resolved 时 core 自动从 activeEntries 移除该 entry + v3.3 修订:按 entry.mode 分支。 +
    entry.mode === "card":phase=resolved → approval-card-patcher.applyResolvedPatch(entry.outTrackId, payload.decision, payload.resolverDisplayName, token);phase=expired → applyExpiredPatch(entry.outTrackId, token)。两个 patcher 内部都做"清按钮 + 写终态指示 + 视情况恢复 btn_stop"(D14 D23)。 +
    entry.mode === "markdown":no-op——markdown 消息不能 edit;v1 不发新通知消息避免刷屏(D14 markdown 路径说明)。 +
    entry 由 core 从 activeEntries 带回,channel 不查任何本地 store(D18) deleteEntry({ cfg, accountId, entry, phase }) @@ -726,57 +786,99 @@

    resolveApproverDmTargets(v1 不实现)

    v2 future 实现时会按 approvers.map(staffId => ({ to: \`user:${staffId}\`, threadId: null })) 模式输出。
  • -

    6.2 投递 pending(v3:仅 origin)

    -
    场景 A:用户在钉钉群 cid_xxx 里 @ agent,agent 跑命令需要批准。
    -       approvers = ["staffA", "staffB"]
    -       触发用户也是 staffA(在群里)
    -
    -approval-runtime 投递计划(D4 v1: origin-only):
    -  └─ origin = { to: "group:cid_xxx" }     ← 只有这一条
    +        

    6.2 投递 pending(v3.3 双路由)

    + +

    场景 A:群里 @ agent,agent AI Card 流式中触发 approval(card 路径)

    +
    前提:messageType=card,agent reply card 已创建并 INPUTING 中。
    +     approvers = ["staffA", "staffB"],触发者 staffA。
    +
    +approval-runtime 投递计划(D4 v1 origin-only):
    +  └─ origin = { to: "group:cid_xxx" }
    +
    +调 prepareTarget(v3.3 D22 路由判定):
    +  ├─ normalizedTo = "group:cid_xxx"
    +  ├─ approval-card-locator.findActiveAgentCard({
    +  │     accountId:"default", sessionKey:request.sessionKey })
    +  │   → card-run-registry 命中 entry,state=INPUTING
    +  │   → 返回 { outTrackId: "ai_card_xxx", sessionKey }
    +  ├─ preparedTarget = {
    +  │     target:    { to:"group:cid_xxx", isGroup:true, accountId:"default" },
    +  │     route:     "card",
    +  │     activeCardOutTrackId: "ai_card_xxx",
    +  │     dedupeKey: "dingtalk:default:group:cid_xxx:ai_card_xxx",
    +  │   }
    +
    +调 deliverPending(route=card 分支):
    +  └─ approval-card-patcher.applyPendingPatch("ai_card_xxx", "abc123", token)
    +       → PUT updateCardVariables("ai_card_xxx", {
    +           btns: JSON.stringify([, , ]),
    +           hasAction: "true",
    +           // D23:approval 期间隐藏 btn_stop,所以 btns 只含 approval 3 个
    +         }, token)
    +       → entry = {
    +           approvalId:"abc123",
    +           accountId:"default",
    +           mode:"card",
    +           outTrackId:"ai_card_xxx",
    +         }
    +  └─ core 把 entry 缓存到 activeEntries
    +
    +→ 群里 staffA 和 staffB 在 agent reply card 底部看到 3 按钮
    +  agent 流式输出暂停(waitDecision 阻塞),等点击 / 命令 / 过期
    + +

    场景 B:用户在 messageType=markdown 模式触发 exec(markdown 路径)

    +
    前提:messageType=markdown,agent reply 已通过 markdown 消息发出。
    +     card-run-registry 中无该 sessionKey 的 entry。
     
     调 prepareTarget:
    -  └─ origin → dedupeKey = "dingtalk:default:group:cid_xxx"
    -              target    = { to: "group:cid_xxx", isGroup: true, accountId: "default" }
    +  ├─ findActiveAgentCard → null
    +  ├─ preparedTarget = {
    +  │     target:    { to:"group:cid_xxx", isGroup:true, accountId:"default" },
    +  │     route:     "markdown",
    +  │     dedupeKey: "dingtalk:default:group:cid_xxx:markdown:abc123",
    +  │   }
     
    -调 deliverPending:
    -  └─ group:cid_xxx → createAndDeliver({
    -        outTrackId: "approval_abc123_",
    -        openConversationId: "cid_xxx",
    -        cardData.cardParamMap: { btns: , content: "...", hasAction: "true" },
    -        callbackType: "STREAM",
    -      })
    -  └─ 返回 entry = { approvalId:"abc123", outTrackId, conversationId:"cid_xxx",
    -                    isGroup:true, accountId:"default" }
    -  └─ core 把 entry 缓存到 activeEntries(channel 端无本地 store)
    +调 deliverPending(route=markdown 分支):
    +  └─ markdownText = approval-markdown-render.buildExecApprovalMarkdown(request, nowMs)
    +       → "### ⚠️ 需要审批:<command preview>
    +          **ID**: `abc123`
    +          **过期时间**: 10 分钟
    +          批准(仅一次):`/approve abc123 allow-once`
    +          批准(总是):`/approve abc123 allow-always`
    +          拒绝:`/approve abc123 deny`"
    +  └─ sendProactiveTextOrMarkdown(config, "group:cid_xxx", markdownText, opts)
    +  └─ entry = { approvalId:"abc123", accountId:"default", mode:"markdown" }
    +                                                      ↑ 注意无 outTrackId
     
    -→ 群里 staffA 和 staffB 都能在群消息流看到审批卡片
    -  staffA / staffB 任一点击都能批(因都在 approvers 名单;§6.3 权限校验)
    -  群里其它非 approver 点击 → 拒绝提示(§6.6)
    +→ 群里出现一条独立 markdown 消息,approver 看到后敲 /approve 命令完成审批
    -——————————————————————————————————————————————————————————————————— +

    场景 C:messageType=card 但 createAndDeliver 中途降级了(runtime 真实场景)

    +
    前提:messageType=card 配置,但本次 reply createAndDeliver 失败,
    +     reply-strategy 已降级为 markdown,card-run-registry 无 entry。
     
    -场景 B:用户 staffA 直接跟 agent DM 触发 exec:
    -       origin = { to: "user:staffA" }
    +→ prepareTarget → findActiveAgentCard 返 null → route="markdown"
    +→ deliverPending → 走 markdown 路径,与场景 B 完全一致
     
    -调 prepareTarget:
    -  └─ origin → dedupeKey = "dingtalk:default:user:staffA"
    -              target    = { to: "user:staffA", isGroup: false, accountId: "default" }
    +D10 关键不变量:messageType 配置不参与决策,纯看 card-run-registry 实际状态。
    +runtime 任何降级都被天然 cover。
    -调 deliverPending: - └─ user:staffA → createAndDeliver({ userIds: ["staffA"], ... }) ✓ +

    场景 D:plugin approval 由 plugin 自己触发,agent 没有 reply card

    +
    前提:plugin 调用过程中触发 approval,agent 自身没在生成 reply。
     
    -→ staffA 私聊里看到审批卡片,自己点自己批(D5 self-approval 允许)
    +→ findActiveAgentCard 返 null → route="markdown"
    +→ markdown 消息发到 plugin 触发会话(turnSourceTo)
     
    -———————————————————————————————————————————————————————————————————
    +注意:此场景下用户可能预期 plugin approval 也走卡片,但 v1 设计明确:
    +没有 active agent reply card 就走 markdown。v2 future 可考虑为 plugin
    +approval 创建轻量"approval-only" 卡(但当前 D9 v3.3 已明确不新建模板)。
    -场景 C:用户从 CLI 跑 codex 触发 exec(turnSourceChannel 为空 / 不是 dingtalk): - resolveOriginTarget 返回 null(v1 没有 DM fallback) +

    场景 E:CLI 触发的 exec approval(turnSourceChannel 非 dingtalk)

    +
    前提:用户从 CLI 跑 codex,approval 触发时 turnSourceChannel ≠ "dingtalk"。
     
    -→ v1 channel 端不投任何 approval 卡片
    -→ 用户需要自己在钉钉里手敲 /approve   完成审批(§6.8)
    -   approval id 用户从 CLI 的 OpenClaw 输出里复制
    +→ availability.shouldHandle 直接返 false(§5.1 v1 origin-only 四连判第 2 条)
    +→ DingTalk 端不投递;用户在钉钉里需要自己手敲 /approve 命令兜底
     
    -v2 future:CLI 触发场景启用 DM 投递,让 approver 在私聊里直接点按钮
    +v2 future:approver-DM 投递启用后,CLI 场景能自动 DM 给 approver。

    6.3 点击 approve → 上游 resolve(核心交互链路)

    用户在卡片上点"允许一次"
    @@ -863,16 +965,17 @@ 

    6.3 点击 approve → 上游 resolve(核心交互链路)

    ├─ 分支命中 → 跳过既有 handleCardAction └─ finally: socketCallBackResponse(messageId, { success: true }) (ack 平台)
    -

    6.4 上游 resolve 后的卡片状态同步(v3:单卡)

    -

    v1 origin-only 模式下,每个 approval 只有 1 张卡片(origin 会话里那张)。点击 → 上游 store 标记 resolved → approval-handler-runtime 对这 1 张 entry 调 transport.updateEntry({ phase: "resolved" })。channel 端 callback handler 自己也会做一次 best-effort update(§6.3 step 6),所以视觉上看到终态来源有两条:

    +

    6.4 上游 resolve 后的状态同步(v3.3:按 entry.mode 分支)

    +

    点击或命令 → 上游 store 标记 resolved → approval-handler-runtime 对每个 entry 调 transport.updateEntry({ phase: "resolved" })。channel 按 entry.mode 分支:

    -
    approval abc123 被 staffA 在群 cid_xxx 点了"允许一次"
    +        

    mode === "card":在原 agent reply card 上做终态 patch

    +
    approval abc123 被 staffA 在 agent reply card 上点了"允许一次"
     
            ┌──────────────────────────────────────┐
    -       │ channel callback handler(§6.3 step 6)│
    -       │   立即 PUT updateCardVariables       │  ← 第一次 update(同步,无网络往返延迟)
    -       │   status="✅ 已批准", hasAction=false  │     用户即时看到终态
    -       └──────────────────┬───────────────────┘
    +       │ channel callback handler(§6.3 step 5)│
    +       │   approval-card-patcher.applyResolvedPatch(outTrackId, ...)
    +       │   → PUT 清按钮 + 写终态指示 + 恢复 btn_stop(如 INPUTING)│
    +       └──────────────────┬───────────────────┘ ← 第一次 update(同步)
                               │
                               ▼ (resolveApprovalOverGateway 异步返回)
            ┌─────────────────────────────────────┐
    @@ -884,24 +987,33 @@ 

    6.4 上游 resolve 后的卡片状态同步(v3:单卡)

    │ 触发 ▼ presentation.buildResolvedResult({ view, entry }) - returns { kind: "update", payload: { - phase: "resolved", - decision: "allow-once", - // 可附带 resolvedBy 显示给用户 - }} + returns { kind:"update", payload:{ + phase:"resolved", decision:"allow-once", + resolverDisplayName: view.resolvedBy, + }} │ ▼ - transport.updateEntry({ cfg, accountId, entry, payload, phase }) - → PUT /v1.0/card/instances ← 第二次 update(幂等,与第一次结果相同) - → status="✅ 已批准 by @staffA (allow-once)", hasAction=false + transport.updateEntry:entry.mode === "card" 分支 + → approval-card-patcher.applyResolvedPatch(entry.outTrackId, + payload.decision, payload.resolverDisplayName, token) + → 与 callback handler 的 step 5 内容一致,幂等覆盖 OK + + agent reply card 看起来:approval 按钮消失,多一行"✅ 已批准 by @staffA · allow-once", + btn_stop 恢复显示,agent 继续流式输出(如果 decision != deny) + + agent reply card 自己的状态机不变(PROCESSING/INPUTING/FINISHED 由 + agent reply 流程控制,approval 只 patch cardParamMap 字段)
    + +

    mode === "markdown":v1 不发新通知消息

    +
    transport.updateEntry:entry.mode === "markdown" 分支
    +  └─ no-op(markdown 消息不能 edit,发新通知消息会刷屏)
     
    -       两次 update 内容一致,无视觉差异;幂等覆盖 OK。
    -       core 在 phase=resolved 后从 activeEntries 移除该 entry。
    +用户从命令成功的自然反馈感知(agent 继续/停止;approval 状态在上游同步可见)
     
    -———————————————————————————————————————————————————————————————————
    +v2 future 可视用户反馈再加 "✅ 已批准 by @staffA" 的轻量回执消息
    -v2 future(DM 投递启用后):core 会对**所有 entry**(origin + 每个 DM) -都调 updateEntry,所有卡片同步刷成相同终态。
    +

    v2 future(DM 投递启用后)

    +

    core 会对所有 entry(origin + 每个 DM)调 updateEntry,所有卡片同步刷成相同终态。当前 v1 origin-only 每个 approval 只有 1 个 entry。

    6.5 用户点 deny / allow-always 的差异

    @@ -957,39 +1069,35 @@

    Channel stopClient(账号停用 / gateway 重启)—— v3 推迟到 v2< v2 future:若 SDK 暴露 activeEntries 查询 API,或 channel 引入轻量 outTrackId Set(仅供 stop-time 清理用,非完整 entry store),再实现 finalize。 -

    6.7 createAndDeliver 失败 → markdown 兜底(error-recovery)

    -
    deliverPending(target)
    -  ├─ card-service.createAndDeliver(...)
    +        

    6.7 失败处理(v3.3 重写:两路由各自的失败模式)

    + +

    card 路径失败

    +
    approval-card-patcher.applyPendingPatch(outTrackId, ...)
    +  ├─ PUT updateCardVariables → 钉钉 API 5xx / 网络 / agent card 已 FINISHED 不可改
       │
    -  ├─ 成功 → return entry(D18:不写本地 store;core 自动缓存到 activeEntries)
    +  └─ 内部 catch + WARN 日志 + return null
    +       core 看 null → 不再触发 updateEntry → 该 approval 在 channel 端无视觉反馈
    +       用户可用 /approve 命令兜底
    +
    +注意:card 路径失败时自动降级到 markdown 路径——这样会导致 1 个
    +approval 投出 2 条消息(按钮 patch 残留 + 新 markdown 消息),UX 混乱。
    +若需"双保险"策略,应在 prepareTarget 层就明确路由(已经是这样)。
    + +

    markdown 路径失败

    +
    sendProactiveTextOrMarkdown(target, markdownText, ...)
    +  ├─ 钉钉 API 5xx / 工作通知权限不足 / target 不可达
       │
    -  └─ 失败(网络 / API 4xx/5xx / 模板未发布)
    -       │
    -       ├─ 内部 log.warn (注意:不调 observe.onDeliveryError——那是 runtime
    -       │    捕获 throw 后才会触发的钩子;本设计选择 transport 自闭环 catch
    -       │    + fallback + return null,runtime 不会再走 observe)
    -       │
    -       ├─ approval-fallback-render.buildMarkdownFallback(request)
    -       │  →  内容包含:
    -       │     ### ⚠️ 需要审批:<exec command / plugin tool 简介>
    -       │     **ID**: `abc123`
    -       │     **过期时间**: 10 分钟
    -       │
    -       │     批准(仅一次):`/approve abc123 allow-once`
    -       │     批准(总是):`/approve abc123 allow-always`
    -       │     拒绝:`/approve abc123 deny`
    -       │
    -       ├─ sendProactiveTextOrMarkdown(target, markdownText, ...)
    -       │
    -       └─ return null(让 core 知道 deliverPending 未成功)
    -            v3 不写 fallback entry——无本地 store;
    -            上游 core 看 null 即认为该 target 投递失败,
    -            不会再触发后续 updateEntry。
    -            用户可用 markdown 消息里的 /approve 命令完成审批。
    + └─ 内部 catch + WARN 日志 + return null + 同上:core 看 null 不触发后续;approval 在 channel 端无视觉反馈 + 此场景下用户可能完全感知不到 approval 存在 → 仅靠 CLI/WebUI 日志
    - 兜底路径的设计取舍 - markdown 消息一旦发出无法编辑(钉钉机器人 API 无 edit)。所以兜底卡片的"终态同步"做不到——expire / resolve 时不会发新消息覆盖原 markdown。这是已知降级;接受理由:(1) createAndDeliver 失败本身就是异常通路;(2) 不再追发消息免得用户被刷屏;(3) 用户用 /approve 命令照样能完成审批。 + v3.3 失败模式的设计取舍 +
      +
    • card 失败不降级 markdown:避免双消息。需要"双保险"留给 v2 future 决策(可能引入 retry + fallback chain)
    • +
    • markdown 失败不降级到其它通道:同样避免双消息;本来 markdown 已经是"最低保证"路径,再降无意义
    • +
    • 两路径共同的接受前提:approval 投递失败是异常通路。生产环境应通过 OpenClaw 端 approval 监控发现(approval 长时间 pending 触发告警)
    • +

    6.8 /approve 命令必须早期 intercept(D2 落地)

    @@ -1107,25 +1215,42 @@

    为什么 D2 不能"纯复用上游 /approve dispatcher"

    7. 审批卡片设计

    -

    7.1 模板字段约定(approval-card-template-v1)

    +

    7.1 复用 AI Card v2 模板的字段映射(v3.3 D9)

    +

    v3.3 不再设计独立 approval 模板——card 路径直接复用 PR #480 已合并的 AI Card v2 模板。approval 渲染只是在 agent reply card 上做 cardParamMap 字段级 patch:

    - + - - - - - - - - - + + + + + + + + + + + + + + +
    变量 key类型语义
    AI Card v2 现有字段approval 用途pending → resolved/expired 变化
    kindBadgeString"⚙️ 命令执行" | "🔌 插件调用"
    titleStringe.g. "Agent 请求批准执行命令"
    bodyMarkdownString正文,markdown 渲染(含命令 preview / 工具描述)
    detailRowsloopArray"cwd: /tmp"、"severity: high" 等 metadata,逐行渲染
    severityBadgeString"🟢 low" | "🟡 medium" | "🔴 high"
    approvalIdString显示用,与按钮 actionId 内 ID 一致
    expiryHintString"⏰ <X> 分钟后过期"
    buttonGroupVisibleBoolean控制三按钮可见性(终态 = false)
    statusFooterString终态条:"✅ 已批准…" / "⛔ 已拒绝…" / "⏰ 已过期" / "❌ 已取消"
    btns(JSON-stringified CardBtn[])承载 approval 3 按钮 + btn_stop 共存策略pending:写入 3 个 approval 按钮(隐藏 btn_stop,D23)
    resolved:恢复 btn_stop(若 card 活跃)
    expired:恢复 btn_stop(若 card 活跃)或清空
    hasAction("true"/"false")控制按钮区可见性pending:"true"
    resolved/expired:若恢复 btn_stop 则 "true";否则 "false"
    approval 终态指示位(PR-2 实施时决定,见下方说明)显示"✅ 已批准 by @user · allow-once" / "ℹ️ 已处理或已过期"pending 不写;resolved/expired 写入对应文案
    +
    + "approval 终态指示位"的实施候选(PR-2 决定) +
      +
    • A. append 到 contentKey 末尾:"...<agent reply>\n\n---\n✅ 已批准 by @user · allow-once"。优点:零模板改动;缺点:与 agent 内容混在一起,视觉权重不分
    • +
    • B. 用 blockListKey 加 system block(若 AI Card v2 模板支持):cleaner UX;缺点:需确认模板的 block type 列表是否含 system/notification 类型
    • +
    • C. 新增 approvalStatus 变量:最干净的语义;缺点:需要模板侧轻微改动(导入 + 重新发布 v2 模板)。考虑做成"模板可选支持,缺失时回落到 A"
    • +
    + PR-2 阶段确认 AI Card v2 模板能力后选定方案;当前 spec 不锁死具体字段以免与未来 main 状态错位。 +
    +

    7.2 三按钮配置(v2 修订:cardPrivateData 结构化 - D15 落地)

    按钮通过 PR #480 引入的 CardBtn[] 模板字段渲染。三按钮共享 actionId "approval",区分靠 params.d

    -
    // approval-card-render.ts 内部
    +        
    // approval-card-patcher.ts 内部(v3.3)
     function makeApprovalBtns(approvalId: string): CardBtn[] {
       return [
         { text: "✅ 允许一次", color: "green",  status: "normal",
    @@ -1150,18 +1275,40 @@ 

    7.2 三按钮配置(v2 修订:cardPrivateData 结构化 - D15 落地)< 回调解析使用 startsWith("approval") + params.d 取 decision 即可,详见 §1.2 与 §6.3。

    -

    7.3 状态机

    -
               ┌─ user click allow-once  ──► RESOLVED (allow-once)
    +        

    7.3 双状态机并存(v3.3)

    +
    approval lifecycle(由 OpenClaw core 驱动):
    +           ┌─ user click allow-once  ──► RESOLVED (allow-once)
                ├─ user click allow-always ──► RESOLVED (allow-always)
                ├─ user click deny         ──► RESOLVED (deny)
     PENDING ───┤
    -           ├─ upstream expired event  ──► EXPIRED
    -           ├─ channel stopClient      ──► CANCELED
    -           └─ upstream canceled event ──► CANCELED
    +           ├─ /approve 文本命令      ──► RESOLVED (按命令 decision)
    +           ├─ upstream expired event ──► EXPIRED
    +           └─ upstream canceled event ──► CANCELED (v1 仅核 cancel;channel stop 不主动 finalize)
    +
    +──────────────────────────────────────────────────────
    +
    +agent reply card lifecycle(由 reply-strategy-card 驱动):
    +PROCESSING ──► INPUTING ──► FINISHED / STOPPED / FAILED
    +                  │
    +                  └─ 期间可被 approval 临时 patch(隐藏 btn_stop / 注入 3 按钮)
     
    -(终态:buttonGroupVisible=false; statusFooter 文案对应)
    +────────────────────────────────────────────────────── + +两状态机的协同(D22 关键不变量): +• approval 只对 cardParamMap 字段做 patch,不触碰 card 的 state +• card 自然走到 FINISHED 后,approval 终态指示仍保留在卡片正文 +• card 进入 STOPPED(用户点 btn_stop 后)会 final 化,approval 若还 pending + 会孤儿——v1 已知降级,v2 future 可考虑 deny propagate
    + +

    v3.3 mockup 说明(重要)

    +
    + 下方 §7.4-7.8 mockup 是 v3.2 状态的"独立审批卡片"渲染样式。 + v3.3 起 approval 按钮挂在 agent reply card 上——视觉上 mockup 应该是 agent reply 内容(model 名、流式 markdown、blockList 思考/工具/图片块、taskInfo footer)**底部新增 3 个按钮区**;不是独立卡片。 + PR-2 实施时按真实 AI Card v2 模板渲染做新 mockup 截图入 docs/artifacts/。 + 当前 mockup 仍可用于理解按钮文案 + 终态指示文案。 +
    -

    7.4 Mockup(pending · exec approval · 群聊)

    +

    7.4 Mockup(pending · exec approval · 群聊 · v3.2 旧版示意)

    #OpenClaw 工作群
    @@ -1315,7 +1462,7 @@

    8. 错误处理矩阵

    createAndDeliver 网络/HTTP 失败 钉钉 API 5xx、超时、429 - transport 自闭环:deliverPending 内部 catch + WARN 日志 + 调 approval-fallback-render 发 markdown 兜底 + return null(D5 §5.3)。调 observe.onDeliveryError——那是 runtime 捕获 throw 才触发的钩子,本设计选 transport 内部处理就不能同时让 runtime observe 介入 + v3.3 transport 自闭环按 route 分支:card 路径下 PUT 失败 → 内部 WARN log + return null(不降级到 markdown,避免双消息,详见 §6.7);markdown 路径下 sendProactiveTextOrMarkdown 失败同样 internal log + return null。调 observe.onDeliveryError——那是 runtime 捕获 throw 才触发的钩子,本设计选 transport 内部处理就不能同时让 runtime observe 介入 收到 markdown 消息,含 /approve 命令模板 @@ -1379,7 +1526,7 @@

    9.1 测试文件布局

    文件覆盖目标预计 case 数 tests/unit/approval-config.test.tsschema 解析、normalize、enabled=auto、fallback chain~12 - tests/unit/approval-card-template.test.tscardParamMap 构造(含 stringify 规则)~8 + tests/unit/approval-card-render.test.tspending/resolved/expired/canceled × exec/plugin = 8 个矩阵~16 tests/unit/approval-target-resolver.test.tsorigin 解析(含 turnSourceChannel=null)、DM 列表构造~10 tests/unit/approval-resolver.test.ts
    ★ v3.2D20/D21 核心:kind 推导 4 情况(plugin: 前缀 / 无前缀 + 两边授权 / 仅 plugin 授权 / 仅 exec 授权)、未授权返 unauthorized、resolveApprovalOverGateway 调用参数(含 resolveMethod 与 allowPluginFallback)、错误分类(already-resolved / not-found / gateway-error);mock SDK gateway~18 @@ -1388,7 +1535,9 @@

    9.1 测试文件布局

    tests/unit/card-callback-service.test.ts(扩展既有)D16 改动:analyzeCardCallbackcardPrivateData 含 actionIds + params;既有 feedback / btn_stop 用例不受影响+6 tests/unit/approval-command-intercept.test.tsparser 命中 → 调 resolver;未授权 → sendProactiveTextOrMarkdown(mock);resolve-failed 仅 log;非 /approve 命令 return false;不重复测 parser / resolver 内部~8 tests/unit/inbound-handler-approve-intercept.test.ts§6.8 在 inbound-handler 的接入:群里带 @mention 前缀、私聊、return 后不进 reply 派发(验证 session lock 不被触发)~8 - tests/unit/approval-fallback-render.test.tsmarkdown 兜底文案、exec/plugin 分支~6 + tests/unit/approval-card-locator.test.ts
    ★ v3.3 新增D22 核心:active card 命中(PROCESSING/INPUTING)返 entry;FINISHED/STOPPED/FAILED 返 null;无 entry 返 null;mock card-run-registry~8 + tests/unit/approval-card-patcher.test.ts
    ★ v3.3 替代 card-render.testapplyPendingPatch(注入 3 按钮 + 隐藏 btn_stop D23)、applyResolvedPatch(清按钮 + 写终态指示 + 恢复 btn_stop)、applyExpiredPatch;mock updateCardVariables~14 + tests/unit/approval-markdown-render.test.ts
    ★ v3.3 替代 fallback-render.testbuildExecApprovalMarkdown / buildPluginApprovalMarkdown:含 /approve 三种 decision 命令模板、过期 hint、id 显示~8 tests/unit/approval-capability.test.tsSDK 工厂参数装配正确、capability 单例~6 tests/unit/approval-native-runtime.test.ts4 子 adapter(availability/presentation/transport/observe)集成 mock;含 v1 origin-only 路径~14 tests/integration/approval-end-to-end.test.ts模拟 createApprovalRequest → 投递(仅 origin)→ 点击 → resolve 回写 → 卡片刷新;含 self-approval、multi-approver 竞争点击、非 approver 拒绝~10 @@ -1444,14 +1593,15 @@

    阶段 0 · 前置依赖(必须先满足)

  • 验证 tsconfig.json path 解析顺序——开发期 monorepo / linked checkout 场景可临时调高 ../openclaw/src/plugin-sdk 在 paths 中的优先级做 unblock,但 release 前必须切回 lockfile 路径以保证发布包正确
  • 本仓库 PR #480 已合并(MERGED;2026-05-18 核实):AI Card v2 模板的 CardBtn[]sendCardRequest 回调格式已在当前 main 可用。实施时需复用 / 确认当前 main 上的 CardBtn 类型与 sendCardRequest 契约(路径与字段命名)与本 spec §1.2 / §7.2 一致;如有偏差以当前 main 为准并回头修订 spec
  • -
  • approval-card 模板上传:在 open-dev.dingtalk.com/fe/card 导入 docs/assets/card-template-approval-v1.json,发布为预置统一模板,拿到 templateId(用于阶段 2 替换占位常量)
  • +
  • ~~approval-card 模板上传~~(v3.3 删除):D9 v3.3 不再新建 approval 模板。替代任务:阶段 1 实施前确认当前 main 上 AI Card v2 模板的字段是否满足 §7.1 候选 A/B/C 之一(btns 必须;approval 终态指示位用 contentKey append / blockList system block / 新增 approvalStatus 三选一)。若需轻微改动 v2 模板,按 PR #480 既有流程处理
  • 阶段 1 · 接口骨架 + 统一 resolver + 命令链路(PR-1)

      -
    • 新增 src/approval/ 目录骨架(v3.2:9 个文件)
    • +
    • 新增 src/approval/ 目录骨架(v3.3:10 个文件,含 card-locator)
    • 实现核心收敛点(D20):approval-resolver.tsapproval-command-parser.ts
    • +
    • 实现 v3.3 新增的 approval-card-locator.ts(D22 落地)—— PR-1 就要做,因为 markdown 路径下 locator 也会被调(返回 null 是触发 markdown 的信号)
    • 实现支撑模块:approval-config.tsapproval-target-resolver.ts(仅 origin)、approval-capability.ts(不含 nativeRuntime 完整实现)、approval-command-intercept.ts(薄壳,调 parser + resolver)
    • src/channel.ts 挂上 approvalCapabilitynativeRuntime 暂留 undefined;capability 仅生效 authorizeActorAction / resolveApproveCommandBehavior 等权限部分
    • src/inbound-handler.ts/approve 早期 intercept(§6.8,D2 落地)—— PR-1 就要做,因为这是 Feishu-同档体验的最后一公里
    • @@ -1463,13 +1613,14 @@

      阶段 1 · 接口骨架 + 统一 resolver + 命令链路(PR-1)

      阶段 2 · 完整 native runtime(PR-2)

        -
      • 实现 approval-card-template.ts(const 默认 + env 覆盖,DINGTALK_APPROVAL_CARD_TEMPLATE_ID)、approval-card-render.ts(按 §7.2 出 cardPrivateData 结构化 btns)、approval-fallback-render.ts
      • -
      • 实现 approval-callback-handler.ts(用 parseApprovalFromCardPrivateData → 调阶段 1 已有的 approval-resolver.resolveApproval——零新增权限逻辑)、approval-native-runtime.ts(4 子 adapter;interactions 不实现)
      • +
      • 实现 v3.3 渲染层:approval-card-patcher.ts(D22 落地:在原 agent card 上 patch 按钮 / 终态指示)+ approval-markdown-render.ts(markdown 路径主路径)
      • +
      • 实现 approval-callback-handler.ts(用 parseApprovalFromCardPrivateData → 调阶段 1 已有的 approval-resolver.resolveApproval——零新增权限逻辑;resolved 后调 approval-card-patcher.applyResolvedPatch 更新原 agent card)、approval-native-runtime.ts(4 子 adapter;interactions 不实现;transport 内部按 preparedTarget.route 分支调 patcher 或 markdown-render)
      • 修改 src/card-callback-service.ts(D16):CardCallbackAnalysiscardPrivateData 字段,analyzeCardCallback 抽 params 并附到 analysis
      • -
      • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支(在 feedback / btn_stop 之前)
      • +
      • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支(在 feedback / btn_stop 之前;callback 命中后通过 outTrackId 找到原 agent card 调 patcher)
      • 在阶段 1 的 capability 里挂上 nativeRuntime(4 子 adapter;v1 不实现 interactions)
      • -
      • 测试:剩余 4 个 unit 文件 + 1 个 integration 文件;新增 callback-handler 与 cardPrivateData 解析的 test(基于 §1.2 的真机回调样本作 fixture);resolver 的 unit test 在 PR-1 已完成,PR-2 仅加 callback 入口集成测试覆盖"按钮 → resolver"链路
      • -
      • 这阶段交付后:完整三按钮卡片 UX 在真机可用——templateId 已为正式发布版;按钮回调与命令 intercept 走同一 resolver 行为一致;可走真机回归(参照 skills/dingtalk-real-device-testing/SKILL.md
      • +
      • 测试:approval-card-patcher(核心:按钮注入 + 终态 patch + btn_stop 恢复策略 D23)+ approval-markdown-render(文案 + alias) + callback-handler(cardPrivateData 解析 → resolver → patcher);resolver 的 unit test 在 PR-1 已完成,PR-2 仅加 callback 入口集成测试覆盖"按钮 → resolver → patcher"链路 + agent card 状态正确同步
      • +
      • 真机回归(参照 skills/dingtalk-real-device-testing/SKILL.md):重点验证 agent reply card 中途出现 approval 按钮 + 点按钮后按钮消失 + agent 继续 stream + 终态指示正确写入
      • +
      • 这阶段交付后:完整 v3.3 双路由 UX 在真机可用

      阶段 3 · 用户文档与回归收尾(PR-3)

      From ed463e889adfdf61cac62620e93e9eb6ecfc862f Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Mon, 18 May 2026 23:20:04 +0800 Subject: [PATCH 07/44] =?UTF-8?q?docs(spec):=20v3.4=20=E4=BF=AE=E8=AE=A2?= =?UTF-8?q?=20=E2=80=94=20=E7=AC=AC=E4=BA=94=E8=BD=AE=20review=EF=BC=8C?= =?UTF-8?q?=E5=A4=B1=E8=B4=A5=E9=99=8D=E7=BA=A7=20+=206=20=E5=A4=84?= =?UTF-8?q?=E5=87=86=E7=A1=AE=E6=80=A7=20fix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 7 处修订(核心是失败降级策略): 1. 【失败降级策略修订】card patch 明确失败时降级到 markdown,而非 v3.3 的 "静默 pending"。理由:v3.3 选择不降级会导致用户连 approval id 都看不到 无法用 /approve 命令兜底,等于双重失败。分两种亚态: - 明确失败(HTTP 4xx/5xx 已收到 / 模板字段不支持 / agent card 已 FINISHED) → 降级 markdown,entry.mode 最终为 "markdown" - 模糊失败(请求超时但可能已成功 / 网络中断)→ return null 不重发,避免 "实际成功 + 重发" 双消息 markdown 路径失败不再降级(本来就是最低保障) 2. 错误矩阵 / §6.7 / §9.3 多处 createAndDeliver 失败 fallback / approval-card 模板未发布等过时表述统一清理,对齐 v3.3 删模板 + v3.4 降级策略 3. card-run-registry 当前无 sessionKey 查询 API(核实 src/card/card-run-registry.ts:91-145 只有 outTrackId / conversation / owner 查询)。spec 明确新增 resolveActiveCardRunBySession(accountId, sessionKey),含 state ∈ {PROCESSING, INPUTING} 过滤。§3.3 接触面表加 card-run-registry 改动面。 4. /approve alias 补全。核实上游 openclaw/src/auto-reply/reply/commands-approve.ts:19-30 实际 10 个 alias (v3.1 漏列 allowonce 和 allowalways 两个无连字符形式)。channel 端 regex 完整支持 10 个 alias × 2 种顺序 = 20 个合法 case。测试 case 数 16 → 20; CI 断言改为"上游 alias 集合 = channel 端 regex 覆盖集"强相等。 5. 删除 resolveApproveCommandBehavior 字段提及。核实上游工厂 createApproverRestrictedNativeApprovalCapability(openclaw/src/plugin-sdk/ approval-delivery-helpers.ts:30)参数集不接受该字段;且 channel 端 /approve 走早期 intercept 不依赖上游 dispatcher,本字段无意义。 6. CardBtn 类型来源修订。核实当前 main 的 src/card/card-template.ts:12 仅暴露 template key 常量,PR #480 引入了 ButtonGroup 资产但没暴露 TS 类型。 approval 模块内自定义 CardBtn 接口(text/color/status/event 四字段), PR-2 实施时附 fixture 验证 v2 模板 btns key 契约。 7. §1.1 / §3.3 等多处 v3.2 残留同步: - 删"不为 markdown 模式做主路径文案"条目(v3.3 起 markdown 就是主路径) - reply-strategy 关系澄清:文件本身仍不动,但 approval card patch 会通过 card-run-registry 找到 reply-strategy-card 创建的 active card 各章节修订面:决策表无新增 D;§1.1 删一条;§3.2 模块表 card-locator/patcher 描述补 card-run-registry API 依赖 + CardBtn 类型来源说明;§3.3 加 card-run- registry 改动面;§4.1 通过 §10 PR-1 条目澄清 resolveApproveCommandBehavior 不需要;§5.3 deliverPending 加 card 失败降级分支;§6.7 重写失败处理;§8 错误 矩阵 createAndDeliver/markdown 分行重列;§9.3 测试场景换成"明确失败降级 + 模糊失败不降级"两个对照 case。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 133 ++++++++++++------ 1 file changed, 89 insertions(+), 44 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index f0f7ca09..2b9091d7 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -53,7 +53,7 @@

      1.1 v1 范围明确不做的事

    • 不做停机 finalize-on-stop(D13 v3 推迟)——D18 删除 store 后失去枚举能力;遗留卡片在用户点击时降级为"已过期/已关闭"
    • 不抽通用 action dispatcher / registry —— peer 三家都没做,approval 走自己的回调前缀分支,与 feedback_up/down / btn_stop 在 TOPIC_CARD listener 中同级并列
    • 不动 btn_stop 与 feedback 既有路径(向后兼容,零回归风险)
    • -
    • 不为 markdown 模式做"教用户打 /approve 命令"的主路径文案——卡片是唯一推广 UX
    • +
    • v3.3 修订删除原条目"不为 markdown 模式做主路径文案"——v3.3 起 markdown 路径就是主路径之一,必须教用户用 /approve 命令完成审批
    • 不做重启后主动 rebind pending approval(v1 范围;用户点过期/失效按钮时显式降级提示)
    • 不引入 select / input / datepicker 等高级组件——仅 button(已 CONFIRMED 平台支持)
    @@ -138,7 +138,7 @@

    2. 已确认的决策清单

    D2 Slash 命令 - 命令字面量与上游一致(/approve <id> <decision>),但 channel 端必须在 inbound-handler 入口早期 intercept,直接调 resolveApprovalOverGateway——不走正常 inbound dispatch 路径
    原因(v1 实施踩坑确认):Plugin Approval 的 waitDecision 阻塞在 dispatchReply 内并持有 DingTalk 自己的 session lock,若 /approve 走 normal pipeline 会 session lock 死锁,120s 超时失败。
    权限校验必须由 channel 自行执行capability.authorizeActorAction),resolveApprovalOverGateway 只连 gateway 不校验 approver(Issue 1 修订)。
    decision alias v3.1 显式支持范围:allow-once|once|allow → allow-once;allow-always|always → allow-always;deny|reject|block → deny;同时接两种顺序 /approve <id> <decision>/approve <decision> <id>。若上游引入新 alias 或新顺序需同步更新(§11.2 风险表登记) + 命令字面量与上游一致(/approve <id> <decision>),但 channel 端必须在 inbound-handler 入口早期 intercept,直接调 resolveApprovalOverGateway——不走正常 inbound dispatch 路径
    原因(v1 实施踩坑确认):Plugin Approval 的 waitDecision 阻塞在 dispatchReply 内并持有 DingTalk 自己的 session lock,若 /approve 走 normal pipeline 会 session lock 死锁,120s 超时失败。
    权限校验必须由 channel 自行执行capability.authorizeActorAction),resolveApprovalOverGateway 只连 gateway 不校验 approver(Issue 1 修订)。
    decision alias v3.4 修订:上游 openclaw/src/auto-reply/reply/commands-approve.ts:19-30 实际共 10 个
    allow / once / allow-once / allowonce → allow-oncealways / allow-always / allowalways → allow-alwaysdeny / reject / block → deny(v3.1 漏列了 allowonceallowalways 两个无连字符形式)。channel 端 regex 必须完整支持这 10 个 alias × 两种顺序 = 20 个合法形式。若上游新增 alias / 顺序需同步更新(§11.2 风险表登记) 对齐 PR #489 揭示的运行时约束 @@ -316,6 +316,14 @@

    2. 已确认的决策清单

    (6) §10 PR-1 措辞改"resolve 通道生效,approval id 可见性依赖外部界面,完整 UX 在 PR-2"; (7) §6.7 删除 store.register 残留; (8) §6.3 / §6.6 / §8 / §9 等所有 already-resolved 文案改"ℹ️ 已处理或已过期"并明确 catch 后 return 避免覆盖;§6.8 alias 范围显式列清;§11.2 风险表新增 kind 派发边界条目 +
  • v3.4(2026-05-18 第五轮 review):7 处修订—— + (1) 失败降级策略修订:card patch 明确失败时降级到 markdown(v3.3 错误地选"静默 pending",用户连 approval id 都看不到无法兜底 /approve);模糊失败(超时)仍 return null 避免双消息; + (2) 错误矩阵 / §6.7 / §10 等多处 createAndDeliver 失败 fallback / approval-card 模板未发布等过时表述统一清理; + (3) card-run-registry 当前无 sessionKey 查询 API,spec 明确新增 resolveActiveCardRunBySession(accountId, sessionKey)src/card/card-run-registry.ts),含 state 过滤;§3.3 接触面表加该 file; + (4) /approve alias 补全至 10 个(v3.1 漏列 allowonceallowalways 两个无连字符形式);测试 case 数 16 → 20;CI 断言改为"上游 = channel 覆盖集"强相等; + (5) 删除 resolveApproveCommandBehavior 字段提及——上游工厂 createApproverRestrictedNativeApprovalCapability 参数集不接受该字段(openclaw/src/plugin-sdk/approval-delivery-helpers.ts:30),且 channel 端 /approve 早期 intercept 不依赖 dispatcher 行为定制; + (6) CardBtn 类型来源修订——当前 main 未导出 CardBtn TS 类型(src/card/card-template.ts:12 仅 template key 常量;PR #480 引入资产但没暴露类型);approval 模块内自定义 CardBtn 接口,PR-2 实施时附 fixture 验证 v2 模板 btns key 契约; + (7) §1.1 / §3.3 等多处 v3.2 残留同步(删"不为 markdown 做主路径文案";reply-strategy 关系澄清)
  • v3.3(2026-05-18 第四轮 review,架构 pivot):用户提出"卡片模式下不要新建独立 approval 卡片,挂到原 agent reply card 上;markdown 模式走 /approve 文字命令"。修订面: (1) 删除 D9 新模板创建/上传,复用 PR #480 已合并的 AI Card v2 + CardBtn[]; (2) D10 路由策略改为按 card-run-registry 实际状态(不读 messageType,因 runtime 降级不反映); @@ -433,9 +441,9 @@

    3.2 模块单一职责表

    approval-card-locator.ts
    ★ v3.3 新增(D22 落地核心) 导出 findActiveAgentCard({ cfg, accountId, sessionKey }):按 sessionKey 查 card-run-registry,仅在 entry 存在且 entry.card.state ∈ {PROCESSING, INPUTING} 时返回 { outTrackId, sessionKey };否则返回 null(caller 走 markdown 路径)。 -
    关键边界:entry.state 必须在 active 集合内,避免对 FINISHED/STOPPED/FAILED card 错误注入按钮 - card-run-registry(既有模块) - ~60 +
    v3.4 关键依赖:card-run-registry 必须新增 sessionKey 查询 API——当前源码只导出 resolveCardRun(outTrackId) / resolveCardRunByConversation / resolveCardRunByOwnersrc/card/card-run-registry.ts:91-145),无 by-sessionKey 查询。需在 src/card/card-run-registry.ts 新增 resolveActiveCardRunBySession(accountId: string, sessionKey: string): CardRunRecord | null——遍历 records Map,按 accountId + sessionKey 精确匹配且 state ∈ active 集合。Record 已有 sessionKey 字段(card-run-registry.ts:16),实现仅是新增一个 export。不要让 locator 依赖 conversation contains 模糊匹配或私有 Map 访问 + card-run-registry(既有模块;需新增 export) + ~60(+ card-run-registry 内 ~20 行新 API) approval-card-patcher.ts
    ★ v3.3 替代 card-template + card-render @@ -444,7 +452,9 @@

    3.2 模块单一职责表


    applyPendingPatch(outTrackId, approvalId, token):PUT 设置 btns=approvalButtons, hasAction="true"(隐藏 btn_stop,D23);
    applyResolvedPatch(outTrackId, decision, resolverDisplayName, token):PUT 清按钮 + 写"✅ 已批准 by @user · <decision>" 到 agent card 的某变量位(具体位置 PR-2 实施时决定)+ 若 card 还活则恢复 btn_stop;
    applyExpiredPatch(outTrackId, token):PUT 清按钮 + 写"ℹ️ 已处理或已过期" + 恢复 btn_stop(如适用) - card-callback-service.updateCardVariables, AI Card v2 CardBtn 类型 + card-callback-service.updateCardVariables;approval 模块内自定义的 CardBtn 类型(v3.4:当前 main 未导出 CardBtn 类型,approval 模块自定义) + + 关于 CardBtn 类型来源(v3.4 修订):核实当前 main 后,src/card/card-template.ts:12 仅暴露 template key 常量;PR #480 引入了 AI Card v2 ButtonGroup 资产但没暴露 TS 类型。所以 approval-card-patcher.ts 内部自定义 CardBtn 接口(含 text/color/status/event 四字段),并在 patcher 测试里加 fixture 验证:"btns" key 在当前 v2 模板有 String 类型 cardParamMap 字段 ~150 @@ -472,8 +482,11 @@

    3.2 模块单一职责表

    approval-command-parser.ts
    ★ v3.2 新增 - 纯解析(无副作用)。导出 parseApproveCommand(text: string): { approvalId, decision } | null:支持 /approve <id> <decision>/approve <decision> <id> 两种顺序 + 8 个 alias(allow-once / once / allow / allow-always / always / deny / reject / block)。 -
    测试以上游 openclaw/src/auto-reply/reply/commands-approve.ts 的 alias 集合做对照,确保跨 channel 体验一致 + 纯解析(无副作用)。导出 parseApproveCommand(text: string): { approvalId, decision } | null:支持 /approve <id> <decision>/approve <decision> <id> 两种顺序 + 10 个 aliasopenclaw/src/auto-reply/reply/commands-approve.ts:19-30): +
    • allow-once 类:allow / once / allow-once / allowonce(4 个) +
    • allow-always 类:always / allow-always / allowalways(3 个) +
    • deny 类:deny / reject / block(3 个) +
    测试以上游 alias 集合做对照 + CI 断言上游 alias 数等于 channel 端 regex 覆盖数,跨 channel 体验一致 —(纯函数) ~60 @@ -531,9 +544,14 @@

    3.3 与现有代码的接触面

    v3.3 不修改——D9 v3.3 已废弃新建 approval 模板,复用现有 AI Card v2 模板字段 — + + src/card/card-run-registry.ts
    (v3.4 新增改动面) + 新增 export function resolveActiveCardRunBySession(accountId: string, sessionKey: string): CardRunRecord | null——遍历 records Map(小,TTL 30min;scan 性能可接受)按 accountId + sessionKey 精确匹配且 record.card.state ∈ {PROCESSING, INPUTING} 过滤。不要修改 sweep 行为(避免影响 btn_stop 路径) + 低(追加 export,不动既有 API;新 API 仅供 approval-card-locator 使用) + src/types.ts - 新增 ApprovalCardEntry / ApprovalDecision / ApprovalPhase / CardBtn 类型(CardBtn 来自 PR #480) + 新增 ApprovalEntry / ApprovalDecision / ApprovalPhase 类型;同时 在 approval 模块内定义 CardBtn 类型(v3.4 修订:当前 main 未导出 CardBtn 类型——src/card/card-template.ts:12 仅暴露 template key 常量。PR #480 引入了 ButtonGroup 资产但没暴露 TS 类型)。approval 模块自定义 CardBtn 接口反映 sendCardRequest 契约(text/color/status/event 四字段);并在 approval-card-patcher 测试里加"btns key 是 AI Card v2 模板约定字段"的 fixture 验证 极低 @@ -559,7 +577,7 @@

    3.3 与现有代码的接触面

    • src/card/card-action-handler.ts(btn_stop 不动)
    • src/feedback-learning-service.ts(与 approval 完全不交叉)
    • -
    • 所有 src/reply-strategy-*(approval 不进 agent reply 路径,独立卡片)
    • +
    • 所有 src/reply-strategy-*(v3.3 修订后仍 不修改 reply-strategy——但请注意 approval 与 reply-strategy 不再是完全无关:approval card 路径会通过 card-run-registry 找到 reply-strategy-card 创建的 active card,并对它做字段级 patch。reply-strategy 文件本身仍不动)
  • @@ -703,11 +721,18 @@

    5.3 transport

    deliverPending({ cfg, accountId, preparedTarget, request, pendingPayload }) - v3.3 修订:按 preparedTarget.route 分两条路径。 -
    route="card":调 approval-card-patcher.applyPendingPatch(activeCardOutTrackId, request.id, token)——PUT updateCardVariables 注入 3 个 approval 按钮到现有 agent card(D22)+ 隐藏 btn_stop(D23)。成功返回 entry = { approvalId, accountId, mode: "card", outTrackId: activeCardOutTrackId }。 -
    route="markdown":调 approval-markdown-render 构造 markdown 文本(含 /approve 命令模板),sendProactiveTextOrMarkdown(config, target.to, text, ...) 发独立消息。成功返回 entry = { approvalId, accountId, mode: "markdown" }(无 outTrackId)。 -
    失败(两路径通用):transport 内部 catch + WARN log + return null。在 catch 内调 observe.onDeliveryError(runtime 契约不允许 transport 自己触发 observe)。 -
    注:card 路径失败时降级到 markdown 路径——core 看 null 即标 entry 失败,避免一次 approval 投出两条消息 + v3.4 修订:按 preparedTarget.route 分两条路径 + card 失败时降级 markdown。 +
    route="card": +
    (1) 调 approval-card-patcher.applyPendingPatch(activeCardOutTrackId, request.id, token)——PUT updateCardVariables 注入 3 个 approval 按钮到现有 agent card(D22)+ 隐藏 btn_stop(D23)。 +
    (2) 成功 → 返回 entry = { approvalId, accountId, mode: "card", outTrackId: activeCardOutTrackId }。 +
    (3) 明确失败(HTTP 4xx/5xx 非超时、模板字段不支持、PUT 被钉钉拒等可确定失败的错误)→ WARN log + 降级 markdown(调下方 route="markdown" 同一段逻辑),成功后返回 entry = { approvalId, accountId, mode: "markdown" }。这样用户至少能看到 approval id 与 /approve 命令模板。 +
    (4) 模糊失败(请求超时但可能已成功)→ WARN log + return null(不重发避免双消息)。 +

    route="markdown": +
    (1) 调 approval-markdown-render 构造 markdown 文本(含 /approve 命令模板); +
    (2) sendProactiveTextOrMarkdown(config, target.to, text, ...) 发独立消息; +
    (3) 成功 → 返回 entry = { approvalId, accountId, mode: "markdown" }; +
    (4) 失败 → WARN log + return null(markdown 已是最低保障路径,再降无意义)。 +

    两路径通用约束:在 catch 内调 observe.onDeliveryError——runtime 契约不允许 transport 自触发该钩子 updateEntry({ cfg, accountId, entry, payload, phase }) @@ -1069,34 +1094,41 @@

    Channel stopClient(账号停用 / gateway 重启)—— v3 推迟到 v2< v2 future:若 SDK 暴露 activeEntries 查询 API,或 channel 引入轻量 outTrackId Set(仅供 stop-time 清理用,非完整 entry store),再实现 finalize。

    -

    6.7 失败处理(v3.3 重写:两路由各自的失败模式)

    +

    6.7 失败处理(v3.4 修订:card 失败明确降级 markdown)

    -

    card 路径失败

    +

    card 路径失败的两种亚态

    approval-card-patcher.applyPendingPatch(outTrackId, ...)
    -  ├─ PUT updateCardVariables → 钉钉 API 5xx / 网络 / agent card 已 FINISHED 不可改
    +  ├─ PUT updateCardVariables
       │
    -  └─ 内部 catch + WARN 日志 + return null
    -       core 看 null → 不再触发 updateEntry → 该 approval 在 channel 端无视觉反馈
    -       用户可用 /approve 命令兜底
    -
    -注意:card 路径失败时自动降级到 markdown 路径——这样会导致 1 个
    -approval 投出 2 条消息(按钮 patch 残留 + 新 markdown 消息),UX 混乱。
    -若需"双保险"策略,应在 prepareTarget 层就明确路由(已经是这样)。
    + ├─ 明确失败(HTTP 4xx/5xx 已收到 / 模板字段不支持 / 钉钉显式拒绝) + │ ├─ WARN log + │ └─ 降级到 markdown 路径:调 sendProactiveTextOrMarkdown 发 /approve + │ 命令模板消息(v3.4 修订——v3.3 错误地选择"静默 pending",但此时用户看 + │ 不到 approval id,无法用 /approve 命令兜底,等于双重失败) + │ 成功 → return entry { mode:"markdown", ... } + │ 失败 → 同 markdown 路径失败处理 + │ + └─ 模糊失败(请求超时但可能已成功 / 网络中断难判定) + ├─ WARN log + └─ return null(不重发,避免"超时其实已成功 + 重发"导致双消息) + 上游 core 把这个 approval 标失败,用户从 OpenClaw 监控看到 pending 超时

    markdown 路径失败

    sendProactiveTextOrMarkdown(target, markdownText, ...)
       ├─ 钉钉 API 5xx / 工作通知权限不足 / target 不可达
       │
       └─ 内部 catch + WARN 日志 + return null
    -       同上:core 看 null 不触发后续;approval 在 channel 端无视觉反馈
    -       此场景下用户可能完全感知不到 approval 存在 → 仅靠 CLI/WebUI 日志
    + core 看 null 标 entry 失败;markdown 已是最低保障路径,再降无意义 + 此场景用户可能完全感知不到 approval 存在 → 仅靠 OpenClaw 端 pending 告警
    - v3.3 失败模式的设计取舍 + v3.4 失败降级策略关键原则
      -
    • card 失败不降级 markdown:避免双消息。需要"双保险"留给 v2 future 决策(可能引入 retry + fallback chain)
    • -
    • markdown 失败不降级到其它通道:同样避免双消息;本来 markdown 已经是"最低保证"路径,再降无意义
    • -
    • 两路径共同的接受前提:approval 投递失败是异常通路。生产环境应通过 OpenClaw 端 approval 监控发现(approval 长时间 pending 触发告警)
    • +
    • card 明确失败 → markdown 降级:保证用户至少能看到 approval id + /approve 命令模板,否则用户连兜底命令都用不上
    • +
    • card 模糊失败(超时)→ 不重发:避免"实际成功 + 重发"导致双消息
    • +
    • 实现层面区分两种亚态的判定:HTTP status code 已收到 + body 含错误码 = 明确失败;socket timeout / connection reset = 模糊失败
    • +
    • markdown 失败不再降级:本来就是最低保障路径
    • +
    • 所有失败都进 OpenClaw 端 approval pending 监控告警(runtime 已有此能力)
    @@ -1460,16 +1492,28 @@

    8. 错误处理矩阵

    失败点触发条件处理用户体验 - createAndDeliver 网络/HTTP 失败 - 钉钉 API 5xx、超时、429 - v3.3 transport 自闭环按 route 分支:card 路径下 PUT 失败 → 内部 WARN log + return null(不降级到 markdown,避免双消息,详见 §6.7);markdown 路径下 sendProactiveTextOrMarkdown 失败同样 internal log + return null。调 observe.onDeliveryError——那是 runtime 捕获 throw 才触发的钩子,本设计选 transport 内部处理就不能同时让 runtime observe 介入 - 收到 markdown 消息,含 /approve 命令模板 + card 路径 PUT updateCardVariables 失败 + 钉钉 API 5xx / 4xx 明确错 / 模板字段不支持 / agent card 已 FINISHED 不可改 + v3.4 修订:明确失败时降级 markdown。card 路径 catch → WARN log → 调 markdown 路径同一段(构造 markdown + sendProactiveTextOrMarkdown)→ 成功 return entry { mode:"markdown" }。调 observe.onDeliveryError(runtime 契约不允许) + 用户收到 markdown 消息含 approval id + /approve 命令模板,可手动完成审批 - approval-card 模板未发布 - templateId 在 DingTalk 侧不存在 - 同上(错误归一化为 createAndDeliver 失败)+ ERROR 级日志(transport 内部 log,不 throw) - 降级到 markdown;运维需看日志修复 + card 路径 PUT 模糊失败 + 请求超时但可能已成功 / 网络中断难判定 + WARN log + return null(不重发避免"实际成功 + 重发"双消息) + 用户可能在卡片上看到按钮也可能看不到;OpenClaw 端 pending 告警兜底 + + + markdown 路径 sendProactiveTextOrMarkdown 失败 + 钉钉 API 5xx / 工作通知权限不足 / target 不可达 + WARN log + return null(markdown 已是最低保障路径,再降无意义) + 用户感知不到 approval;仅靠 OpenClaw 端 pending 告警 + + + v3.3 提到的 "approval-card 模板未发布" + 不适用(v3.3 起 D9 不再新建模板) + — + — updateCardVariables 失败 @@ -1530,7 +1574,7 @@

    9.1 测试文件布局

    tests/unit/approval-card-render.test.tspending/resolved/expired/canceled × exec/plugin = 8 个矩阵~16 tests/unit/approval-target-resolver.test.tsorigin 解析(含 turnSourceChannel=null)、DM 列表构造~10 tests/unit/approval-resolver.test.ts
    ★ v3.2D20/D21 核心:kind 推导 4 情况(plugin: 前缀 / 无前缀 + 两边授权 / 仅 plugin 授权 / 仅 exec 授权)、未授权返 unauthorized、resolveApprovalOverGateway 调用参数(含 resolveMethod 与 allowPluginFallback)、错误分类(already-resolved / not-found / gateway-error);mock SDK gateway~18 - tests/unit/approval-command-parser.test.ts
    ★ v3.2两种顺序 × 8 个 alias = 16 个合法 case + 5 个 malformed case + 上游 commands-approve.ts alias 集合对照断言~12 + tests/unit/approval-command-parser.test.ts
    ★ v3.2(v3.4 修订)两种顺序 × 10 个 alias = 20 个合法 case + 5 个 malformed case + 上游 commands-approve.ts:19-30 alias 集合对照断言(CI 断言:上游 alias 数 = channel 端 regex 覆盖数)~26 tests/unit/approval-callback-handler.test.tscardPrivateData 结构化 parse(含 button index 后缀)、调 resolver、按 result 分支处理(resolved/unauthorized/已处理或已过期);以 §1.2 的真机回调样本作 fixture(不重复测 resolver 内部逻辑——那由 resolver test 覆盖)~14 tests/unit/card-callback-service.test.ts(扩展既有)D16 改动:analyzeCardCallbackcardPrivateData 含 actionIds + params;既有 feedback / btn_stop 用例不受影响+6 tests/unit/approval-command-intercept.test.tsparser 命中 → 调 resolver;未授权 → sendProactiveTextOrMarkdown(mock);resolve-failed 仅 log;非 /approve 命令 return false;不重复测 parser / resolver 内部~8 @@ -1563,7 +1607,8 @@

    9.3 关键 integration 场景(v3:仅 origin)

  • self-approval 在 DM:approver 自己 DM 发起 exec → 投 1 卡到自己私聊 → 自己点 → 通过
  • 非 approver 点击:投 1 卡 → 非 approver 用户点 → 收私聊拒绝 → 卡片不变(按钮保留)
  • 过期:投卡 → mock 上游 expired event → core 调 updateEntry → 卡片刷成过期
  • -
  • createAndDeliver 失败 fallback:mock HTTP 错 → deliverPending 返 null → 同步调 sendProactiveTextOrMarkdown 发 markdown 兜底(含 /approve 命令模板)
  • +
  • card patch 明确失败降级 markdown(v3.4):route="card" → mock applyPendingPatch 抛 HTTP 400 → deliverPending 自动降级到 markdown 路径 → 验证 sendProactiveTextOrMarkdown 被调(含 /approve 命令模板)→ entry.mode 最终为 "markdown"
  • +
  • card patch 模糊失败不降级(v3.4):route="card" → mock applyPendingPatch 抛 socket timeout → deliverPending 返 null(不重发避免双消息)→ 验证 sendProactiveTextOrMarkdown 未被调
  • 用户敲 /approve 命令:群里 @bot /approve abc once → inbound-handler 早期 intercept → resolveApprovalOverGateway → 不进 reply 派发(不触发 session lock)
  • 未配置 approver:execApprovals 缺省 → isConfigured=false → shouldHandle=false → 上游不会调 DingTalk 路径
  • turnSourceChannel 非 dingtalk(CLI 触发):shouldHandle=false → 跳过 DingTalk 投递;用户在钉钉里手敲 /approve 完成审批
  • @@ -1603,7 +1648,7 @@

    阶段 1 · 接口骨架 + 统一 resolver + 命令链路(PR-1)

  • 实现核心收敛点(D20):approval-resolver.tsapproval-command-parser.ts
  • 实现 v3.3 新增的 approval-card-locator.ts(D22 落地)—— PR-1 就要做,因为 markdown 路径下 locator 也会被调(返回 null 是触发 markdown 的信号)
  • 实现支撑模块:approval-config.tsapproval-target-resolver.ts(仅 origin)、approval-capability.ts(不含 nativeRuntime 完整实现)、approval-command-intercept.ts(薄壳,调 parser + resolver)
  • -
  • src/channel.ts 挂上 approvalCapabilitynativeRuntime 暂留 undefined;capability 仅生效 authorizeActorAction / resolveApproveCommandBehavior 等权限部分
  • +
  • src/channel.ts 挂上 approvalCapabilitynativeRuntime 暂留 undefined;capability 仅生效工厂吐出的部分(authorizeActorAction / getActionAvailabilityState / delivery / native.describeDeliveryCapabilities)。
    v3.4 修订引入 resolveApproveCommandBehavior——上游工厂 createApproverRestrictedNativeApprovalCapabilityopenclaw/src/plugin-sdk/approval-delivery-helpers.ts:30)参数集不接受该字段;channel 端 /approve 走 §6.8 早期 intercept(绕过上游 dispatcher),不依赖 dispatcher 行为定制,因此本字段无意义
  • src/inbound-handler.ts/approve 早期 intercept(§6.8,D2 落地)—— PR-1 就要做,因为这是 Feishu-同档体验的最后一公里
  • SDK 基线三件套(D17 v3.2):peerDependency bump + pnpm-lock.yaml 更新 + 确认 node_modules/openclaw 版本 / tsconfig path 优先级
  • schema、配置文档(草稿)
  • @@ -1678,8 +1723,8 @@

    11.2 已知风险

    上游 commands-approve.ts 扩展 decision alias / 命令顺序 - 钉钉 channel 端 §6.8 的 regex 写死支持 allow-once|once|allow / allow-always|always / deny|reject|block 共 8 个别名 + 两种顺序;若上游新增 alias 或新顺序,channel 端用户敲新形式会被 channel 拒为 "malformed" - 在 §6.8 regex 旁加显式注释引用上游文件路径;CI 加一个测试断言上游 alias 集合不超出 channel 端支持范围(参考 openclaw/src/auto-reply/reply/commands-approve.ts alias 列表);或考虑改为复用上游 parser(如果上游导出 pure parse 函数则无须重写) + 钉钉 channel 端 §6.8 的 regex v3.4 完整支持上游 10 个别名 + 两种顺序;若上游新增 alias 或新顺序,channel 端用户敲新形式会被 channel 拒为 "malformed" + 在 §6.8 regex 旁加显式注释引用 openclaw/src/auto-reply/reply/commands-approve.ts:19-30;CI 加一个测试断言上游 alias 集合 = channel 端 regex 覆盖集(强相等,非子集);或考虑改为复用上游 parser(如果上游导出 pure parse 函数则无须重写) approval kind 按前缀派发的边界(Issue 2 修订相关) From 388f49e8a39cc1f781027dffce6012612f174950 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 00:00:00 +0800 Subject: [PATCH 08/44] =?UTF-8?q?docs(spec):=20v3.5=20=E4=BF=AE=E8=AE=A2?= =?UTF-8?q?=20=E2=80=94=20=E5=AF=B9=E9=BD=90=E7=94=A8=E6=88=B7=E5=AE=9E?= =?UTF-8?q?=E9=99=85=E9=85=8D=E5=A5=BD=E7=9A=84=20schema?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户在 DingTalk 开发者平台实际配置 v2 模板后揭示 5 处事实修订: 1. 模板新增两个变量(用户实测确认): - approve_btns (按钮组,template 内置 3 按钮定义) - show_approve_btns (Boolean,按钮组可见性条件) 2. 三按钮独立命名 actionId,与 OpenClaw 语义对齐: - allow-once / allow-always / deny 重要修正:DingTalk 仅在 actionId 重名时才加 button index 后缀(消歧用)。 独立命名时原样回传,无后缀。 之前 v3.2 看到的 "approve0/1/2" 是因为 3 按钮都叫 approve 触发的消歧。 3. 按钮在 v2 模板内置("按钮组来源:指定"),channel 不再构造 CardBtn[]: - approval-card-patcher 大幅简化(从 ~150 行降到 ~80 行) - approval 模块不再需要自定义 CardBtn 类型(v3.4 的关心点失效) - 三按钮的 actionId / 颜色 / 回传参数都固化在模板里 4. 回传参数仅 params.action(值 = decision),不带 approval id: - 必须通过 outTrackId 反查 approval id 5. 新增 D24 approval id 反查机制(用户拍板 A 方案): - 给 CardRunRecord 加 pendingApprovalId?: string 字段 - 新增 markCardRunPendingApproval / clearCardRunPendingApproval 两个 API - patch 时 set,callback 反查,resolved/expired 时 clear - 不引入独立 approval-store,与 D18 边界一致 - 一卡只能挂 1 个 approval(pendingApprovalId 单字段,agent 串行约束) 各章节修订面: - D15 大改(按钮独立命名 + 无后缀 + params.action) - 新增 D24(approvalId 反查机制) - D23 实施细节修订(show_approve_btns 与 hasAction 是独立变量) - §1.2 button payload 编码/解码完整重写,对齐实测 schema - §3.2 approval-card-patcher 简化,加 v1 终态展示限制说明 - §3.3 card-run-registry 改动面加 D24 字段与 API;types 行删 CardBtn - §5.2 buildPendingPayload 删 buttons 输出 - §5.3 deliverPending(card) 简化为 toggle 变量 + 注册反查映射 - §6.3 click flow 重写解码 + 加反查步骤 - §7.1 模板字段映射改成对照用户实配 schema + v1 终态展示限制坦率说明 - §7.2 按钮配置重写为"template 内置 + channel 仅 toggle" 模块结构:保持 10 个文件,但 approval-card-patcher 简化 ~70 行。 v3.5 业务代码 ~830 行(v3.4 ~900,简化 ~70);测试代码 ~1700 行。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 357 +++++++++++------- 1 file changed, 217 insertions(+), 140 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index 2b9091d7..758ab4d2 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -58,66 +58,89 @@

    1.1 v1 范围明确不做的事

  • 不引入 select / input / datepicker 等高级组件——仅 button(已 CONFIRMED 平台支持)
  • -

    1.2 按钮 payload 编码与解码(D15 落地)

    -
    - 真机回调实证(2026-03-21):DingTalk 会自动在 actionId 末尾追加 button 索引
    - 定义 btns = [{actionId:"btn_approve"}, {actionId:"btn_approve_once"}, {actionId:"btn_deny"}] → - 回调拿到的 cardPrivateData.actionIds[0] 依次是 "btn_approve0" / "btn_approve_once1" / "btn_deny2"。 - 所以 actionId 不能承载结构化语义(解析正则会乱);结构化信息必须走 params。 +

    1.2 按钮 payload 编码与解码(D15/D24 落地,v3.5 对齐用户实配 schema)

    + +
    + v3.5 实测核对:DingTalk 仅在 actionId 重名时才追加 index 后缀
    + v3.2 测试 3 按钮都叫 "approve" → 回调 "approve0/1/2"(消歧); + v3.5 用户在模板配 3 按钮独立 actionId(allow-once/allow-always/deny)→ 回调原样回 "allow-once" / "allow-always" / "deny"(无后缀)。 + 唯一名时无后缀。
    -

    编码(构造按钮时)

    -

    三按钮共享同一个 actionId: "approval",靠 params 区分:

    -
    // approval-card-service.ts 内部
    -btn.event = {
    -  type: "sendCardRequest",
    -  params: {
    -    actionId: "approval",                              // 三按钮全用这个
    -    params: { t: "approval", d: "<decision>", id: "<approvalId>" },
    -  },
    -}
    -// decision ∈ { "allow-once" | "allow-always" | "deny" }
    +

    编码(v2 模板内置的按钮组定义)

    +

    v3.5 起按钮在 AI Card v2 模板内置("按钮组来源:指定"),channel 不再构造 CardBtn[]。模板 schema 配置(用户实测):

    +
    // AI Card v2 模板里 approve_btns 按钮组的 3 个按钮:
    +//
    +// 按钮 1(允许一次):
    +//   text=允许一次, color=green, status=normal
    +//   event:
    +//     type:    "sendCardRequest"
    +//     ActionId: "allow-once"
    +//     回传参数: [{ 参数名: "action", 参数类型: 静态值, 参数值: "allow-once" }]
    +//
    +// 按钮 2(总是允许):
    +//   ActionId: "allow-always", params={action:"allow-always"}
    +//
    +// 按钮 3(拒绝):
    +//   ActionId: "deny", params={action:"deny"}
    +//
    +// 显示控制(条件计算):show_approve_btns 的值为 true → 整组可见
     
    -        

    回调实测形态

    -
    // 用户点"允许"按钮(btn[0]),平台 push 的 callback data.content:
    +// channel 端 patch 时只需 PUT 两个变量:
    +updateCardVariables(outTrackId, {
    +  show_approve_btns: "true",     // ← 显示 approval 按钮组(D23)
    +  hasAction:         "false",    // ← 隐藏 btn_stop(D23)
    +}, token)
    +// 三个按钮的 actionId / params 都在 template 里固定,channel 不传
    + +

    回调实测形态(用户配 schema 后的预期)

    +
    // 用户点"允许一次"按钮,平台 push 的 callback data.content:
     {
       "cardPrivateData": {
    -    "actionIds": ["approval0"],                        // ← 平台追加 index "0"
    -    "params": { "t": "approval", "d": "allow-once", "id": "abc123" }
    -  }
    +    "actionIds": ["allow-once"],          // ← 唯一命名,无 index 后缀
    +    "params":    { "action": "allow-once" }
    +  },
    +  "outTrackId":  "ai_card_xxx",           // ← agent reply card 的 id
    +  "userId":      "staffA",                 // ← clicker staffId
    +  "spaceType":   "im" 或 "group",
    +  ...
     }
    -// 注意:content 与 value 字段内容一致(钉钉冗余)
    -// userId 字段:staffId(已用户确认),不是 unionId
    +// 注意 approval id 不在 payload —— 必须通过 outTrackId 反查(D24)

    解码(callback 入口)

    // 第一步:扩展后的 analyzeCardCallback 把 cardPrivateData 整体放进 analysis
    -const cpd = analysis.cardPrivateData;                  // { actionIds, params }
    +const cpd = analysis.cardPrivateData;          // { actionIds, params }
     
     // 第二步:parseApprovalFromCardPrivateData(cpd)
    -if (!cpd?.actionIds?.[0]?.startsWith("approval")) return null;  // 前缀匹配兼容 index 后缀
    -if (cpd.params?.t !== "approval") return null;
    -if (!["allow-once","allow-always","deny"].includes(cpd.params?.d)) return null;
    -if (typeof cpd.params?.id !== "string") return null;
    -return { approvalId: cpd.params.id, decision: cpd.params.d };
    +// actionId 用 exact match(唯一命名无后缀);fallback 用 params.action 兜底 +// (万一未来 schema 改动让 actionId 出现后缀也不会断) +if (!cpd?.params?.action) return null; +const action = cpd.params.action; +if (!["allow-once", "allow-always", "deny"].includes(action)) return null; +const decision = action as "allow-once" | "allow-always" | "deny"; + +// 第三步(D24):approvalId 反查 +const cardRun = resolveCardRun(analysis.outTrackId); +if (!cardRun?.pendingApprovalId) { + // 卡片不存在/未挂 approval/已被清理 → 视为"已处理或已过期" + return { approvalId: null, decision, reason: "no-pending-approval" }; +} +return { approvalId: cardRun.pendingApprovalId, decision };

    回写上游

    -
    // 用 SDK 公开 API(v2026.4.7+):
    -import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime";
    -
    -await resolveApprovalOverGateway({
    -  cfg,
    -  approvalId,
    -  decision,
    +        
    // 用 SDK 公开 API(v2026.4.7+),通过 approval-resolver 单点收敛(D20):
    +await approvalResolver.resolveApproval({
    +  cfg, accountId,
    +  approvalId, decision,
       senderId: analysis.userId,           // staffId
    -  clientDisplayName: "DingTalk",
    -});
    -// SDK 内部按 approval kind 自动 dispatch 到
    -// exec.approval.resolve / plugin.approval.resolve
    + log, +}) +// resolver 内部按 D21 推导 resolveMethod + allowPluginFallback, +// 然后调 resolveApprovalOverGateway。channel 端零重复
    命令路径与按钮路径的关系
    - v1 spec 宣称"按钮和 /approve 命令走完全同一条解析链"——v2 不再成立(按钮走 cardPrivateData,命令走文本 regex)。 - 但两条路径都最终调同一个 resolveApprovalOverGateway,**等价收敛到上游 store**。用户感知一致,channel 内部解码逻辑分两条。 + 两条路径都最终调同一个 approval-resolver.resolveApproval(D20 单点收敛)。差异仅在 decode 阶段:按钮走 cardPrivateData + outTrackId 反查;命令走文本 regex。用户感知一致。
    @@ -225,9 +248,16 @@

    2. 已确认的决策清单

    D15 - 按钮 payload 编码 - 所有三按钮共享 actionId "approval",decision/id 走 params 结构化字段:
    event.params = { actionId: "approval", params: { t: "approval", d: "allow-once"|"allow-always"|"deny", id: <approvalId> } }
    原因(真机回调证据):DingTalk 会在 actionId 末尾自动追加 button index("approval" → "approval0"/"approval1"/"approval2"),用前缀匹配 + 结构化 params 比字符串字面量解析稳得多。 - 对齐 PR #489 + 真机 payload + 按钮 payload 编码(v3.5 大改:对齐用户实配 schema) + 三个按钮独立命名 actionId(与 OpenClaw 语义对齐): +
    • 允许一次 → actionId=allow-once, params={action:"allow-once"} +
    • 总是允许 → actionId=allow-always, params={action:"allow-always"} +
    • 拒绝 → actionId=deny, params={action:"deny"} +
    +
    关键修正(v3.5 实测):DingTalk 仅在 actionId 重名时才追加 button index 后缀(消歧用)。独立命名时 callback 中 actionId 原样回传,无 index 后缀。之前 v3.2 看到的 "approve0/1/2" 是测试时 3 按钮都叫 "approve" 触发的消歧行为。 +
    +
    approval id 不在按钮 payload——只能通过 callback 的 outTrackId(agent reply card 的 id)反查(详见 D24) + 对齐用户实测 + OpenClaw 命名习惯 D16 @@ -291,12 +321,28 @@

    2. 已确认的决策清单

    D23 - btn_stop 与 approval 按钮共存(v3.3 新增) - approval 期间隐藏 btn_stop(cardParamMap.btns 只含 approval 3 按钮); -
    resolved 后恢复 btn_stop(若 agent card 还在 INPUTING 状态,让用户继续可中止 agent); -
    expired/decision=deny 时恢复 btn_stop 不一定有意义——deny 时 agent 通常会被上游终止,card 进入 STOPPED 状态,按钮自然隐藏。 -
    语义动机:"等批准" 阶段 stop 没语义(agent 卡在等决定),4 按钮也太挤;批准放行后 agent 继续输出,stop 才重新有用 - v3.3 用户拍板 + btn_stop 与 approval 按钮共存(v3.3 新增;v3.5 实施修订) + 独立 cardParamMap 变量分别控制可见性: +
    show_approve_btns(Boolean)→ 控制 approval 按钮组(D15 三按钮) +
    hasAction(Boolean)→ 控制 btn_stop(现有 AI Card v2 既有字段) +
    +
    pending 时:show_approve_btns=true + hasAction=false(隐藏 stop); +
    resolved 时:show_approve_btns=false + hasAction=true(恢复 stop,若 card 还活跃); +
    expired 时:show_approve_btns=false + hasAction 视 card 状态而定。 +
    语义动机:"等批准" 阶段 stop 没语义(agent 卡在等决定);批准放行后 agent 继续输出,stop 才重新有用 + v3.3 用户拍板 + v3.5 实测 schema 字段 + + + D24 + approvalId 反查机制(v3.5 新增) + 按钮 payload 不携带 approval id(D15),但 callback 一定带 outTrackId(agent reply card 的 id)。channel 端通过扩展 CardRunRecord 加一个新字段 pendingApprovalId?: string 来建立反查映射: +
    approval-card-patcher.applyPendingPatch(outTrackId, approvalId) 内部调 markCardRunPendingApproval(outTrackId, approvalId) 把 approvalId 写到 record; +
    • callback handler 拿到 outTrackId 后 resolveCardRun(outTrackId).pendingApprovalId 即可拿到 approvalId; +
    applyResolvedPatch / applyExpiredPatch 内部调 clearCardRunPendingApproval(outTrackId) 清字段。 +
    +
    不引入独立 approval-store——只是给现有 card-run-registry 加 1 个 optional 字段 + 2 个 API,与 D18 边界一致。 +
    一卡只能挂 1 个 approval(pendingApprovalId 是单字段);agent 串行 approval 是天然约束,多并发 approval 不在 v1 范围 + v3.5 用户拍板(A 方案:不再改模板) @@ -316,6 +362,13 @@

    2. 已确认的决策清单

    (6) §10 PR-1 措辞改"resolve 通道生效,approval id 可见性依赖外部界面,完整 UX 在 PR-2"; (7) §6.7 删除 store.register 残留; (8) §6.3 / §6.6 / §8 / §9 等所有 already-resolved 文案改"ℹ️ 已处理或已过期"并明确 catch 后 return 避免覆盖;§6.8 alias 范围显式列清;§11.2 风险表新增 kind 派发边界条目 +
  • v3.5(2026-05-18 用户实配 schema 对齐):用户在 v2 模板上实际配置后揭示 5 处事实修订—— + (1) 模板新增 approve_btns(按钮组)+ show_approve_btns(Boolean)两个变量; + (2) 三个按钮 actionId 独立命名 allow-once / allow-always / deny无 index 后缀(之前测试 "approve0/1/2" 是因为 3 按钮都叫 approve 重名,DingTalk 才加 index 消歧;唯一名时原样回); + (3) 按钮在 template 内置("按钮组来源: 指定"),channel 不再构造 CardBtn[],只 toggle show_approve_btns 可见性; + (4) 回传参数仅 params.action(值 = decision),不带 approval id——必须通过 outTrackId 反查 approvalId; + (5) 新增 D24:approvalId 反查机制——给 CardRunRecordpendingApprovalId?: string 字段 + markCardRunPendingApproval / clearCardRunPendingApproval API。 +
    影响:D15 改、新增 D24;§1.2 / §3.2 / §3.3 / §5.2 / §5.3 / §6.3 / §7.1 / §7.2 全部对齐;模块层面 approval-card-patcher 大幅简化(不构造按钮数组);approval 模块不再需要自定义 CardBtn 类型
  • v3.4(2026-05-18 第五轮 review):7 处修订—— (1) 失败降级策略修订:card patch 明确失败时降级到 markdown(v3.3 错误地选"静默 pending",用户连 approval id 都看不到无法兜底 /approve);模糊失败(超时)仍 return null 避免双消息; (2) 错误矩阵 / §6.7 / §10 等多处 createAndDeliver 失败 fallback / approval-card 模板未发布等过时表述统一清理; @@ -446,16 +499,17 @@

    3.2 模块单一职责表

    ~60(+ card-run-registry 内 ~20 行新 API) - approval-card-patcher.ts
    ★ v3.3 替代 card-template + card-render - 知道如何在已存在的 agent reply card上做按钮区域 patch: -
    buildApprovalButtons(approvalId): CardBtn[](3 个按钮,编码同 §1.2 D15); -
    applyPendingPatch(outTrackId, approvalId, token):PUT 设置 btns=approvalButtons, hasAction="true"(隐藏 btn_stop,D23); -
    applyResolvedPatch(outTrackId, decision, resolverDisplayName, token):PUT 清按钮 + 写"✅ 已批准 by @user · <decision>" 到 agent card 的某变量位(具体位置 PR-2 实施时决定)+ 若 card 还活则恢复 btn_stop; -
    applyExpiredPatch(outTrackId, token):PUT 清按钮 + 写"ℹ️ 已处理或已过期" + 恢复 btn_stop(如适用) - card-callback-service.updateCardVariables;approval 模块内自定义的 CardBtn 类型(v3.4:当前 main 未导出 CardBtn 类型,approval 模块自定义) - - 关于 CardBtn 类型来源(v3.4 修订):核实当前 main 后,src/card/card-template.ts:12 仅暴露 template key 常量;PR #480 引入了 AI Card v2 ButtonGroup 资产但没暴露 TS 类型。所以 approval-card-patcher.ts 内部自定义 CardBtn 接口(含 text/color/status/event 四字段),并在 patcher 测试里加 fixture 验证:"btns" key 在当前 v2 模板有 String 类型 cardParamMap 字段 - ~150 + approval-card-patcher.ts
    ★ v3.5 大幅简化(不再构造按钮) + v3.5:按钮在 v2 模板内置("按钮组来源:指定",3 按钮 actionId/params 都固化),patcher 只 toggle 变量 + 维护 D24 反查映射。 +
    applyPendingPatch(outTrackId, approvalId, token):PUT updateCardVariables({ show_approve_btns: "true", hasAction: "false" }) + markCardRunPendingApproval(outTrackId, approvalId); +
    applyResolvedPatch(outTrackId, decision, token, cardStillActive):PUT updateCardVariables({ show_approve_btns: "false", hasAction: cardStillActive ? "true" : "false" }) + clearCardRunPendingApproval(outTrackId); +
    applyExpiredPatch(outTrackId, token, cardStillActive):同上但语义不同。 +
    +
    v3.5 已知限制(v1):用户实测 schema 里没专门的 approval 终态文字位(statusLine 被 taskInfo 占用,contentKey 与 stream 写冲突)。v1 终态仅靠"按钮消失"——用户感知按钮没了就是已处理。若需明确终态文字,v1.x 让用户在模板加 approval_status 变量后再 PUT 写入。 +
    +
    v3.5 不再需要自定义 CardBtn 类型(v3.4 关心的事在 v3.5 自动失效——按钮 channel 端不构造) + card-callback-service.updateCardVariables; card-run-registry pendingApprovalId API(v3.5 新增) + ~80(v3.4 是 ~150) approval-markdown-render.ts
    ★ v3.3 替代 fallback-render @@ -545,13 +599,19 @@

    3.3 与现有代码的接触面

    — - src/card/card-run-registry.ts
    (v3.4 新增改动面) - 新增 export function resolveActiveCardRunBySession(accountId: string, sessionKey: string): CardRunRecord | null——遍历 records Map(小,TTL 30min;scan 性能可接受)按 accountId + sessionKey 精确匹配且 record.card.state ∈ {PROCESSING, INPUTING} 过滤。不要修改 sweep 行为(避免影响 btn_stop 路径) - 低(追加 export,不动既有 API;新 API 仅供 approval-card-locator 使用) + src/card/card-run-registry.ts
    (v3.4 + v3.5 新增改动面) + v3.4 改动:新增 resolveActiveCardRunBySession(accountId, sessionKey): CardRunRecord | null——遍历 records Map 按 accountId + sessionKey 精确匹配且 state ∈ {PROCESSING, INPUTING} 过滤。 +
    v3.5 新增(D24 落地): +
    CardRunRecord 接口新增 pendingApprovalId?: string 字段; +
    • 新增 markCardRunPendingApproval(outTrackId: string, approvalId: string): void; +
    • 新增 clearCardRunPendingApproval(outTrackId: string): void。 +
    两个 API 都是无副作用的 setter/clearer,对既有 outTrackId 路径无影响。 +
    不要修改 sweep 行为(避免影响 btn_stop 路径)。注意:record 被 sweep 时 pendingApprovalId 自然丢失——callback 反查失败时降级为 "已处理或已过期" 提示(D24 + §6.6) + 低(仅追加字段与 export,不动既有 API;仅供 approval-card-patcher / approval-callback-handler 使用) src/types.ts - 新增 ApprovalEntry / ApprovalDecision / ApprovalPhase 类型;同时 在 approval 模块内定义 CardBtn 类型(v3.4 修订:当前 main 未导出 CardBtn 类型——src/card/card-template.ts:12 仅暴露 template key 常量。PR #480 引入了 ButtonGroup 资产但没暴露 TS 类型)。approval 模块自定义 CardBtn 接口反映 sendCardRequest 契约(text/color/status/event 四字段);并在 approval-card-patcher 测试里加"btns key 是 AI Card v2 模板约定字段"的 fixture 验证 + 新增 ApprovalEntry / ApprovalDecision / ApprovalPhase 类型。
    v3.5 修订:不再需要 CardBtn 类型——v3.4 关心的"channel 端构造按钮"在 v3.5 失效(按钮在 v2 模板内置)。channel 端只 PUT cardParamMap 的 show_approve_btnshasAction 两个 Boolean 字段,无任何按钮数据结构 极低 @@ -679,21 +739,12 @@

    5.2 presentation

    buildPendingPayload({ request, nowMs, view }) - v3.3 修订:同时构造 card 路径用的 buttons + markdown 路径用的文本。 -
    transport 后续按 preparedTarget.route 选用其一。返回 { approvalId: request.id, buttons: CardBtn[], markdownText: string }。 -
    buttons(card 路径用):3 个 CardBtn,共享 actionId "approval",靠 params.d 区分(D15 编码): -
    buttons = [
    -  { text:"✅ 允许一次", color:"green",
    -    event:{type:"sendCardRequest", params:{
    -      actionId:"approval", params:{t:"approval", d:"allow-once", id:request.id}}}},
    -  { text:"✅ 总是允许", color:"blue",
    -    event:{type:"sendCardRequest", params:{
    -      actionId:"approval", params:{t:"approval", d:"allow-always", id:request.id}}}},
    -  { text:"⛔ 拒绝", color:"red",
    -    event:{type:"sendCardRequest", params:{
    -      actionId:"approval", params:{t:"approval", d:"deny", id:request.id}}}},
    -]
    -
    markdownText(markdown 路径用):由 approval-markdown-render.buildExecApprovalMarkdown(request, nowMs)buildPluginApprovalMarkdown 输出,含 approval id、command/tool preview、3 个 /approve <id> <decision> 复制即用块 + v3.5 简化(D15 + 模板内置按钮):channel 不再构造按钮数组。 +
    返回 { approvalId: request.id, markdownText: string }。 +
    approvalId:transport.deliverPending(card) 用来 markCardRunPendingApproval(outTrackId, approvalId)(D24); +
    markdownText(markdown 路径用):由 approval-markdown-render.buildExecApprovalMarkdown(request, nowMs)buildPluginApprovalMarkdown 输出,含 approval id、command/tool preview、3 个 /approve <id> <decision> 复制即用块。 +
    +
    按钮本身的 actionId / params 都在 v2 模板的 approve_btns 按钮组里固化(D15),channel 端永远不传按钮定义 buildResolvedResult({ request, resolved, view, entry }) @@ -723,7 +774,7 @@

    5.3 transport

    deliverPending({ cfg, accountId, preparedTarget, request, pendingPayload }) v3.4 修订:按 preparedTarget.route 分两条路径 + card 失败时降级 markdown
    route="card": -
    (1) 调 approval-card-patcher.applyPendingPatch(activeCardOutTrackId, request.id, token)——PUT updateCardVariables 注入 3 个 approval 按钮到现有 agent card(D22)+ 隐藏 btn_stop(D23)。 +
    (1) 调 approval-card-patcher.applyPendingPatch(activeCardOutTrackId, request.id, token)——内部 PUT updateCardVariables({ show_approve_btns: "true", hasAction: "false" }) 显示 approval 按钮组(D15 + D22)+ 隐藏 btn_stop(D23)+ 把 approvalId 写到 card-run-registry record.pendingApprovalId(D24 反查映射)。按钮 actionId/params 都在模板内置,channel 不传按钮定义。
    (2) 成功 → 返回 entry = { approvalId, accountId, mode: "card", outTrackId: activeCardOutTrackId }
    (3) 明确失败(HTTP 4xx/5xx 非超时、模板字段不支持、PUT 被钉钉拒等可确定失败的错误)→ WARN log + 降级 markdown(调下方 route="markdown" 同一段逻辑),成功后返回 entry = { approvalId, accountId, mode: "markdown" }。这样用户至少能看到 approval id 与 /approve 命令模板。
    (4) 模糊失败(请求超时但可能已成功)→ WARN log + return null(不重发避免双消息)。 @@ -909,38 +960,48 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    用户在卡片上点"允许一次"
     
     t=0   DingTalk Stream 平台推送 TOPIC_CARD 回调
    -        payload.content/value (内嵌 JSON, 真机实测 §1.2):
    +        payload.content/value (内嵌 JSON, v3.5 实配 schema):
               { cardPrivateData: {
    -              actionIds: ["approval0"],          ← 平台自动追加 button index
    -              params: { t:"approval", d:"allow-once", id:"abc123" }   ← decision payload
    +              actionIds: ["allow-once"],          ← 唯一命名无 index 后缀(D15 v3.5)
    +              params:    { action: "allow-once" } ← 仅 decision,不带 approvalId
                 } }
    -        payload.userId                    = "staffA"  (staffId)
    -        payload.spaceId / spaceType       = "cid_xxx" / "group"
    -        payload.outTrackId                = "approval_abc123_"
    +        payload.userId      = "staffA"             ← clicker staffId
    +        payload.spaceType   = "im" 或 "group"
    +        payload.outTrackId  = "ai_card_xxx"        ← 原 agent reply card 的 outTrackId
     
     t=1   src/gateway/channel-gateway.ts:330 listener 触发
             ├─ messageId = res.headers.messageId
             ├─ payload   = JSON.parse(res.data)
             ├─ analysis  = analyzeCardCallback(payload)
    -        │    analysis.actionId       = "approval0"          ← 平台追加 button index
    -        │    analysis.userId         = "staffA"             ← staffId(已用户确认)
    -        │    analysis.cardPrivateData = {                    ← D16 新增字段
    -        │      actionIds: ["approval0"],
    -        │      params: { t:"approval", d:"allow-once", id:"abc123" }
    +        │    analysis.actionId       = "allow-once"          ← 唯一命名无后缀
    +        │    analysis.userId         = "staffA"
    +        │    analysis.outTrackId     = "ai_card_xxx"
    +        │    analysis.cardPrivateData = {                     ← D16 新增字段
    +        │      actionIds: ["allow-once"],
    +        │      params: { action: "allow-once" }
             │    }
             │
             ├─ 【新增分支】tryHandleApprovalCallback(analysis, ...)
             │    │
    -        │    1. 前缀 + 结构化字段双校验
    +        │    1. 双源解码(v3.5 对齐实配 schema)
             │       parsed = parseApprovalFromCardPrivateData(analysis.cardPrivateData)
    -        │       // 内部:actionIds[0].startsWith("approval") + params.t==="approval"
    -        │       //       + params.d ∈ {allow-once|allow-always|deny}
    -        │       //       + typeof params.id === "string"
    +        │       // 内部:params.action ∈ {allow-once|allow-always|deny} → decision
    +        │       //       (actionId 仅用于"是不是 approval 按钮"路由,exact match 即可)
             │       if (!parsed) return { handled: false }
    -        │       parsed = { approvalId: "abc123", decision: "allow-once" }
    +        │       parsed = { decision: "allow-once" }       ← 注意:还没有 approvalId
             │
    -        │    2. (v2 不再走 parseExecApprovalCommandText——按钮路径走 cardPrivateData 解析;
    -        │        命令路径走 §6.8 早期 intercept 的 regex;两条路径最终都调 resolveApprovalOverGateway)
    +        │    2. 反查 approvalId(D24)
    +        │       const cardRun = resolveCardRun(analysis.outTrackId);
    +        │       const approvalId = cardRun?.pendingApprovalId;
    +        │       if (!approvalId) {
    +        │         // card 不存在(重启清空 / TTL sweep)/ 未挂 pending approval
    +        │         // → 卡片刷"ℹ️ 已处理或已过期"提示用户
    +        │         await updateCardVariables(analysis.outTrackId, {
    +        │           show_approve_btns: "false",
    +        │         }, token).catch(() => {});
    +        │         return { handled: true, reason: "no-pending-approval" };
    +        │       }
    +        │       parsed.approvalId = approvalId;          ← 现在有了
             │
             │    3. 调统一 resolver(D20:所有 kind 推导 / 授权 / fallback /
             │                       gateway 调用 / 错误分类都在内部)
    @@ -1247,64 +1308,80 @@ 

    为什么 D2 不能"纯复用上游 /approve dispatcher"

    7. 审批卡片设计

    -

    7.1 复用 AI Card v2 模板的字段映射(v3.3 D9)

    -

    v3.3 不再设计独立 approval 模板——card 路径直接复用 PR #480 已合并的 AI Card v2 模板。approval 渲染只是在 agent reply card 上做 cardParamMap 字段级 patch:

    +

    7.1 v2 模板字段映射(v3.5 对齐用户实配 schema)

    +

    v3.5 用户已在 AI Card v2 模板上配好两个新增变量(schema id ending in 876de.schema)+ 一个 ButtonGroup:

    - + - - - + + + + + + + + + + - - - + + + + - + + - +
    AI Card v2 现有字段approval 用途pending → resolved/expired 变化
    cardParamMap 变量类型approval 用途pending / resolved / expired 时的值
    btns(JSON-stringified CardBtn[])承载 approval 3 按钮 + btn_stop 共存策略pending:写入 3 个 approval 按钮(隐藏 btn_stop,D23)
    resolved:恢复 btn_stop(若 card 活跃)
    expired:恢复 btn_stop(若 card 活跃)或清空
    approve_btns按钮组(template 内置 3 按钮定义)不在 channel 端构造——按钮 actionId / params 都在模板里固化(D15)不需要 channel 端更新
    show_approve_btnsBooleanapproval 按钮组的显隐条件pending:"true";resolved/expired:"false"
    hasAction("true"/"false")控制按钮区可见性pending:"true"
    resolved/expired:若恢复 btn_stop 则 "true";否则 "false"
    hasAction(既有字段)Booleanbtn_stop 的显隐(既有 AI Card v2)pending:"false"(D23 隐藏 stop);resolved/expired:card 还 active ? "true" : "false"
    approval 终态指示位(PR-2 实施时决定,见下方说明)approval 终态指示位 显示"✅ 已批准 by @user · allow-once" / "ℹ️ 已处理或已过期"pending 不写;resolved/expired 写入对应文案v1 不实现——用户感知"按钮消失=已处理"(详见下方限制)
    - "approval 终态指示位"的实施候选(PR-2 决定) + v3.5 v1 终态展示限制(坦率描述) + 用户实配 schema 没有专门给 approval 终态文字的字段位:
      -
    • A. append 到 contentKey 末尾:"...<agent reply>\n\n---\n✅ 已批准 by @user · allow-once"。优点:零模板改动;缺点:与 agent 内容混在一起,视觉权重不分
    • -
    • B. 用 blockListKey 加 system block(若 AI Card v2 模板支持):cleaner UX;缺点:需确认模板的 block type 列表是否含 system/notification 类型
    • -
    • C. 新增 approvalStatus 变量:最干净的语义;缺点:需要模板侧轻微改动(导入 + 重新发布 v2 模板)。考虑做成"模板可选支持,缺失时回落到 A"
    • +
    • statusLine:已被 taskInfo 占用(agent 显示 model/tokens/effort/taskTime/dapiUsage/agent 名)
    • +
    • content:与 agent stream 写冲突,patch append 不安全
    • +
    • blockList:未确认模板支持 system/notification block 类型
    • +
    • quoteContent:群聊引用上下文用,语义不匹配
    - PR-2 阶段确认 AI Card v2 模板能力后选定方案;当前 spec 不锁死具体字段以免与未来 main 状态错位。 + v1 策略:不主动写终态文字。show_approve_btns 由 true → false 让按钮消失,用户感知"我点的按钮成功了"即可。 +
    v1.x 升级路径:让维护者在模板加 approval_status 变量(小工作量)后再 PUT 写入"✅ 已批准 by @<name> · <decision>"
    -

    7.2 三按钮配置(v2 修订:cardPrivateData 结构化 - D15 落地)

    -

    按钮通过 PR #480 引入的 CardBtn[] 模板字段渲染。三按钮共享 actionId "approval",区分靠 params.d

    -
    // approval-card-patcher.ts 内部(v3.3)
    -function makeApprovalBtns(approvalId: string): CardBtn[] {
    -  return [
    -    { text: "✅ 允许一次", color: "green",  status: "normal",
    -      event: { type: "sendCardRequest",
    -               params: { actionId: "approval",
    -                         params: { t: "approval", d: "allow-once", id: approvalId } } } },
    -    { text: "✅ 总是允许", color: "blue",   status: "normal",
    -      event: { type: "sendCardRequest",
    -               params: { actionId: "approval",
    -                         params: { t: "approval", d: "allow-always", id: approvalId } } } },
    -    { text: "⛔ 拒绝",     color: "red",    status: "normal",
    -      event: { type: "sendCardRequest",
    -               params: { actionId: "approval",
    -                         params: { t: "approval", d: "deny", id: approvalId } } } },
    -  ];
    -}
    -// 实际放进 cardParamMap:
    -//   cardParamMap.btns = JSON.stringify(makeApprovalBtns(approvalId))
    -//   cardParamMap.hasAction = "true"  (终态时改为 "false")
    +

    7.2 三按钮配置(v3.5:在 v2 模板内置)

    +

    按钮定义在 AI Card v2 模板的 approve_btns 按钮组里("按钮组来源:指定"),channel 端不构造,仅 toggle show_approve_btns 可见性。模板配置(用户实测):

    +
    // approve_btns 按钮组(template 内置):
    +// ─────────────────────────────────────────────────────────────
    +//   按钮 1 · 允许一次(color: green)
    +//     ActionId:  "allow-once"
    +//     回传参数:  [{ name: "action", type: 静态值, value: "allow-once" }]
    +//
    +//   按钮 2 · 总是允许(color: blue)
    +//     ActionId:  "allow-always"
    +//     回传参数:  [{ name: "action", type: 静态值, value: "allow-always" }]
    +//
    +//   按钮 3 · 拒绝(color: red)
    +//     ActionId:  "deny"
    +//     回传参数:  [{ name: "action", type: 静态值, value: "deny" }]
    +//
    +//   显示控制 → 条件计算:show_approve_btns 的值为 true → 整组可见
    +// ─────────────────────────────────────────────────────────────
    +
    +// channel 端 patch 只 PUT 这两个变量(approval-card-patcher.ts,v3.5):
    +updateCardVariables(outTrackId, {
    +  show_approve_btns: "true",     // ← 显示 approval 3 按钮(D15)
    +  hasAction:         "false",    // ← 隐藏 btn_stop(D23)
    +}, token)
    +// approvalId 通过 markCardRunPendingApproval(outTrackId, approvalId) 写到
    +// card-run-registry record,callback 时反查(D24)

    - 注意:DingTalk 平台会在回调里给每个按钮的 actionId 末尾追加 button index("approval" → "approval0"/"approval1"/"approval2"), - 回调解析使用 startsWith("approval") + params.d 取 decision 即可,详见 §1.2 与 §6.3。 + v3.5 实证:actionId 唯一命名时 DingTalk callback 原样回传,无 index 后缀。 + 回调解析以 params.action 取 decision,详见 §1.2 与 §6.3。

    7.3 双状态机并存(v3.3)

    From da156271a4d5b27ab8cec1cda61a12cc96175a40 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 00:04:21 +0800 Subject: [PATCH 09/44] =?UTF-8?q?docs(spec):=20v3.5.1=20=E6=B5=81=E7=A8=8B?= =?UTF-8?q?=E5=AE=A1=E6=A0=B8=20=E2=80=94=20=E5=90=8C=E6=AD=A5=20=C2=A76?= =?UTF-8?q?=20=E6=95=B0=E6=8D=AE=E6=B5=81=E5=88=B0=20v3.5=20=E5=AE=9E?= =?UTF-8?q?=E6=96=BD=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户重审 approval 完整流程时发现 v3.5 改动遗漏了 §6 数据流里多处描述: 1. §6.2 场景 A deliverPending 描述还在 PUT btns + hasAction:"true"(v3.3 按钮 channel 端构造模型)。v3.5 应该是: - PUT show_approve_btns="true" + hasAction="false" - markCardRunPendingApproval(outTrackId, approvalId) 按钮在 v2 模板内置,channel 不传按钮定义。 2. §6.4 同步流 applyResolvedPatch 描述还在"清按钮 + 写终态指示" (依赖 statusFooter 之类的字段)。v3.5 应该是: - PUT show_approve_btns="false" + hasAction=cardStillActive - clearCardRunPendingApproval(outTrackId) 明确说明 v1 不写终态文字(用户感知"按钮消失=已处理",§7.1 v1 限制)。 3. §6.5 表头改为 "card 按钮 actionId / params.action / 等价命令 / agent 行为"四列,体现 v3.5 三按钮独立 actionId + params.action 编码。 4. §6.6 重复点击:重写为 D24 反查模型 - 先查 pendingApprovalId - 命中 → 调 resolver → already-resolved catch → PUT show_approve_btns=false - 未命中 → 直接 PUT show_approve_btns=false 不调上游 5. §6.6 重启后点旧卡片:no-pending-approval 路径 - 反查 null → PUT show_approve_btns=false return - 不调上游 resolve(无 approvalId 可传) - 上游 approval 由 TTL 兜底 6. §6.6 上游过期事件触达:PUT 字段同步成 show_approve_btns + hasAction + clearCardRunPendingApproval,不再用旧的 status/btns:"[]" 整体语义对齐用户描述的两条路径: - markdown 路径:发文字消息引导用户敲 /approve - card 路径:approve_btns 模板内置,patch 时 show_approve_btns=true 显示 按钮 → 等审批 → resolved 时 show_approve_btns=false 隐藏按钮 → agent 继续执行任务 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 116 +++++++++++------- 1 file changed, 75 insertions(+), 41 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index 758ab4d2..ce017ba2 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -362,6 +362,14 @@

    2. 已确认的决策清单

    (6) §10 PR-1 措辞改"resolve 通道生效,approval id 可见性依赖外部界面,完整 UX 在 PR-2"; (7) §6.7 删除 store.register 残留; (8) §6.3 / §6.6 / §8 / §9 等所有 already-resolved 文案改"ℹ️ 已处理或已过期"并明确 catch 后 return 避免覆盖;§6.8 alias 范围显式列清;§11.2 风险表新增 kind 派发边界条目
  • +
  • v3.5.1(2026-05-19 流程审核):用户重审 approval 完整流程时发现 v3.5 改动没扫干净的 6 处残留—— + (1) §6.2 场景 A 的 deliverPending 描述还在 PUT btns + hasAction:"true"(v3.3 风格),改成 toggle show_approve_btns=true + hasAction=false + markCardRunPendingApproval; + (2) §6.4 同步流的 applyResolvedPatch 描述还在"清按钮 + 写终态指示",改成 toggle show_approve_btns=false + cardStillActive ? hasAction=true + clearCardRunPendingApproval,明确说明 v1 不写终态文字; + (3) §6.5 表头从 statusFooter 改成"card 按钮 actionId / params.action / 等价命令 / agent 行为"四列,明确独立 actionId; + (4) §6.6 重复点击:重写为"先反查 pendingApprovalId,命中/未命中两分支",体现 D24; + (5) §6.6 重启后点旧卡片:改为"反查未命中 → 直接隐藏按钮 + return,不调上游 resolve"(之前还在描述上游 not-found catch); + (6) §6.6 上游过期事件:PUT 字段改成 show_approve_btns + hasAction(之前还写 status + btns:"[]")。 +
    这是流程 end-to-end 一致性修订,不引入新决策
  • v3.5(2026-05-18 用户实配 schema 对齐):用户在 v2 模板上实际配置后揭示 5 处事实修订—— (1) 模板新增 approve_btns(按钮组)+ show_approve_btns(Boolean)两个变量; (2) 三个按钮 actionId 独立命名 allow-once / allow-always / deny无 index 后缀(之前测试 "approve0/1/2" 是因为 3 按钮都叫 approve 重名,DingTalk 才加 index 消歧;唯一名时原样回); @@ -884,23 +892,28 @@

    场景 A:群里 @ agent,agent AI Card 流式中触发 approval(card │ dedupeKey: "dingtalk:default:group:cid_xxx:ai_card_xxx", │ } -调 deliverPending(route=card 分支): +调 deliverPending(route=card 分支,v3.5:toggle 变量 + 反查映射): └─ approval-card-patcher.applyPendingPatch("ai_card_xxx", "abc123", token) - → PUT updateCardVariables("ai_card_xxx", { - btns: JSON.stringify([, , ]), - hasAction: "true", - // D23:approval 期间隐藏 btn_stop,所以 btns 只含 approval 3 个 - }, token) - → entry = { - approvalId:"abc123", - accountId:"default", - mode:"card", - outTrackId:"ai_card_xxx", - } + │ + ├─ PUT updateCardVariables("ai_card_xxx", { + │ show_approve_btns: "true", // 显示 approval 3 按钮(D15 模板内置) + │ hasAction: "false", // 隐藏 btn_stop(D23) + │ }, token) + │ (按钮 actionId/params/颜色都在 v2 模板的 approve_btns 按钮组里固化, + │ channel 不传按钮定义;只 toggle 两个 Boolean 变量) + │ + └─ markCardRunPendingApproval("ai_card_xxx", "abc123") + (D24 反查映射:把 approvalId 写到 card-run-registry record, + 供后续 callback 用 outTrackId 反查) + + → entry = { approvalId:"abc123", accountId:"default", + mode:"card", outTrackId:"ai_card_xxx" } └─ core 把 entry 缓存到 activeEntries → 群里 staffA 和 staffB 在 agent reply card 底部看到 3 按钮 - agent 流式输出暂停(waitDecision 阻塞),等点击 / 命令 / 过期 + (允许一次 / 总是允许 / 拒绝;btn_stop 暂时隐藏) + agent 流式输出暂停(waitDecision 阻塞上游 turn 直到 resolve), + 等用户点按钮 / 敲命令 / 过期

    场景 B:用户在 messageType=markdown 模式触发 exec(markdown 路径)

    前提:messageType=markdown,agent reply 已通过 markdown 消息发出。
    @@ -1051,16 +1064,21 @@ 

    6.3 点击 approve → 上游 resolve(核心交互链路)

    ├─ 分支命中 → 跳过既有 handleCardAction └─ finally: socketCallBackResponse(messageId, { success: true }) (ack 平台)
    -

    6.4 上游 resolve 后的状态同步(v3.3:按 entry.mode 分支)

    +

    6.4 上游 resolve 后的状态同步(v3.5:按 entry.mode 分支)

    点击或命令 → 上游 store 标记 resolved → approval-handler-runtime 对每个 entry 调 transport.updateEntry({ phase: "resolved" })。channel 按 entry.mode 分支:

    -

    mode === "card":在原 agent reply card 上做终态 patch

    +

    mode === "card":在原 agent reply card 上 toggle 变量

    approval abc123 被 staffA 在 agent reply card 上点了"允许一次"
     
            ┌──────────────────────────────────────┐
            │ channel callback handler(§6.3 step 5)│
    -       │   approval-card-patcher.applyResolvedPatch(outTrackId, ...)
    -       │   → PUT 清按钮 + 写终态指示 + 恢复 btn_stop(如 INPUTING)│
    +       │   approval-card-patcher.applyResolvedPatch(outTrackId,
    +       │     decision, cardStillActive, token)
    +       │   → PUT updateCardVariables(outTrackId, {
    +       │       show_approve_btns: "false",     // 隐藏 approval 3 按钮
    +       │       hasAction: cardStillActive ? "true" : "false", // 恢复 btn_stop
    +       │     }, token)
    +       │   → clearCardRunPendingApproval(outTrackId)  // D24 清反查映射
            └──────────────────┬───────────────────┘ ← 第一次 update(同步)
                               │
                               ▼ (resolveApprovalOverGateway 异步返回)
    @@ -1070,7 +1088,7 @@ 

    mode === "card":在原 agent reply card 上做终态 patch

    │ decision = allow-once │ │ resolvedBy = staffA │ └──────────────────┬──────────────────┘ - │ 触发 + │ 触发上游 runtime ▼ presentation.buildResolvedResult({ view, entry }) returns { kind:"update", payload:{ @@ -1080,15 +1098,19 @@

    mode === "card":在原 agent reply card 上做终态 patch

    │ ▼ transport.updateEntry:entry.mode === "card" 分支 - → approval-card-patcher.applyResolvedPatch(entry.outTrackId, - payload.decision, payload.resolverDisplayName, token) + → approval-card-patcher.applyResolvedPatch(entry.outTrackId, ...) → 与 callback handler 的 step 5 内容一致,幂等覆盖 OK + → core 从 activeEntries 移除该 entry - agent reply card 看起来:approval 按钮消失,多一行"✅ 已批准 by @staffA · allow-once", - btn_stop 恢复显示,agent 继续流式输出(如果 decision != deny) + agent reply card 看起来:approval 按钮消失,btn_stop 恢复显示 + (若 card 还 INPUTING),agent 继续流式输出(waitDecision 解除阻塞) + + v3.5 v1 不写"✅ 已批准 by @user"终态文字(schema 无干净字段位, + §7.1 已说明);用户感知"按钮消失=已处理"。v1.x 加 approval_status + 变量后再写 agent reply card 自己的状态机不变(PROCESSING/INPUTING/FINISHED 由 - agent reply 流程控制,approval 只 patch cardParamMap 字段)
    + agent reply 流程控制,approval 只 patch 2 个 Boolean cardParamMap 字段)

    mode === "markdown":v1 不发新通知消息

    transport.updateEntry:entry.mode === "markdown" 分支
    @@ -1101,24 +1123,29 @@ 

    mode === "markdown":v1 不发新通知消息

    v2 future(DM 投递启用后)

    core 会对所有 entry(origin + 每个 DM)调 updateEntry,所有卡片同步刷成相同终态。当前 v1 origin-only 每个 approval 只有 1 个 entry。

    -

    6.5 用户点 deny / allow-always 的差异

    +

    6.5 三个 decision 的差异(v3.5 同步)

    - + - - - + + +
    按钮actionIdstatusFooter 终态
    按钮card 按钮 actionIdparams.action等价 /approve 命令resolve 后 agent 行为
    ✅ 允许一次/approve <id> allow-once✅ 已批准 by @user · 允许一次
    ✅ 总是允许/approve <id> allow-always✅ 已批准 by @user · 总是允许
    ⛔ 拒绝/approve <id> deny⛔ 已拒绝 by @user
    ✅ 允许一次allow-once"allow-once"/approve <id> allow-once本次允许,下次相同操作仍需审批
    ✅ 总是允许allow-always"allow-always"/approve <id> allow-always本次允许 + 加入白名单,相同操作后续免审批
    ⛔ 拒绝deny"deny"/approve <id> deny本次拒绝,agent turn 通常被上游终止
    +

    resolve 是否触发 agent 继续 / 终止由上游 approval-handler-runtime 决定,channel 端只负责传递 decision,不解释语义。

    6.6 失败 / 边界场景

    用户重复点击(按钮看起来还在但已 resolved)

      -
    1. callback-handler 调 resolveApprovalOverGateway
    2. -
    3. 上游返回 already-resolved 类的具名错误(catch 到)
    4. -
    5. catch 分支调 updateCardVariables 把卡片刷成"ℹ️ 已处理或已过期"(即使 UI 还没刷过来也立即修正)
    6. -
    7. 对用户:第二次点击看起来等同于第一次"已生效",无打扰提示
    8. +
    9. callback-handler 解析按钮 → 反查 cardRun.pendingApprovalId
    10. +
    11. 反查可能命中(resolved 后还没来得及 clear)或未命中(已 clear): +
        +
      • 命中:调 resolver → 上游返回 already-resolved → catch 分支 PUT { show_approve_btns: "false" } 把按钮再次隐藏
      • +
      • 未命中:直接 PUT { show_approve_btns: "false" },return "no-pending-approval"
      • +
      +
    12. +
    13. 对用户:第二次点击 = 按钮立即消失,与第一次结果一致,无打扰提示

    非 approver 点击

    @@ -1129,13 +1156,12 @@

    非 approver 点击

  • 日志 [DingTalk][Approval][Denied] approvalId=<id> clicker=<userId>
  • -

    Channel 重启后用户点旧卡片(v3:no-store 模式下的行为)

    +

    Channel 重启后用户点旧卡片(v3.5)

      -
    1. channel 端无本地 store(D18),但 entry 信息从平台回调的 outTrackId 仍能拿到
    2. -
    3. callback-handler 解析 cardPrivateData 拿到 approvalId + decision → 直接调 resolveApprovalOverGateway
    4. -
    5. 上游 core 在重启后 activeEntries 也清空了,resolveApprovalOverGateway 返回 not-found
    6. -
    7. callback-handler 进 catch 分支 → updateCardVariables(outTrackId, { status: "ℹ️ 已处理或已过期", hasAction: "false", btns: "[]" })
    8. -
    9. 用户体感:按钮点了一下卡片刷成"已过期",明确反馈——对长时间下线降级,可接受
    10. +
    11. channel 端 card-run-registry 内存清空 → resolveCardRun(outTrackId).pendingApprovalId 返 null
    12. +
    13. callback-handler 进 "no-pending-approval" 分支 → PUT { show_approve_btns: "false" },return
    14. +
    15. 不调上游 resolve(没 approvalId 可传);上游 approval 由它自己的 TTL 兜底
    16. +
    17. 用户体感:按钮点了一下就消失——降级到"按钮自动消失",对长时间下线可接受

    上游过期事件触达

    @@ -1144,9 +1170,17 @@

    上游过期事件触达

    │ phase: "expired", │ }} └─ transport.updateEntry({ entry, payload, phase: "expired" }) - → entry 由 core 从 activeEntries 取出回传(无需 channel 本地 store) - → PUT /v1.0/card/instances 把 status 改为 "⏰ 已过期",hasAction=false - → core 从 activeEntries 移除该 entry
    + → entry.mode === "card" 分支: + approval-card-patcher.applyExpiredPatch(entry.outTrackId, cardStillActive, token) + → PUT updateCardVariables({ + show_approve_btns: "false", + hasAction: cardStillActive ? "true" : "false", + }) + → clearCardRunPendingApproval(entry.outTrackId) + → entry.mode === "markdown" 分支:no-op + → core 从 activeEntries 移除该 entry + + agent reply card:approval 按钮自动消失,btn_stop 视 card 状态恢复

    Channel stopClient(账号停用 / gateway 重启)—— v3 推迟到 v2

    From 379c01540fc779e7c898bb88848c3468ec7499fc Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 00:06:26 +0800 Subject: [PATCH 10/44] =?UTF-8?q?docs(spec):=20=E6=96=B0=E5=A2=9E=20AI=20C?= =?UTF-8?q?ard=20v3.0.0=20schema=20=E8=B5=84=E4=BA=A7=20+=20=E5=9C=A8=20sp?= =?UTF-8?q?ec=20=E5=BC=95=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 提交用户实配后从开发者平台导出的 AI Card v3.0.0 schema 完整低代码 JSON 到 docs/assets/card-template-v3.json(267KB),含: - approve_btns 按钮组定义(template 内置 3 按钮:allow-once / allow-always / deny,actionId + params 全部固化) - show_approve_btns Boolean 变量(按钮组可见性条件) - 现有 v2 字段保留(content / blockList / quoteContent / hasAction / statusLine / copy_content) spec 修订: - §3.1 ASCII 拓扑加资产引用 - §7.1 加 docs/assets/card-template-v3.json 锚点链接 - §10 阶段 0 把"v2 模板能力确认"替换为"v3 schema 部署任务"——发布到 open-dev、拿 templateId、写到 card-template.ts(替代 vs 并存由 PR-2 实施时定)、真机回归 show_approve_btns toggle Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/assets/card-template-v3.json | 1 + .../2026-05-18-gap-01-approval-native-design.html | 15 ++++++++++++--- 2 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 docs/assets/card-template-v3.json diff --git a/docs/assets/card-template-v3.json b/docs/assets/card-template-v3.json new file mode 100644 index 00000000..39cba483 --- /dev/null +++ b/docs/assets/card-template-v3.json @@ -0,0 +1 @@ +{"editorData":"{\"schemaVersion\":\"3.0.0\",\"schema\":{\"componentsMap\":[{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AIPending\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AIPending\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardStatusContainer\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardStatusContainer\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"BaseText\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"BaseText\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Divider\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Divider\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"ButtonGroup\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"ButtonGroup\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"MarkdownBlock\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"MarkdownBlock\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Image\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Image\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Grid\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Grid\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Loop\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Loop\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Input\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Input\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Column\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Column\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"ColumnLayout\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"ColumnLayout\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"FixedSingleButton\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"FixedSingleButton\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardContent\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardContent\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Icon\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Icon\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AIGenerationProcessing\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AIGenerationProcessing\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Feedback\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Feedback\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardContainer\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardContainer\"}],\"componentsTree\":[{\"componentName\":\"AICardContainer\",\"id\":\"node_ocmncu094g1\",\"props\":{\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePending\":true,\"enableWriting\":true,\"enableFailed\":false,\"enableDoing\":false,\"enableTitle\":false,\"operationPenalType\":\"prompt\",\"summaryContent\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"enableGradientBorder\":true,\"flowStatusVar\":{\"variable\":\"\",\"variableType\":\"global\",\"type\":\"variableValue\"},\"cardSizeMode\":\"adaptive\",\"cardSizeHeightMode\":\"adaptive\",\"cardSizeWidthMode\":\"adaptive\",\"cardSizeHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":226,\"variable\":\"\",\"variableType\":\"global\"},\"hasBackground\":false,\"backgroundType\":\"Standard\",\"standardBackgroundColor\":\"gray\",\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"enableFlowAbort\":true,\"enableEngineUpgrade\":false,\"enableExposeStatPoint\":false,\"enableDebugTool\":false},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_ocmncu094g2\",\"props\":{\"status\":1,\"enableExtend\":false,\"enableCollapse\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0},\"title\":\"处理中状态\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AIPending\",\"id\":\"node_ocmncu094g3\",\"props\":{\"pendingTip\":{\"i18n\":true,\"type\":\"dynamicString\",\"content\":{\"zh_Hans\":\"思考中...\",\"zh_Hant\":\"Progressing...\",\"en_US\":\"Progressing...\",\"ja_JP\":\"進行中...\",\"vi_VN\":\"Progressing...\",\"th_TH\":\"Progressing...\",\"id_ID\":\"Progressing...\"}},\"style\":\"embed\",\"hideIcon\":false},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_ocmncu094g4\",\"props\":{\"status\":2,\"enableExtend\":false,\"enableCollapse\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0},\"title\":\"输出中状态\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardContent\",\"id\":\"node_ocmncu094g5\",\"props\":{\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0,\"transformToEventChain\":false,\"disabledWhileForward\":false,\"enableStatPoint\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}]},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmncu094gf\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${quoteContent}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_DTXX_message_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":true,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Divider\",\"id\":\"node_ocmncu094gg\",\"props\":{\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"height\":30,\"direction\":\"horizontal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"color\":\"#1F111F2C\",\"margin\":-2,\"innerOffset\":0},\"title\":\"分割线\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Loop\",\"id\":\"node_ocmncu094gh\",\"props\":{\"listData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"blockList\"},\"direction\":\"vertical\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"isFixedWidth\":false,\"isAutoWidth\":false,\"width\":50,\"equalSpace\":false,\"flowLayout\":false,\"childGap\":false,\"childGapSize\":4,\"childDivider\":false,\"childDividerMarginLeft\":0,\"childDividerMarginRight\":0,\"childDividerMarginBottom\":0,\"childDividerMarginTop\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"scrollable\":false,\"childWidth\":\"match_content\",\"childDividerWidth\":0.5,\"childDividerColorDark\":\"rgba(255, 255, 255, 0.12)\",\"childDividerColorLight\":\"rgba(17, 31, 44, 0.12)\",\"paging\":false,\"hasMore\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onLoadMore\":{\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false}},\"isFixedHeight\":false,\"height\":100,\"flowDirection\":\"x\",\"margin\":0,\"innerOffset\":0},\"title\":\"循环渲染容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"ButtonGroup\",\"id\":\"node_ocmnee6af5g\",\"props\":{\"dynamicButtons\":{\"variable\":\"blockList[0].btns\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"blockList[0].btns\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"responsiveLayoutWidth\":350,\"buttonsSource\":\"variable\",\"fixedButtonIds\":[],\"fixedButtons\":[],\"enableResponsiveLayout\":false,\"matchContent\":false,\"buttonSpacing\":8,\"margin\":-2,\"innerOffset\":0},\"title\":\"审批按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"MarkdownBlock\",\"id\":\"node_ocmncu094g6\",\"props\":{\"isStreaming\":false,\"content\":{\"variable\":\"blockList[0].markdown\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"lt\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}}},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Image\",\"id\":\"node_ocmnd5z3kwq\",\"props\":{\"images\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"@lALPDfmVRLVVAm_NBdzNBdw\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"darkModeImages\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"single\":true,\"height\":{\"type\":\"dynamicWidth\",\"valueType\":\"fixed\",\"value\":200,\"variableType\":\"global\",\"variable\":\"\",\"full\":false,\"adaptive\":false},\"width\":{\"type\":\"dynamicWidth\",\"valueType\":\"full\",\"value\":100,\"variableType\":\"global\",\"variable\":\"\",\"full\":true,\"adaptive\":false},\"cornerRadius\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":6,\"variable\":\"\",\"variableType\":\"global\"},\"scaleType\":\"centerCrop\",\"enablePreview\":true,\"enableBorder\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"offsetLeft\":0,\"offsetRight\":0,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"adaptiveSize\":false,\"fixedRatio\":false,\"borderColor\":\"#297f8790\",\"enableStatPoint\":false,\"imageType\":\"adaptiveSize\",\"aspectRatio\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"margin\":-2,\"innerOffset\":0},\"title\":\"图片\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"BaseText\",\"id\":\"node_ocmnlxveuu8\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":0,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"图片标注\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Grid\",\"id\":\"node_ocmnd5z3kwx\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":true,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":5,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"childGravity\":\"center\",\"hasBackground\":true,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":-2,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Custom\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"extended_green0_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":16,\"cornerRadiusLeftTop\":16,\"cornerRadiusRightTop\":16,\"cornerRadiusRightBottom\":16,\"cornerRadiusLeftBottom\":16,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#cbf797\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#a4e191\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"enableClickEvent\":true,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"主题布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnd5z3kwy\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level2_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_tiny_bold_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]},{\"componentName\":\"Grid\",\"id\":\"node_ocmnlxveuu1\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"leftTop\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"MarkdownBlock\",\"id\":\"node_ocmnj1dw4n1\",\"props\":{\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"content\":{\"variable\":\"content\",\"variableType\":\"global\",\"type\":\"variableValue\",\"varType\":\"markdown\"},\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isMarkdownNotEmpty\",\"variable\":\"content\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"isStreaming\":true,\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"marginTop\":0,\"marginBottom\":0,\"marginLeft\":0,\"marginRight\":0},\"title\":\"流式答案\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"ColumnLayout\",\"id\":\"node_ocmnee6af51\",\"props\":{\"columnCount\":2,\"columnWidth\":[{\"widthMode\":\"weighted\",\"weight\":1,\"width\":50},{\"widthMode\":\"fixed\",\"weight\":1,\"width\":80}],\"columnSpacing\":8,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isTrue\",\"variable\":\"hasAction\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"childGravity\":\"center\",\"enableResponsiveLayout\":false,\"responsiveLayout\":1,\"margin\":-2,\"innerOffset\":0},\"title\":\"Action按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Column\",\"id\":\"node_ocmnee6af52\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"center\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Input\",\"id\":\"node_ocmnee6af5i\",\"props\":{\"placeholder\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"添加指引\"},\"currentValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"message\":{\"type\":\"dynamicString\",\"content\":\"请输入内容\",\"i18n\":false},\"title\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"添加指引\"},\"id\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"guideInput\"},\"params\":[{\"type\":\"builtIn\",\"variable\":\"\",\"value\":\"选中会话列表\",\"name\":\"guideInput\",\"variableType\":\"global\",\"id\":\"__built_in_inputResult__\"}],\"visible\":{\"type\":\"dynamicVisible\",\"value\":false,\"valueType\":\"fixed\",\"condition\":{\"op\":\"or\",\"conditions\":[]}},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"actionType\":\"request\",\"localVarAction\":{\"variable\":\"guideString\",\"variableType\":\"global\",\"varType\":\"string\",\"type\":\"variableValue\"},\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"inlineMode\":false,\"textArea\":false,\"minRows\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"maxRows\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":6,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"margin\":0,\"innerOffset\":0},\"title\":\"指引文本输入\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"Column\",\"id\":\"node_ocmnee6af53\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"rightCenter\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Grid\",\"id\":\"node_ocmnee6af55\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":true,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"rightCenter\",\"hasBackground\":true,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Custom\",\"backgroundType\":\"Custom\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"extended_red0_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"rgba(255,53,0,0.95)\",\"darkModeBackgroundColor\":\"#ca0505\",\"cornerRadius\":20,\"cornerRadiusLeftTop\":20,\"cornerRadiusRightTop\":20,\"cornerRadiusRightBottom\":20,\"cornerRadiusLeftBottom\":20,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#fcbb50\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f28326\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"actionType\":\"actionSheet\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":true,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"确认\"},\"confirmMessage\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"是否立即中止运行?\"},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"},\"actionSheetMessage\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"是否中止任务?\"},\"actionSheetItems\":[{\"id\":\"0\",\"actionSheetStyle\":\"destructive\",\"actionSheetName\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"立即中止\"},\"actionSheetDesc\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"},\"actionIconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_raise_hand\"},\"actionSheetAction\":\"request\",\"actionSheetRequestItemActionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"btn_stop\"},\"actionSheetRequestItemParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"true\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetRequestItemSuccessToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetUrlItemUrlType\":\"all\",\"actionSheetUrlItemAndroidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemPcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIsDtmd\":false,\"actionSheetUrlItemUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetOpenModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetOpenModalHeight\":500,\"actionSheetOpenModalWidth\":400,\"actionSheetApiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetApiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetApiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"}},{\"id\":\"1\",\"actionSheetStyle\":\"default\",\"actionSheetName\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"取消\"},\"actionSheetDesc\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"},\"actionIconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"actionSheetAction\":\"none\",\"actionSheetRequestItemActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetRequestItemParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetRequestItemSuccessToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetUrlItemUrlType\":\"all\",\"actionSheetUrlItemAndroidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemPcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIsDtmd\":false,\"actionSheetUrlItemUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetOpenModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetOpenModalHeight\":500,\"actionSheetOpenModalWidth\":400,\"actionSheetApiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetApiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetApiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"}}],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[{\"event\":{\"actionType\":\"url\",\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false}}],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"enableClickEvent\":true,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnee6af56\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"中止\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_raise_hand\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":6,\"marginRight\":6,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Custom\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level2_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_action_bold_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":false,\"enableIcon\":true,\"margin\":6,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]},{\"componentName\":\"BaseText\",\"id\":\"node_ocmncu094gn\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${statusLine}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_ai_diagonal_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":true,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"ButtonGroup\",\"id\":\"node_ocmpbcor1i2\",\"props\":{\"dynamicButtons\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"btns\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"and\",\"conditions\":[{\"value\":\"\",\"op\":\"isTrue\",\"variable\":\"show_approve_btns\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"responsiveLayoutWidth\":350,\"buttonsSource\":\"fixed\",\"fixedButtonIds\":[\"1\",\"2\",\"3\"],\"fixedButtons\":[{\"id\":\"1\",\"type\":\"button\"},{\"id\":\"2\",\"type\":\"button\"},{\"id\":\"3\",\"type\":\"button\"}],\"enableResponsiveLayout\":false,\"matchContent\":false,\"buttonSpacing\":8,\"margin\":-2,\"innerOffset\":0},\"title\":\"按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"FixedSingleButton\",\"id\":\"node_ocmpbcor1i3\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"允许一次\"},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"blue\",\"variable\":\"\",\"variableType\":\"global\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"$iwElAqNwbmcDBgTRBOYF0QTmBrDiyibODEbGnAngitJlqmwABwAIAAm3aW50ZXJhY3RpdmUtY2FyZC1lZGl0b3IKAAsA\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkModeIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"enableIcon\":true,\"iconPosition\":\"left\",\"iconType\":\"image\",\"iconFont\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"\"},\"variable\":\"\",\"variableType\":\"global\"},\"enableCornerMark\":false,\"cornerMarkPosition\":\"right\",\"cornerMarkText\":{\"type\":\"dynamicString\",\"content\":\"按钮角标文案\",\"i18n\":false},\"cornerMarkTextColor\":\"white\",\"cornerBackgroundColorType\":\"standard\",\"standardCornerBackgroundColor\":\"RedGradient\",\"customCornerBackgroundColor\":\"#FF6A00\",\"customCornerDarkModeBackgroundColor\":\"#FF6A00\",\"actionType\":\"request\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"allow-once\"},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"allow-once\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableClickEvent\":true,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"widthMode\":\"match_parent\",\"width\":200,\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePadButtonText\":false,\"cornerMarkVisible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"按钮1\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"FixedSingleButton\",\"id\":\"node_ocmpbcor1i4\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"总是允许\"},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"blue\",\"variable\":\"\",\"variableType\":\"global\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"$iwELAqNwbmcDBgTRBOYF0QTmBrAVv0YTQHioCwngjGjME4IABwAIAAm3aW50ZXJhY3RpdmUtY2FyZC1lZGl0b3IKAAsA\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkModeIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"enableIcon\":true,\"iconPosition\":\"left\",\"iconType\":\"image\",\"iconFont\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"variable\":\"\",\"variableType\":\"global\"},\"enableCornerMark\":false,\"cornerMarkPosition\":\"right\",\"cornerMarkText\":{\"type\":\"dynamicString\",\"content\":\"按钮角标文案\",\"i18n\":false},\"cornerMarkTextColor\":\"white\",\"cornerBackgroundColorType\":\"standard\",\"standardCornerBackgroundColor\":\"RedGradient\",\"customCornerBackgroundColor\":\"#FF6A00\",\"customCornerDarkModeBackgroundColor\":\"#FF6A00\",\"actionType\":\"request\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"allow-always\"},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"allow-always\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableClickEvent\":true,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"widthMode\":\"match_parent\",\"width\":200,\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePadButtonText\":false,\"cornerMarkVisible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"按钮2\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"FixedSingleButton\",\"id\":\"node_ocmpbcor1i5\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"拒绝\"},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"blue\",\"variable\":\"\",\"variableType\":\"global\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"$iwElAqNwbmcDBgTRBOYF0QTmBrBhBQt9e-UVYgngjO_OkvEABwAIAAm3aW50ZXJhY3RpdmUtY2FyZC1lZGl0b3IKAAsA\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkModeIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"enableIcon\":true,\"iconPosition\":\"left\",\"iconType\":\"image\",\"iconFont\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"variable\":\"\",\"variableType\":\"global\"},\"enableCornerMark\":false,\"cornerMarkPosition\":\"right\",\"cornerMarkText\":{\"type\":\"dynamicString\",\"content\":\"按钮角标文案\",\"i18n\":false},\"cornerMarkTextColor\":\"white\",\"cornerBackgroundColorType\":\"standard\",\"standardCornerBackgroundColor\":\"RedGradient\",\"customCornerBackgroundColor\":\"#FF6A00\",\"customCornerDarkModeBackgroundColor\":\"#FF6A00\",\"actionType\":\"request\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"deny\"},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"deny\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableClickEvent\":true,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"widthMode\":\"match_parent\",\"width\":200,\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePadButtonText\":false,\"cornerMarkVisible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"按钮3\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]},{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_ocmncu094g7\",\"props\":{\"status\":3,\"enableExtend\":false,\"enableCollapse\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0},\"title\":\"完成状态\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardContent\",\"id\":\"node_ocmncu094g8\",\"props\":{\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0,\"transformToEventChain\":false,\"disabledWhileForward\":false,\"enableStatPoint\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}]},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnd5z3kwa\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${quoteContent}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_DTXX_message_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":true,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"Divider\",\"id\":\"node_ocmnd5z3kwb\",\"props\":{\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"height\":30,\"direction\":\"horizontal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"color\":\"#1F111F2C\",\"margin\":-2,\"innerOffset\":0},\"title\":\"分割线\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"Loop\",\"id\":\"node_ocmnd5z3kwc\",\"props\":{\"listData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"blockList\"},\"direction\":\"vertical\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"isFixedWidth\":false,\"isAutoWidth\":false,\"width\":50,\"equalSpace\":false,\"flowLayout\":false,\"childGap\":false,\"childGapSize\":4,\"childDivider\":false,\"childDividerMarginLeft\":0,\"childDividerMarginRight\":0,\"childDividerMarginBottom\":0,\"childDividerMarginTop\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"scrollable\":false,\"childWidth\":\"match_content\",\"childDividerWidth\":0.5,\"childDividerColorDark\":\"rgba(255, 255, 255, 0.12)\",\"childDividerColorLight\":\"rgba(17, 31, 44, 0.12)\",\"paging\":false,\"hasMore\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onLoadMore\":{\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false}},\"isFixedHeight\":false,\"height\":100,\"flowDirection\":\"x\",\"margin\":0,\"innerOffset\":0},\"title\":\"循环渲染容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true,\"children\":[{\"componentName\":\"MarkdownBlock\",\"id\":\"node_ocmnd5z3kwf\",\"props\":{\"isStreaming\":false,\"content\":{\"variable\":\"blockList[0].markdown\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"lt\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}}},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Image\",\"id\":\"node_ocmnd5z3kws\",\"props\":{\"images\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"@lALPDfmVRLVVAm_NBdzNBdw\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"darkModeImages\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"single\":true,\"height\":{\"type\":\"dynamicWidth\",\"valueType\":\"fixed\",\"value\":200,\"variableType\":\"global\",\"variable\":\"\",\"full\":false,\"adaptive\":false},\"width\":{\"type\":\"dynamicWidth\",\"valueType\":\"full\",\"value\":100,\"variableType\":\"global\",\"variable\":\"\",\"full\":true,\"adaptive\":false},\"cornerRadius\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":6,\"variable\":\"\",\"variableType\":\"global\"},\"scaleType\":\"centerCrop\",\"enablePreview\":true,\"enableBorder\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"offsetLeft\":0,\"offsetRight\":0,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"adaptiveSize\":false,\"fixedRatio\":false,\"borderColor\":\"#297f8790\",\"enableStatPoint\":false,\"imageType\":\"adaptiveSize\",\"aspectRatio\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"margin\":-2,\"innerOffset\":0},\"title\":\"图片\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"BaseText\",\"id\":\"node_ocmnlxveuu9\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":0,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"图片标注\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"Grid\",\"id\":\"node_ocmndcaakx3\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":true,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":5,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"childGravity\":\"center\",\"hasBackground\":true,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":-2,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Custom\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"extended_green0_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":16,\"cornerRadiusLeftTop\":16,\"cornerRadiusRightTop\":16,\"cornerRadiusRightBottom\":16,\"cornerRadiusLeftBottom\":16,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#cbf797\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#a4e191\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"enableClickEvent\":true,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"主题布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true,\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmndcaakx4\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level2_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_tiny_bold_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"ButtonGroup\",\"id\":\"node_ocmnd5z3kwt\",\"props\":{\"dynamicButtons\":{\"variable\":\"blockList[0].btns\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\",\"value\":4}]}},\"responsiveLayoutWidth\":350,\"buttonsSource\":\"variable\",\"fixedButtonIds\":[],\"fixedButtons\":[],\"enableResponsiveLayout\":false,\"matchContent\":false,\"buttonSpacing\":8,\"margin\":-2,\"innerOffset\":0},\"title\":\"审批按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true}]},{\"componentName\":\"ColumnLayout\",\"id\":\"node_ocmnd5z3kwg\",\"props\":{\"columnCount\":2,\"columnWidth\":[{\"widthMode\":\"weighted\",\"weight\":1,\"width\":50},{\"widthMode\":\"fixed\",\"weight\":1,\"width\":30}],\"columnSpacing\":5,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":12,\"marginBottom\":12,\"childGravity\":\"leftBottom\",\"enableResponsiveLayout\":false,\"responsiveLayout\":1,\"margin\":12,\"innerOffset\":0},\"title\":\"分栏\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true,\"children\":[{\"componentName\":\"Column\",\"id\":\"node_ocmnd5z3kwh\",\"props\":{\"isAutoHeight\":true,\"height\":26,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"leftCenter\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnd5z3kwi\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${statusLine}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_ai_diagonal_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":true,\"enableIcon\":false,\"margin\":0,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"Column\",\"id\":\"node_ocmnd5z3kwj\",\"props\":{\"isAutoHeight\":true,\"height\":25,\"direction\":\"horizontal\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"rightCenter\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0,\"enableClickEvent\":true,\"actionType\":\"copy\",\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyType\":\"common\",\"copyValue\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${copy_content}\"},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"customHandler\":\"\",\"customDurboHandler\":\"\",\"enableCustomLocalData\":false,\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"customLocalData\":\"{}\",\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[{\"event\":{\"actionType\":\"copy\",\"copyType\":\"full-content\",\"copyValue\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"}}}],\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectParams\":[],\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentMode\":\"predefine\",\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false}},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Icon\",\"id\":\"node_ocmnlxveuu7\",\"props\":{\"marginLeft\":5,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"styleType\":\"custom\",\"styleToken\":\"common_body_text_style\",\"icon\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_duplicate\"},\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"size\":\"large\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"margin\":-2,\"innerOffset\":0},\"title\":\"图标\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]},{\"componentName\":\"Feedback\",\"id\":\"node_ocmncu094ga\",\"props\":{\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"visible\":{\"type\":\"dynamicVisible\",\"value\":false,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"marginConfigurable\":false,\"enableLikeDislike\":false,\"enableCopy\":true,\"copyType\":\"variable\",\"copyVariable\":{\"type\":\"variableValue\",\"variable\":\"content\",\"variableType\":\"global\",\"varType\":\"markdown\"},\"enableDivider\":false,\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0,\"enableRefresh\":false,\"actionType\":\"none\",\"refreshParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"refreshSuccessCondition\":{\"op\":\"and\",\"conditions\":[]},\"refreshSuccessToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"refreshFailureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"refreshVisible\":{\"type\":\"dynamicVisible\",\"value\":false,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"customRightArea\":{\"type\":\"JSSlot\",\"title\":\"插槽容器\",\"id\":\"node_ocmncu094gb\"}},\"title\":\"操作区\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AIGenerationProcessing\",\"id\":\"node_ocmncu094gc\",\"props\":{},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]}],\"i18n\":{},\"version\":\"1.0.0\"},\"mockData\":{\"cardData\":{\"approve_btns\":[{\"text\":\"次按钮\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"openLink\",\"params\":{\"url\":\"https://www.dingtalk.com\"}}},{\"text\":\"主按钮\",\"color\":\"blue\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}}],\"show_approve_btns\":true,\"statusLine\":\"gpt-5.4 | low | main\\n2m13s | ↑1.2k(C:800) ↓350 | DAPI+12\",\"copy_content\":\"\",\"hasAction\":true,\"content\":\"# 流式富文本内容\",\"quoteContent\":\"这是一条测试引用文本\",\"blockList\":[{\"text\":\"思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"type\":1,\"markdown\":\"> 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"mediaId\":\"\"},{\"text\":\"测试主题\",\"type\":5,\"markdown\":\"# markdown 内容\",\"mediaId\":\"\"},{\"markdown\":\"## 富文本二级标题\\n富文本正文\\n![外链图片测试](https://static.dingtalk.com/media/lADPDetfXH_Pn3HNAbrNBDg_1080_442.jpg)\",\"type\":0,\"text\":\"\",\"mediaId\":\"\"},{\"text\":\" Exec: 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"type\":2,\"markdown\":\"> Exec: 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"mediaId\":\"\"},{\"type\":4,\"btns\":[{\"text\":\"允许\",\"color\":\"blue\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}},{\"text\":\"本次允许\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}},{\"text\":\"驳回\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}}],\"text\":\"\",\"markdown\":\"# markdown 内容\",\"mediaId\":\"\"},{\"type\":3,\"mediaId\":\"@lALPDfmVRLVVAm_NBdzNBdw\",\"text\":\"图片标注123\",\"markdown\":\"# markdown 内容\"}],\"version\":1,\"flowStatus\":2,\"_IC_AIGC_DETAIL_URL\":{\"mob\":\"https://dingtalk.com\",\"pc\":\"https://dingtalk.com\"},\"btns\":[{\"text\":\"次按钮\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"openLink\",\"params\":{\"url\":\"https://www.dingtalk.com\"}}},{\"text\":\"主按钮\",\"color\":\"blue\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}}]},\"cardPrivateData\":{},\"localData\":{\"copy_content\":\"\",\"hasGuide1\":true,\"hasQuote\":true,\"quoteContent\":\"\",\"blockList\":[{\"text\":\"\",\"markdown\":\"# markdown 内容\",\"isTool\":true,\"type\":0,\"mediaId\":\"\",\"topic\":\"\",\"topics\":[{\"text\":\"\"}]}],\"taskInfo\":{\"model\":\"\",\"effort\":\"\",\"dap_usage\":0,\"taskTime\":0,\"dapi_usage\":0,\"agent\":\"\"},\"hasAction\":true,\"topic\":{\"text\":\"\",\"color\":\"\"},\"hasTopic\":true,\"version\":0,\"hasGuide\":false,\"guideString\":\"\",\"statusline\":\"\"},\"richTextData\":{\"cardData\":{\"content\":{\"items\":[{\"data\":{\"text\":\"流式富文本内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"},\"blockList\":[{\"markdown\":{\"items\":[{\"data\":{\"content\":[{\"data\":{\"text\":\"思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\"},\"style\":{\"colorTokenV2\":\"common_level2_base_color\",\"darkColor\":\"#A5A5A6\",\"lightColor\":\"#747576\",\"lineHeightToken\":\"common_body_text_style__line_height\",\"sizeToken\":\"common_h5_text_style__font_size\"},\"type\":\"text\"}]},\"type\":\"quote\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"富文本二级标题\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h2_text_style__line_height\",\"size\":19,\"sizeToken\":\"common_h2_text_style__font_size\"},\"type\":\"text\"},{\"data\":{},\"style\":{\"gap\":14},\"type\":\"paragraphSpace\"},{\"data\":{\"text\":\"富文本正文 \"},\"style\":{\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.5,\"lineHeightToken\":\"common_body_text_style__line_height\",\"size\":14,\"sizeToken\":\"common_body_text_style__font_size\"},\"type\":\"text\"},{\"data\":{\"alt\":\"外链图片测试\",\"url\":\"https://static.dingtalk.com/media/lADPDetfXH_Pn3HNAbrNBDg_1080_442.jpg\"},\"style\":{},\"type\":\"image\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"content\":[{\"data\":{\"text\":\"Exec: 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\"},\"style\":{\"colorTokenV2\":\"common_level2_base_color\",\"darkColor\":\"#A5A5A6\",\"lightColor\":\"#747576\",\"lineHeightToken\":\"common_body_text_style__line_height\",\"sizeToken\":\"common_h5_text_style__font_size\"},\"type\":\"text\"}]},\"type\":\"quote\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}}]},\"localData\":{\"blockList\":[{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}}]}}},\"renderContext\":{\"regenerateEnabled\":\"1\",\"regenerateIndex\":\"2\",\"regenerateTotal\":\"5\"},\"editVersion\":0,\"customWidgetInfo\":\"\",\"useCustomWidgetInfo\":false,\"variableList\":[{\"name\":\"version\",\"private\":false,\"type\":\"number\",\"id\":\"version\",\"description\":\"配置版本号\",\"editorVarType\":\"variables\"},{\"id\":\"content\",\"type\":\"markdown\",\"name\":\"content\",\"description\":\"大模型输出的流式markdown内容\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":false},{\"name\":\"quoteContent\",\"private\":false,\"type\":\"string\",\"id\":\"quoteContent\",\"description\":\"引用文本\",\"editorVarType\":\"variables\"},{\"name\":\"blockList\",\"private\":false,\"type\":\"loopArray\",\"id\":\"blockList\",\"description\":\"动态消息组\",\"editorVarType\":\"variables\",\"disabled\":false,\"schema\":[{\"id\":\"blockList[0].text\",\"type\":\"string\",\"name\":\"text\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"示例文本\"},{\"id\":\"blockList[0].markdown\",\"type\":\"markdown\",\"name\":\"markdown\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"正文\"},{\"id\":\"blockList[0].type\",\"type\":\"number\",\"name\":\"type\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"消息类型\"},{\"id\":\"blockList[0].mediaId\",\"type\":\"string\",\"name\":\"mediaId\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"图片id\"},{\"id\":\"blockList[0].btns\",\"type\":\"buttonGroup\",\"name\":\"btns\",\"private\":false,\"editorVarType\":\"variables\",\"schema\":[{\"id\":\"blockList[0].btns[0].text\",\"type\":\"string\",\"name\":\"text\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮文案\"},{\"id\":\"blockList[0].btns[0].color\",\"type\":\"string\",\"name\":\"color\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮颜色\"},{\"id\":\"blockList[0].btns[0].status\",\"type\":\"string\",\"name\":\"status\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮状态\"},{\"id\":\"blockList[0].btns[0].event\",\"type\":\"dynamicEvent\",\"name\":\"event\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮点击事件\",\"schema\":[{\"id\":\"blockList[0].btns[0].event.type\",\"type\":\"string\",\"name\":\"type\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件类型,支持有 openLink,sendCardRequest\"},{\"id\":\"blockList[0].btns[0].event.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件参数\",\"schema\":[{\"id\":\"blockList[0].btns[0].event.params.url\",\"type\":\"string\",\"name\":\"url\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"点击时打开的链接,可以配置为对象 { android: string; ios: string; pc: string } 分别给不同的平台设置不同的链接,当 type 为 openLink 时生效\"},{\"id\":\"blockList[0].btns[0].event.params.actionId\",\"type\":\"string\",\"name\":\"actionId\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"标记回传请求的 id,当 type 为 sendCardRequest 时生效\"},{\"id\":\"blockList[0].btns[0].event.params.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"回传请求的参数,当 type 为 sendCardRequest 时生效\"}]}]}],\"description\":\"动态按钮组\"}]},{\"name\":\"hasAction\",\"private\":false,\"type\":\"boolean\",\"id\":\"hasAction\",\"description\":\"有互动按钮\",\"editorVarType\":\"variables\"},{\"name\":\"statusLine\",\"private\":false,\"type\":\"string\",\"id\":\"statusLine\",\"description\":\"脚标字符串\",\"editorVarType\":\"variables\"},{\"name\":\"copy_content\",\"private\":false,\"type\":\"string\",\"id\":\"copy_content\",\"description\":\"回答转义字串\",\"editorVarType\":\"variables\"},{\"name\":\"approve_btns\",\"private\":false,\"type\":\"buttonGroup\",\"id\":\"approve_btns\",\"description\":\"审批按钮组\",\"editorVarType\":\"variables\",\"disabled\":false,\"schema\":[{\"id\":\"approve_btns[0].text\",\"type\":\"string\",\"name\":\"text\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮文案\"},{\"id\":\"approve_btns[0].color\",\"type\":\"string\",\"name\":\"color\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮颜色\"},{\"id\":\"approve_btns[0].status\",\"type\":\"string\",\"name\":\"status\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮状态\"},{\"id\":\"approve_btns[0].event\",\"type\":\"dynamicEvent\",\"name\":\"event\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮点击事件\",\"schema\":[{\"id\":\"approve_btns[0].event.type\",\"type\":\"string\",\"name\":\"type\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件类型,支持有 openLink,sendCardRequest\"},{\"id\":\"approve_btns[0].event.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件参数\",\"schema\":[{\"id\":\"approve_btns[0].event.params.url\",\"type\":\"string\",\"name\":\"url\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"点击时打开的链接,可以配置为对象 { android: string; ios: string; pc: string } 分别给不同的平台设置不同的链接,当 type 为 openLink 时生效\"},{\"id\":\"approve_btns[0].event.params.actionId\",\"type\":\"string\",\"name\":\"actionId\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"标记回传请求的 id,当 type 为 sendCardRequest 时生效\"},{\"id\":\"approve_btns[0].event.params.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"回传请求的参数,当 type 为 sendCardRequest 时生效\"}]}]}]},{\"name\":\"show_approve_btns\",\"private\":false,\"type\":\"boolean\",\"id\":\"show_approve_btns\",\"description\":\"是否有审批按钮组\",\"editorVarType\":\"variables\"},{\"id\":\"_IC_AIGC_DETAIL_URL\",\"name\":\"_IC_AIGC_DETAIL_URL\",\"editorVarType\":\"variables\",\"type\":\"object\",\"disabled\":true,\"visible\":false,\"private\":false,\"description\":\"内容生成过程链接\",\"schema\":[{\"id\":\"_IC_AIGC_DETAIL_URL.mob\",\"type\":\"string\",\"name\":\"mob\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"移动端链接\"},{\"id\":\"_IC_AIGC_DETAIL_URL.pc\",\"type\":\"string\",\"name\":\"pc\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"桌面端链接\"}]},{\"id\":\"flowStatus\",\"type\":\"string\",\"name\":\"flowStatus\",\"description\":\"AI卡片状态,包含 pending(1)、writing(2)、done(3)、doing(4)、failed(5)\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"visible\":false}],\"formList\":[],\"customContextList\":[],\"expList\":[],\"localList\":[],\"hsfList\":[],\"lwpList\":[],\"pageData\":{},\"extension\":{\"extendType\":\"AI\",\"aiStatusList\":[1,2,3],\"fileTypeList\":[{\"fileName\":\"event_chain\",\"fileSource\":\"{\\\"node_ocmnee6af5i_input\\\":{\\\"main_success_left_callback\\\":{\\\"type\\\":\\\"dtSendOutData\\\",\\\"params\\\":{\\\"actionType\\\":\\\"0\\\",\\\"cardInstanceId\\\":\\\"@data{data.cardInstanceId}\\\",\\\"actionId\\\":\\\"@toStr{'guideInput'}\\\",\\\"dataKey\\\":\\\"main_success_left_callback\\\",\\\"actionData\\\":{\\\"context\\\":\\\"@dtMapAppend{@data{data.renderContext},'platform','im','platformBizId',@data{data.renderContext.mid}}\\\",\\\"cardPrivateData\\\":{\\\"params\\\":\\\"@dtMapAppend{@dtMapAppend{null},@toStr{'guideInput'},@dtGetEventChainData{'main_success.value'}}\\\",\\\"actionIds\\\":[\\\"@toStr{'guideInput'}\\\"]}},\\\"requestStatusKey\\\":\\\"@concat{@toStr{'guideInput'},'_request_status'}\\\"},\\\"callback\\\":{}},\\\"main\\\":{\\\"type\\\":\\\"dtOpenLink\\\",\\\"params\\\":{},\\\"next\\\":\\\"@triple{@equal{'normal','normal'},'$(main_success)',''}\\\"},\\\"main_success\\\":{\\\"type\\\":\\\"dtCallAPI\\\",\\\"params\\\":{\\\"type\\\":\\\"callJSAPI\\\",\\\"params\\\":{\\\"apiName\\\":\\\"device.notification.prompt\\\",\\\"params\\\":{\\\"message\\\":\\\"@toStr{'请输入内容'}\\\",\\\"title\\\":\\\"@toStr{'添加指引'}\\\",\\\"buttonLabels\\\":\\\"@triple{@equal{@data{env},'pc'},@dtArrayAppend{null,@dti18NAdapter{'提交','提交','Submit'},@dti18NAdapter{'取消','取消','Cancel'}},@dtArrayAppend{null,@dti18NAdapter{'取消','取消','Cancel'},@dti18NAdapter{'提交','提交','Submit'}}}\\\",\\\"defaultText\\\":\\\"@toStr{''}\\\"}},\\\"dataKey\\\":\\\"main_success\\\"},\\\"dataKey\\\":\\\"main_success\\\",\\\"callback\\\":{\\\"success\\\":\\\"@triple{@equal{@toStr{@dtGetEventChainData{'main_success.buttonIndex'}},@triple{@equal{@data{env},'pc'},0,1}},'$(main_success_left_callback)',''}\\\"}}},\\\"ai_card_share_node_ocmncu094ga\\\":{\\\"main\\\":{\\\"type\\\":\\\"dtSendOutData\\\",\\\"params\\\":{\\\"actionType\\\":\\\"0\\\",\\\"cardInstanceId\\\":\\\"@data{data.cardInstanceId}\\\",\\\"actionId\\\":\\\"sys_action_ai_card_share\\\",\\\"requestStatusKey\\\":\\\"sys_action_ai_card_share_loading_status\\\",\\\"actionData\\\":{\\\"context\\\":\\\"@dtMapAppend{@data{data.renderContext},'platform','im','platformBizId',@data{data.renderContext.mid}}\\\",\\\"cardPrivateData\\\":{\\\"params\\\":{\\\"command\\\":\\\"shareAIConv\\\",\\\"params\\\":\\\"@data{data.cardData._IC_SHARE_CARD.params}\\\"},\\\"actionIds\\\":[\\\"sys_action_ai_card_share\\\"]}},\\\"dataKey\\\":\\\"main\\\"},\\\"dataKey\\\":\\\"main\\\",\\\"callback\\\":{\\\"success\\\":\\\"@triple{@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.isSuccess},'$(main_success_left_callback)','$(main_success_right_callback)'}\\\",\\\"failure\\\":\\\"$(main_success_right_callback)\\\"}},\\\"main_success_left_callback\\\":{\\\"type\\\":\\\"dtOpenLink\\\",\\\"params\\\":{\\\"url\\\":\\\"@triple{@and{@equal{@data{env},'android'},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.androidUrl}},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.androidUrl},@triple{@and{@equal{@data{env},'ios'},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.iosUrl}},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.iosUrl},@triple{@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.pcUrl},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.pcUrl},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.url}}}}\\\"}},\\\"main_success_right_callback\\\":{\\\"type\\\":\\\"dtToast\\\",\\\"params\\\":{\\\"type\\\":2,\\\"text\\\":\\\"@triple{@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.errorMsg},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.errorMsg},@dti18NAdapter{'操作失败,请稍候重试','操作失敗,請稍候重試','Operation failed, please try again later','Operation failed, please try again later','Operation failed, please try again later','Operation failed, please try again later','Operation failed, please try again later'}}\\\"}}}}\",\"fileType\":\"json\"}]}}","widgetInfo":"\n \n \n\n \n \n\n \n \n \n \n \n \n\n \n\n \n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n\n \n \n\n \n\n \n \n\n \n \n \n \n \n \n \n\n \n\n \n \n \n\n \n \n\n \n\n \n \n \n\n \n\n \n \n \n\n \n\n \n \n \n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n \n \n \n\n \n \n\n \n\n \n\n \n \n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n\n \n \n\n \n\n \n\n \n\n \n\n \n \n \n\n \n\n \n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n \n\n \n \n\n \n\n \n \n\n \n \n \n \n \n \n \n\n \n \n\n \n\n \n \n \n\n \n\n \n \n \n\n \n\n \n \n \n \n\n \n\n \n\n \n \n \n\n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n\n \n\n \n\n \n \n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n \n \n \n\n \n \n \n\n \n\n \n\n \n \n \n\n \n \n\n \n \n \n\n \n\n \n\n \n \n\n \n\n \n\n \n \n \n \n\n \n \n\n \n\n \n\n \n \n \n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n\n","type":"im","mode":"card"} \ No newline at end of file diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index ce017ba2..672d70e8 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -478,7 +478,10 @@

    3.1 上下游分工

    │ └─ src/types.ts 加 ApprovalEntry / Decision │ │ │ │ 资产 │ -│ (v3.3 删除 card-template-approval-v1.json 与 -source.md,复用 AI v2) │ +│ ├─ docs/assets/card-template-v3.json ★ v3.5.1 新增:用户实配的 │ +│ │ AI Card v3.0.0 schema(含 │ +│ │ approve_btns + show_approve_btns +│ │ 变量定义,267KB 完整低代码) │ │ └─ docs/user/features/exec-approval.md 用户配置指南 │ │ │ └────────────────────────────────────────────────────────────────────────────┘ @@ -1343,7 +1346,7 @@

    为什么 D2 不能"纯复用上游 /approve dispatcher"

    7. 审批卡片设计

    7.1 v2 模板字段映射(v3.5 对齐用户实配 schema)

    -

    v3.5 用户已在 AI Card v2 模板上配好两个新增变量(schema id ending in 876de.schema)+ 一个 ButtonGroup:

    +

    v3.5 用户已在 AI Card v2 模板上配好两个新增变量(schema id 末尾 876de.schema)+ 一个 ButtonGroup。完整低代码 schema(v3.0.0)见 docs/assets/card-template-v3.json(267KB,从开发者平台导出)——可重新导入开放平台卡片搭建器对比 / 定制 / 版本迁移用。

    @@ -1749,7 +1752,13 @@

    阶段 0 · 前置依赖(必须先满足)

  • 验证 tsconfig.json path 解析顺序——开发期 monorepo / linked checkout 场景可临时调高 ../openclaw/src/plugin-sdk 在 paths 中的优先级做 unblock,但 release 前必须切回 lockfile 路径以保证发布包正确
  • 本仓库 PR #480 已合并(MERGED;2026-05-18 核实):AI Card v2 模板的 CardBtn[]sendCardRequest 回调格式已在当前 main 可用。实施时需复用 / 确认当前 main 上的 CardBtn 类型与 sendCardRequest 契约(路径与字段命名)与本 spec §1.2 / §7.2 一致;如有偏差以当前 main 为准并回头修订 spec
  • -
  • ~~approval-card 模板上传~~(v3.3 删除):D9 v3.3 不再新建 approval 模板。替代任务:阶段 1 实施前确认当前 main 上 AI Card v2 模板的字段是否满足 §7.1 候选 A/B/C 之一(btns 必须;approval 终态指示位用 contentKey append / blockList system block / 新增 approvalStatus 三选一)。若需轻微改动 v2 模板,按 PR #480 既有流程处理
  • +
  • AI Card v3 模板部署(v3.5.1 修订):用户已配好 schema 并导出到 docs/assets/card-template-v3.json(267KB,schemaVersion 3.0.0,含 approve_btns 按钮组定义 + show_approve_btns Boolean 变量 + 三按钮 actionId/params 全部固化)。 +
    阶段 0 任务: +
      +
    1. open-dev.dingtalk.com 把这份 schema 发布为预置卡片模板,拿到正式 templateId
    2. +
    3. 把 templateId 写到 src/card/card-template.ts ——决定是替代现有 BUILTIN_DINGTALK_CARD_TEMPLATE_ID(v3 全面替换 v2)还是并存(v2 用于无 approval 场景、v3 用于 approval 场景),由 PR-2 实施时根据真机回归结果决定
    4. +
    5. 真机回归确认 show_approve_btns toggle 行为符合 §6.2 / §6.4 描述(true→显示 3 按钮,false→隐藏)
    6. +
  • From 3cb2a5d83f536ca9347127dad7a0307d28521ec1 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 00:14:15 +0800 Subject: [PATCH 11/44] =?UTF-8?q?docs(spec):=20v3.6=20=E4=BF=AE=E8=AE=A2?= =?UTF-8?q?=20=E2=80=94=20approvalId=20=E5=86=85=E7=BD=AE=E5=88=B0?= =?UTF-8?q?=E6=8C=89=E9=92=AE=20+=204=20=E5=A4=84=E5=AE=9E=E6=96=BD=20bloc?= =?UTF-8?q?ker?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第六轮 review 反馈,1 个结构性修订 + 4 处实施 blocker: (2) D24 重构(结构性):v3.5 用 card-run-registry 内存反查 approvalId 的 方案受重启 / 多 worker / TTL 影响——registry 是进程内 Map, src/card/card-run-registry.ts:1-9 明确多进程约束。v3.6 改为: - v3 模板再加 approval_id 字符串变量 - 三按钮 params 加 id 参数绑定到 approval_id 变量 - channel 端 PUT 时一起设 approval_id="" - callback 自带 params.id 为主链路 - registry pendingApprovalId 字段降级为 fallback(应对老卡片/异常) (3) §10 阶段 0 措辞收紧:v3 模板"必须替换"默认 templateId(不再写"替代 还是并存")。v3 是 v2 字段超集向后兼容,并存边界不清。 (4) §3.3 加 src/card-service.ts 改动面: - createAICard 必须显式 show_approve_btns:"false" + approval_id:"" - finalize / stop / 错误路径同样确保 false - 否则 v3 模板默认值会让 agent reply 一上线就显示未绑 approval 的按钮 (5) §10 阶段 2 把 src/card-callback-service.ts 扩展明确标为 BLOCKER: - 当前接口只暴露 actionId,approval handler 拿不到 params.action / params.id - 必须扩展 CardCallbackAnalysis 加 cardPrivateData 字段才能工作 (6) v3.3 残留清理: - 顶部核心原则段 "复用 CardBtn[] 能力" → 改为 v3 模板字段超集 - D9 "复用 PR #480 CardBtn" → 改为 v3 模板字段超集 - D22 "btns: JSON.stringify(approvalButtons)" → 改为 show_approve_btns + approval_id 三变量 toggle - 错误矩阵 "actionIds[0] 不是 approval 前缀" → 改为不在 {allow-once, allow-always, deny} - §12 "实施时复用 CardBtn 类型" → 改为按 v3 模板新字段实施 数据流一致性修订(同步 D24 v3.6 主链路): - §1.2 解码段:先读 params.id(主),缺失再 fallback 到 registry - §3.2 patcher 描述:applyPendingPatch 也写 approval_id 变量 - §6.2 场景 A:PUT 命令加 approval_id 字段 - §6.3 ASCII 流:payload params 含 id 字段 - §6.6 重复点击描述:主链路读 params.id,缺失 fallback 查 registry Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 97 ++++++++++++------- 1 file changed, 63 insertions(+), 34 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index 672d70e8..fbfd3699 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -42,7 +42,7 @@

    1. 总览

  • 与 peer 对齐:approver schema / ID 不短化 / slash 命令文本格式都跟 Discord/Telegram/Slack 一致;origin-only 边界与 PR #489 v1 一致
  • 钉钉特性最大化(v3.3 修订):审批按 card-run-registry 实际状态分两路由——card 路径在 agent reply card 上挂按钮(不创建新卡,不突兀);markdown 路径发独立文字消息含 /approve 模板。不读 messageType 配置,因为它不反映 runtime 实际降级状态
  • 上游约定为权威:按钮点击解码后通过 SDK 公开 API resolveApprovalOverGateway 回写;用户手敲 /approve 因 session lock 死锁需早期 intercept,但同样收敛到 resolveApprovalOverGateway
  • -
  • 零模板部署摩擦(v3.3 新约束):不再需要为 approval 单独发布卡片模板——card 路径复用 PR #480 已合并的 AI Card v2 模板的 CardBtn[] 能力,markdown 路径无需模板。只需把 approver staffId 列表写进 channels.dingtalk.execApprovals.approvers 即可启用
  • +
  • v3 卡片模板部署 + approver 配置(v3.6 修订):card 路径用 v3 模板(既有 AI Card v2 字段超集,新加 approve_btns + show_approve_btns + approval_id 三个变量;按钮内置无需 channel 构造);markdown 路径无需模板。把 v3 模板发布并替换默认 templateId(§10 阶段 0),然后把 approver staffId 列表写进 channels.dingtalk.execApprovals.approvers 即可启用
  • @@ -119,13 +119,20 @@

    解码(callback 入口)

    if (!["allow-once", "allow-always", "deny"].includes(action)) return null; const decision = action as "allow-once" | "allow-always" | "deny"; -// 第三步(D24):approvalId 反查 -const cardRun = resolveCardRun(analysis.outTrackId); -if (!cardRun?.pendingApprovalId) { - // 卡片不存在/未挂 approval/已被清理 → 视为"已处理或已过期" +// 第三步(D24 v3.6):approvalId 主链路从 cardPrivateData.params.id 直接取; +// registry 反查仅作为 fallback(应对老卡片 / 平台异常) +let approvalId: string | null = + (typeof cpd.params?.id === "string" && cpd.params.id) || null; +if (!approvalId) { + // 兜底:v3 模板未带 approval_id,或 callback 字段丢失 → 查 registry + const cardRun = resolveCardRun(analysis.outTrackId); + approvalId = cardRun?.pendingApprovalId ?? null; +} +if (!approvalId) { + // 主链路 + fallback 都没拿到 → 视为"已处理或已过期" return { approvalId: null, decision, reason: "no-pending-approval" }; } -return { approvalId: cardRun.pendingApprovalId, decision }; +return { approvalId, decision };

    回写上游

    // 用 SDK 公开 API(v2026.4.7+),通过 approval-resolver 单点收敛(D20):
    @@ -205,7 +212,7 @@ 

    2. 已确认的决策清单

    - @@ -314,7 +321,7 @@

    2. 已确认的决策清单

    @@ -334,15 +341,20 @@

    2. 已确认的决策清单

    - - + - +
    一卡仍只挂 1 个 approval(同 v3.5:agent 串行 approval 是天然约束) +
    cardParamMap 变量类型approval 用途pending / resolved / expired 时的值
    D9 卡片模板(v3.3 大改)不再新建 approval 专用模板——card 路径直接复用 PR #480 已合并的 AI Card v2 模板的 CardBtn[] 能力。 + 不再新建 approval 专用模板——card 路径用 v3 模板(v2 字段超集 + 新加 approve_btns / show_approve_btns / approval_id 三变量;按钮在模板内置)。
    v3.3 移除:BUILTIN_APPROVAL_CARD_TEMPLATE_ID 常量、DINGTALK_APPROVAL_CARD_TEMPLATE_ID env、docs/assets/card-template-approval-v1.json 源、阶段 0 模板上传任务。
    实施时确认 AI Card v2 模板的 cardParamMap.btns + hasAction 字段在当前 main 行为符合 §7.2 描述(默认应该 OK,PR #480 设计就这样)
    v3.3 用户拍板:approval 挂到原 card card 路径下,approval 按钮挂在 原 agent reply card 而非新建独立卡片:
    (1) transport.prepareTarget 内部调 approval-card-locator.findActiveAgentCard(request),按 request.sessionKeycard-run-registry
    (2) 命中且 entry.state ∈ {PROCESSING, INPUTING} → preparedTarget.route="card" 携带 entry.outTrackId;否则 route="markdown"; -
    (3) transport.deliverPending 按 route 分支:card 走 PUT updateCardVariables(outTrackId, { btns: JSON.stringify(approvalButtons), hasAction: "true" });markdown 走 sendProactiveTextOrMarkdown; +
    (3) transport.deliverPending 按 route 分支:card 走 PUT updateCardVariables(outTrackId, { show_approve_btns:"true", hasAction:"false", approval_id:"<id>" })(v3.6 修订:按钮在模板内置;D24 v3.6 把 approvalId 通过 approval_id 变量带到 callback);markdown 走 sendProactiveTextOrMarkdown
    (4) entry 记下 mode + outTrackId(如有),core 把 entry 带回 updateEntry 时按 mode 分支处理终态
    关键不变量:approval 只对 cardParamMap 做字段级 patch,不触碰 agent card 的 lifecycle 状态机(PROCESSING/INPUTING/FINISHED 仍由 agent reply 流控制)
    v3.3 用户拍板
    D24approvalId 反查机制(v3.5 新增)按钮 payload 不携带 approval id(D15),但 callback 一定带 outTrackId(agent reply card 的 id)。channel 端通过扩展 CardRunRecord 加一个新字段 pendingApprovalId?: string 来建立反查映射: -
    approval-card-patcher.applyPendingPatch(outTrackId, approvalId) 内部调 markCardRunPendingApproval(outTrackId, approvalId) 把 approvalId 写到 record; -
    • callback handler 拿到 outTrackId 后 resolveCardRun(outTrackId).pendingApprovalId 即可拿到 approvalId; -
    applyResolvedPatch / applyExpiredPatch 内部调 clearCardRunPendingApproval(outTrackId) 清字段。 +
    approvalId 传递机制(v3.6 重构:模板内置为主,registry 兜底)v3.6 主链路:approval_id 通过 cardParamMap 变量带到按钮 callback—— +
    1) v3 模板新增字符串变量 approval_id(PR-2 前置;用户回去给 schema 加,与 show_approve_btns 同级); +
    2) 三个按钮各加一个回传参数:name="id", type=绑定变量, 绑定到 approval_id(不是静态值); +
    3) channel 端 applyPendingPatch 时 PUT { show_approve_btns:"true", hasAction:"false", approval_id:"<id>" }; +
    4) callback payload 自带 params:{ action:"allow-once", id:"<id>" },approvalId 不再依赖反查; +
    5) applyResolvedPatch/applyExpiredPatch 时 PUT { show_approve_btns:"false", approval_id:"" } 清空。 +
    +
    fallback:card-run-registry.pendingApprovalId 字段——保留为兜底,应对老版本卡片 / 平台行为异常等:当 callback 没带 params.id 或带的是空时,反查 resolveCardRun(outTrackId).pendingApprovalId。该字段仍由 patcher 在 pending/resolved 时 set/clear。 +
    +
    原因(v3.5 D24 内存反查的脆弱性):进程重启会丢、多 worker 部署 callback 可能路由到另一个 worker(src/card/card-run-registry.ts:1-9 多进程约束说明)、TTL sweep 也会清。v3.6 把主链路从"内存映射"改成"卡片自带",registry 仅作降级保护。
    -
    不引入独立 approval-store——只是给现有 card-run-registry 加 1 个 optional 字段 + 2 个 API,与 D18 边界一致。 -
    一卡只能挂 1 个 approval(pendingApprovalId 是单字段);agent 串行 approval 是天然约束,多并发 approval 不在 v1 范围
    v3.5 用户拍板(A 方案:不再改模板)v3.6 用户拍板(模板已就位,再加一个变量成本低;换来主链路抗重启 / 抗多 worker)
    @@ -362,6 +374,12 @@

    2. 已确认的决策清单

    (6) §10 PR-1 措辞改"resolve 通道生效,approval id 可见性依赖外部界面,完整 UX 在 PR-2"; (7) §6.7 删除 store.register 残留; (8) §6.3 / §6.6 / §8 / §9 等所有 already-resolved 文案改"ℹ️ 已处理或已过期"并明确 catch 后 return 避免覆盖;§6.8 alias 范围显式列清;§11.2 风险表新增 kind 派发边界条目

  • +
  • v3.6(2026-05-19 第六轮 review,含 1 个结构性修订 + 4 处实施 blocker): + (2) D24 重构:v3.5 用 card-run-registry 内存反查 approvalId 的方案受重启 / 多 worker / TTL 影响(registry 是进程内 Map,src/card/card-run-registry.ts:1-9 明确多进程约束)。v3.6 改为 v3 模板再加 approval_id 变量 + 三按钮 params 加 id 绑定到该变量;callback 自带 approvalId 为主链路,registry pendingApprovalId 降级为兜底; + (3) §10 阶段 0 措辞收紧:v3 模板"必须替换默认 templateId"(不再写"替代还是并存"),v3 是 v2 字段超集向后兼容,并存会让边界不清; + (4) §3.3 加 src/card-service.ts 改动面:createAICard 必须显式写 show_approve_btns:"false" + approval_id:"",否则 v3 模板默认值会让 agent reply 一上线就显示未绑 approval 的按钮;finalize/stop 路径同样确保 false; + (5) §10 阶段 2 把 src/card-callback-service.ts 扩展明确标 BLOCKER——当前 src/card-callback-service.ts:6 接口只暴露 actionId,approval handler 拿不到 params.action / params.id,必须扩展才能跑; + (6) 清理 v3.3 残留:顶部核心原则段、D9、D22、错误矩阵、§12 references 等多处"PUT btns / JSON.stringify(CardBtn[]) / 复用 CardBtn / actionIds[0] 不是 approval 前缀"老语义,统一对齐 v3.5+v3.6
  • v3.5.1(2026-05-19 流程审核):用户重审 approval 完整流程时发现 v3.5 改动没扫干净的 6 处残留—— (1) §6.2 场景 A 的 deliverPending 描述还在 PUT btns + hasAction:"true"(v3.3 风格),改成 toggle show_approve_btns=true + hasAction=false + markCardRunPendingApproval; (2) §6.4 同步流的 applyResolvedPatch 描述还在"清按钮 + 写终态指示",改成 toggle show_approve_btns=false + cardStillActive ? hasAction=true + clearCardRunPendingApproval,明确说明 v1 不写终态文字; @@ -512,7 +530,7 @@

    3.2 模块单一职责表

    approval-card-patcher.ts
    ★ v3.5 大幅简化(不再构造按钮) v3.5:按钮在 v2 模板内置("按钮组来源:指定",3 按钮 actionId/params 都固化),patcher 只 toggle 变量 + 维护 D24 反查映射。 -
    applyPendingPatch(outTrackId, approvalId, token):PUT updateCardVariables({ show_approve_btns: "true", hasAction: "false" }) + markCardRunPendingApproval(outTrackId, approvalId); +
    applyPendingPatch(outTrackId, approvalId, token):PUT updateCardVariables({ show_approve_btns: "true", hasAction: "false", approval_id: "<id>" }) + markCardRunPendingApproval(outTrackId, approvalId)(D24 v3.6:主链路靠 approval_id 变量带到按钮回调,registry 写入仅作 fallback);
    applyResolvedPatch(outTrackId, decision, token, cardStillActive):PUT updateCardVariables({ show_approve_btns: "false", hasAction: cardStillActive ? "true" : "false" }) + clearCardRunPendingApproval(outTrackId)
    applyExpiredPatch(outTrackId, token, cardStillActive):同上但语义不同。
    @@ -609,6 +627,13 @@

    3.3 与现有代码的接触面

    v3.3 不修改——D9 v3.3 已废弃新建 approval 模板,复用现有 AI Card v2 模板字段 — + + src/card-service.ts
    (v3.6 新增改动面) + createAICard 必须显式设置 show_approve_btns:"false" + approval_id:""(D9 v3 模板会让 mock 默认 show_approve_btns=true,运行时若不显式覆盖可能出现"agent reply card 一上线就显示 3 个未绑 approval 的按钮")。 +
    finalize / stop / 错误兜底路径也确保 show_approve_btns:"false" + approval_id:""——避免遗留 approval 按钮卡在终态卡上。 +
    当前 src/card-service.ts:802 只写了 hasAction:"true",PR-2 必须补这两个新字段 + 低(在现有 createAndDeliver/finalize 调用处 cardParamMap 增加两个 KV) + src/card/card-run-registry.ts
    (v3.4 + v3.5 新增改动面) v3.4 改动:新增 resolveActiveCardRunBySession(accountId, sessionKey): CardRunRecord | null——遍历 records Map 按 accountId + sessionKey 精确匹配且 state ∈ {PROCESSING, INPUTING} 过滤。 @@ -899,15 +924,17 @@

    场景 A:群里 @ agent,agent AI Card 流式中触发 approval(card └─ approval-card-patcher.applyPendingPatch("ai_card_xxx", "abc123", token) │ ├─ PUT updateCardVariables("ai_card_xxx", { - │ show_approve_btns: "true", // 显示 approval 3 按钮(D15 模板内置) - │ hasAction: "false", // 隐藏 btn_stop(D23) + │ show_approve_btns: "true", // 显示 approval 3 按钮(D15) + │ hasAction: "false", // 隐藏 btn_stop(D23) + │ approval_id: "abc123", // D24 v3.6 主链路:按钮 params.id + │ // 绑定到此变量,callback 自带 │ }, token) - │ (按钮 actionId/params/颜色都在 v2 模板的 approve_btns 按钮组里固化, - │ channel 不传按钮定义;只 toggle 两个 Boolean 变量) + │ (按钮 actionId/颜色都在 v3 模板的 approve_btns 按钮组里固化, + │ channel 不传按钮定义;只 toggle 三个变量) │ └─ markCardRunPendingApproval("ai_card_xxx", "abc123") - (D24 反查映射:把 approvalId 写到 card-run-registry record, - 供后续 callback 用 outTrackId 反查) + (D24 v3.6 fallback:approvalId 也写到 registry,应对老卡片 / + 平台异常等 callback 没带 params.id 的情况) → entry = { approvalId:"abc123", accountId:"default", mode:"card", outTrackId:"ai_card_xxx" } @@ -979,7 +1006,7 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    payload.content/value (内嵌 JSON, v3.5 实配 schema): { cardPrivateData: { actionIds: ["allow-once"], ← 唯一命名无 index 后缀(D15 v3.5) - params: { action: "allow-once" } ← 仅 decision,不带 approvalId + params: { action: "allow-once", id: "abc123" } ← D24 v3.6 主链路 } } payload.userId = "staffA" ← clicker staffId payload.spaceType = "im" 或 "group" @@ -994,7 +1021,7 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ analysis.outTrackId = "ai_card_xxx" │ analysis.cardPrivateData = { ← D16 新增字段 │ actionIds: ["allow-once"], - │ params: { action: "allow-once" } + │ params: { action: "allow-once", id: "abc123" } ← D24 v3.6 │ } │ ├─ 【新增分支】tryHandleApprovalCallback(analysis, ...) @@ -1141,7 +1168,7 @@

    6.6 失败 / 边界场景

    用户重复点击(按钮看起来还在但已 resolved)

      -
    1. callback-handler 解析按钮 → 反查 cardRun.pendingApprovalId
    2. +
    3. callback-handler 解析按钮 → 主链路读 params.id(D24 v3.6);缺失 fallback 到 resolveCardRun(outTrackId).pendingApprovalId
    4. 反查可能命中(resolved 后还没来得及 clear)或未命中(已 clear):
      • 命中:调 resolver → 上游返回 already-resolved → catch 分支 PUT { show_approve_btns: "false" } 把按钮再次隐藏
      • @@ -1637,7 +1664,7 @@

        8. 错误处理矩阵

        parseApprovalFromCardPrivateData 失败 - cardPrivateData 缺失 / actionIds[0] 不是 "approval" 前缀 / params 结构损坏 + cardPrivateData 缺失 / actionIds[0] 不在 {"allow-once","allow-always","deny"} / params.action 缺失或值非法(v3.5 实配 schema,已对齐 D15 修订) WARN 日志 + ack 平台 + 卡片不变(保留按钮);continue 走原 callback 既有路径(feedback / btn_stop 等) 非 approval 按钮回调不受影响;approval 按钮异常时可通过 /approve 命令兜底 @@ -1751,13 +1778,14 @@

        阶段 0 · 前置依赖(必须先满足)

      • pnpm-lock.yaml 同步更新——当前 node_modules/openclaw 仍是 2026.3.28,tsconfig.json 优先读 ./node_modules/openclaw/dist/plugin-sdk/*.d.ts,旧类型里没有 approvalCapability / nativeRuntime / resolveApprovalOverGateway;若不一并 bump, pnpm run type-check 会失败。pnpm install 后必须确认 node_modules/openclaw/package.json version >= 2026.4.7
      • 验证 tsconfig.json path 解析顺序——开发期 monorepo / linked checkout 场景可临时调高 ../openclaw/src/plugin-sdk 在 paths 中的优先级做 unblock,但 release 前必须切回 lockfile 路径以保证发布包正确
  • -
  • 本仓库 PR #480 已合并(MERGED;2026-05-18 核实):AI Card v2 模板的 CardBtn[]sendCardRequest 回调格式已在当前 main 可用。实施时需复用 / 确认当前 main 上的 CardBtn 类型与 sendCardRequest 契约(路径与字段命名)与本 spec §1.2 / §7.2 一致;如有偏差以当前 main 为准并回头修订 spec
  • -
  • AI Card v3 模板部署(v3.5.1 修订):用户已配好 schema 并导出到 docs/assets/card-template-v3.json(267KB,schemaVersion 3.0.0,含 approve_btns 按钮组定义 + show_approve_btns Boolean 变量 + 三按钮 actionId/params 全部固化)。 -
    阶段 0 任务: +
  • 本仓库 PR #480 已合并(MERGED;2026-05-18 核实):AI Card v2 模板的 sendCardRequest 回调格式已在当前 main 可用。v3.6 本设计在此基础上发布 v3 模板(v2 字段超集,按钮在模板内置,channel 不构造按钮);详见 §10 阶段 0
  • +
  • AI Card v3 模板部署(v3.6 定死必做):用户已配好 schema 并导出到 docs/assets/card-template-v3.json(267KB,schemaVersion 3.0.0,含 approve_btns 按钮组定义 + show_approve_btns Boolean 变量 + 三按钮 actionId/params 全部固化)。v3.6 还要再加一个变量 approval_id(D24 v3.6 重构主链路)+ 三按钮回传参数加 id 绑定到 approval_id。 +
    阶段 0 任务(PR-2 前置 BLOCKER):
      -
    1. open-dev.dingtalk.com 把这份 schema 发布为预置卡片模板,拿到正式 templateId
    2. -
    3. 把 templateId 写到 src/card/card-template.ts ——决定是替代现有 BUILTIN_DINGTALK_CARD_TEMPLATE_ID(v3 全面替换 v2)还是并存(v2 用于无 approval 场景、v3 用于 approval 场景),由 PR-2 实施时根据真机回归结果决定
    4. -
    5. 真机回归确认 show_approve_btns toggle 行为符合 §6.2 / §6.4 描述(true→显示 3 按钮,false→隐藏)
    6. +
    7. 用户在 open-dev.dingtalk.com 给 v3 schema 加 approval_id 字符串变量 + 三按钮 params 加 id 绑定,重新导出更新 docs/assets/card-template-v3.json
    8. +
    9. 把 v3 schema 发布为预置卡片模板,拿到正式 templateId
    10. +
    11. 必须替换 src/card/card-template.tsBUILTIN_DINGTALK_CARD_TEMPLATE_ID 默认值为 v3 templateId(不并存——v3 是 v2 的超集向后兼容,并存会让"哪个场景用哪个模板"的决策复杂化,且 createAICard 不知道哪种 cardParamMap 字段集生效)
    12. +
    13. 真机回归确认:(a) v3 模板下既有 AI Card 流式行为不变(v2 是 v3 子集,向后兼容);(b) show_approve_btns toggle 行为符合 §6.2 / §6.4;(c) callback payload 含 params.id
  • @@ -1780,7 +1808,8 @@

    阶段 2 · 完整 native runtime(PR-2)

    • 实现 v3.3 渲染层:approval-card-patcher.ts(D22 落地:在原 agent card 上 patch 按钮 / 终态指示)+ approval-markdown-render.ts(markdown 路径主路径)
    • 实现 approval-callback-handler.ts(用 parseApprovalFromCardPrivateData → 调阶段 1 已有的 approval-resolver.resolveApproval——零新增权限逻辑;resolved 后调 approval-card-patcher.applyResolvedPatch 更新原 agent card)、approval-native-runtime.ts(4 子 adapter;interactions 不实现;transport 内部按 preparedTarget.route 分支调 patcher 或 markdown-render)
    • -
    • 修改 src/card-callback-service.ts(D16):CardCallbackAnalysiscardPrivateData 字段,analyzeCardCallback 抽 params 并附到 analysis
    • +
    • 【BLOCKER】修改 src/card-callback-service.ts(D16):当前 src/card-callback-service.ts:6CardCallbackAnalysis 接口只暴露 actionId,approval handler 拿不到 params.actionparams.id。必须扩展接口加 cardPrivateData?: { actionIds?: string[]; params?: Record<string, unknown> } 字段,analyzeCardCallbackcontent / value 嵌套 JSON 抽 params 并附到 analysis。这是 callback handler 能工作的硬前置——没这步什么都跑不通
    • +
    • 修改 src/card-service.ts(D24 v3.6):createAICard 显式写 show_approve_btns:"false" + approval_id:"";finalize / stop / 错误路径也写 false——避免 v3 模板默认值导致 agent reply 一上线就显示未绑 approval 的按钮
    • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支(在 feedback / btn_stop 之前;callback 命中后通过 outTrackId 找到原 agent card 调 patcher)
    • 在阶段 1 的 capability 里挂上 nativeRuntime(4 子 adapter;v1 不实现 interactions)
    • 测试:approval-card-patcher(核心:按钮注入 + 终态 patch + btn_stop 恢复策略 D23)+ approval-markdown-render(文案 + alias) + callback-handler(cardPrivateData 解析 → resolver → patcher);resolver 的 unit test 在 PR-1 已完成,PR-2 仅加 callback 入口集成测试覆盖"按钮 → resolver → patcher"链路 + agent card 状态正确同步
    • From 906afabea314cd778f7ed53d8040d59fa45865c2 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 00:23:28 +0800 Subject: [PATCH 12/44] =?UTF-8?q?docs(spec):=20v3.7=20=E4=BF=AE=E8=AE=A2?= =?UTF-8?q?=20=E2=80=94=20approval=5Fid=20=E2=86=92=20approveId=20?= =?UTF-8?q?=E5=91=BD=E5=90=8D=E5=90=8C=E6=AD=A5=E7=94=A8=E6=88=B7=E5=AE=9E?= =?UTF-8?q?=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户回去给 v3 schema 实际加了 approveId 变量(截图证实)+ 在 deny 按钮配了 第 2 个回传参数(参数名=approveId, 参数类型=变量, 参数值=approveId)。 命名修正: - 变量名是 approveId(camelCase),不是 v3.6 spec 假设的 approval_id - 回传参数名也是 approveId(v3.6 spec 假设 id) - schema id 已从 876de.schema 变到 05061.schema 修订面: - 全局批量替换 approval_id → approveId,params.id → params.approveId - D24 按钮 params 描述细化为"参数名=approveId, 类型=变量, 值=approveId" - §10 阶段 0 描述同步用户实际进展: - 用户已配齐 approveId 变量 ✓ - 待重新 export 更新 docs/assets/card-template-v3.json - 待发布 v3 schema 拿正式 templateId - 待替换 src/card/card-template.ts 默认 templateId - §1.2 解码片段:cpd.params?.id → cpd.params?.approveId - §6.2 / §6.3 callback params 结构同步 隐式假设:用户截图仅展示 deny 按钮配齐 approveId 参数;spec 假设 allow-once / allow-always 也同样配齐(用户应在 PR-2 前置阶段确认 其它两按钮也配了相同 params2)。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 65 +++++++++++-------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index fbfd3699..430e26ad 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -42,7 +42,7 @@

      1. 总览

    • 与 peer 对齐:approver schema / ID 不短化 / slash 命令文本格式都跟 Discord/Telegram/Slack 一致;origin-only 边界与 PR #489 v1 一致
    • 钉钉特性最大化(v3.3 修订):审批按 card-run-registry 实际状态分两路由——card 路径在 agent reply card 上挂按钮(不创建新卡,不突兀);markdown 路径发独立文字消息含 /approve 模板。不读 messageType 配置,因为它不反映 runtime 实际降级状态
    • 上游约定为权威:按钮点击解码后通过 SDK 公开 API resolveApprovalOverGateway 回写;用户手敲 /approve 因 session lock 死锁需早期 intercept,但同样收敛到 resolveApprovalOverGateway
    • -
    • v3 卡片模板部署 + approver 配置(v3.6 修订):card 路径用 v3 模板(既有 AI Card v2 字段超集,新加 approve_btns + show_approve_btns + approval_id 三个变量;按钮内置无需 channel 构造);markdown 路径无需模板。把 v3 模板发布并替换默认 templateId(§10 阶段 0),然后把 approver staffId 列表写进 channels.dingtalk.execApprovals.approvers 即可启用
    • +
    • v3 卡片模板部署 + approver 配置(v3.6 修订):card 路径用 v3 模板(既有 AI Card v2 字段超集,新加 approve_btns + show_approve_btns + approveId 三个变量;按钮内置无需 channel 构造);markdown 路径无需模板。把 v3 模板发布并替换默认 templateId(§10 阶段 0),然后把 approver staffId 列表写进 channels.dingtalk.execApprovals.approvers 即可启用
    @@ -119,12 +119,12 @@

    解码(callback 入口)

    if (!["allow-once", "allow-always", "deny"].includes(action)) return null; const decision = action as "allow-once" | "allow-always" | "deny"; -// 第三步(D24 v3.6):approvalId 主链路从 cardPrivateData.params.id 直接取; +// 第三步(D24 v3.6):approvalId 主链路从 cardPrivateData.params.approveId 直接取; // registry 反查仅作为 fallback(应对老卡片 / 平台异常) let approvalId: string | null = - (typeof cpd.params?.id === "string" && cpd.params.id) || null; + (typeof cpd.params?.approveId === "string" && cpd.params.approveId) || null; if (!approvalId) { - // 兜底:v3 模板未带 approval_id,或 callback 字段丢失 → 查 registry + // 兜底:v3 模板未带 approveId,或 callback 字段丢失 → 查 registry const cardRun = resolveCardRun(analysis.outTrackId); approvalId = cardRun?.pendingApprovalId ?? null; } @@ -212,7 +212,7 @@

    2. 已确认的决策清单

    D9 卡片模板(v3.3 大改) - 不再新建 approval 专用模板——card 路径用 v3 模板(v2 字段超集 + 新加 approve_btns / show_approve_btns / approval_id 三变量;按钮在模板内置)。 + 不再新建 approval 专用模板——card 路径用 v3 模板(v2 字段超集 + 新加 approve_btns / show_approve_btns / approveId 三变量;按钮在模板内置)。
    v3.3 移除:BUILTIN_APPROVAL_CARD_TEMPLATE_ID 常量、DINGTALK_APPROVAL_CARD_TEMPLATE_ID env、docs/assets/card-template-approval-v1.json 源、阶段 0 模板上传任务。
    实施时确认 AI Card v2 模板的 cardParamMap.btns + hasAction 字段在当前 main 行为符合 §7.2 描述(默认应该 OK,PR #480 设计就这样) v3.3 用户拍板:approval 挂到原 card @@ -321,7 +321,7 @@

    2. 已确认的决策清单

    card 路径下,approval 按钮挂在 原 agent reply card 而非新建独立卡片:
    (1) transport.prepareTarget 内部调 approval-card-locator.findActiveAgentCard(request),按 request.sessionKeycard-run-registry
    (2) 命中且 entry.state ∈ {PROCESSING, INPUTING} → preparedTarget.route="card" 携带 entry.outTrackId;否则 route="markdown"; -
    (3) transport.deliverPending 按 route 分支:card 走 PUT updateCardVariables(outTrackId, { show_approve_btns:"true", hasAction:"false", approval_id:"<id>" })(v3.6 修订:按钮在模板内置;D24 v3.6 把 approvalId 通过 approval_id 变量带到 callback);markdown 走 sendProactiveTextOrMarkdown; +
    (3) transport.deliverPending 按 route 分支:card 走 PUT updateCardVariables(outTrackId, { show_approve_btns:"true", hasAction:"false", approveId:"<id>" })(v3.6 修订:按钮在模板内置;D24 v3.6 把 approvalId 通过 approveId 变量带到 callback);markdown 走 sendProactiveTextOrMarkdown
    (4) entry 记下 mode + outTrackId(如有),core 把 entry 带回 updateEntry 时按 mode 分支处理终态
    关键不变量:approval 只对 cardParamMap 做字段级 patch,不触碰 agent card 的 lifecycle 状态机(PROCESSING/INPUTING/FINISHED 仍由 agent reply 流控制) v3.3 用户拍板 @@ -342,14 +342,14 @@

    2. 已确认的决策清单

    D24 approvalId 传递机制(v3.6 重构:模板内置为主,registry 兜底) - v3.6 主链路:approval_id 通过 cardParamMap 变量带到按钮 callback—— -
    1) v3 模板新增字符串变量 approval_id(PR-2 前置;用户回去给 schema 加,与 show_approve_btns 同级); -
    2) 三个按钮各加一个回传参数:name="id", type=绑定变量, 绑定到 approval_id(不是静态值); -
    3) channel 端 applyPendingPatch 时 PUT { show_approve_btns:"true", hasAction:"false", approval_id:"<id>" }; + v3.6 主链路:approveId 通过 cardParamMap 变量带到按钮 callback—— +
    1) v3 模板新增字符串变量 approveId(PR-2 前置;用户回去给 schema 加,与 show_approve_btns 同级); +
    2) 三个按钮各加一个回传参数:参数名="approveId", 参数类型=变量, 参数值=approveId(绑定到上一步新增的同名变量); +
    3) channel 端 applyPendingPatch 时 PUT { show_approve_btns:"true", hasAction:"false", approveId:"<id>" }
    4) callback payload 自带 params:{ action:"allow-once", id:"<id>" },approvalId 不再依赖反查; -
    5) applyResolvedPatch/applyExpiredPatch 时 PUT { show_approve_btns:"false", approval_id:"" } 清空。 +
    5) applyResolvedPatch/applyExpiredPatch 时 PUT { show_approve_btns:"false", approveId:"" } 清空。
    -
    fallback:card-run-registry.pendingApprovalId 字段——保留为兜底,应对老版本卡片 / 平台行为异常等:当 callback 没带 params.id 或带的是空时,反查 resolveCardRun(outTrackId).pendingApprovalId。该字段仍由 patcher 在 pending/resolved 时 set/clear。 +
    fallback:card-run-registry.pendingApprovalId 字段——保留为兜底,应对老版本卡片 / 平台行为异常等:当 callback 没带 params.approveId 或带的是空时,反查 resolveCardRun(outTrackId).pendingApprovalId。该字段仍由 patcher 在 pending/resolved 时 set/clear。

    原因(v3.5 D24 内存反查的脆弱性):进程重启会丢、多 worker 部署 callback 可能路由到另一个 worker(src/card/card-run-registry.ts:1-9 多进程约束说明)、TTL sweep 也会清。v3.6 把主链路从"内存映射"改成"卡片自带",registry 仅作降级保护。
    @@ -374,11 +374,16 @@

    2. 已确认的决策清单

    (6) §10 PR-1 措辞改"resolve 通道生效,approval id 可见性依赖外部界面,完整 UX 在 PR-2"; (7) §6.7 删除 store.register 残留; (8) §6.3 / §6.6 / §8 / §9 等所有 already-resolved 文案改"ℹ️ 已处理或已过期"并明确 catch 后 return 避免覆盖;§6.8 alias 范围显式列清;§11.2 风险表新增 kind 派发边界条目 +
  • v3.7(2026-05-19 用户实配 D24 确认):用户回去给 v3 schema 实际加 approveId 变量 + 在 deny 按钮配了第 2 个回传参数(参数名=approveId, 参数类型=变量, 参数值=approveId)。命名修正: + (1) 变量名实际是 approveId(camelCase),不是 v3.6 spec 假设的 approval_id(snake_case); + (2) 回传参数名也是 approveId(v3.6 spec 假设 id); + (3) schema id 已从 v3.5 的 876de.schema 变到 05061.schema,用户需重新 export 更新 docs/assets/card-template-v3.json。 + spec 全局替换 approval_id → approveId、params.id → params.approveId;§10 阶段 0 描述同步用户实际进展(schema 已配 ✓,待 export + 发布);按钮 params 描述细化为"参数名=approveId, 类型=变量, 值=approveId";隐式假设 allow-once / allow-always 按钮也配同样的 params2(用户截图仅展示 deny 按钮,应在 PR-2 前置确认其它两个按钮也配齐)
  • v3.6(2026-05-19 第六轮 review,含 1 个结构性修订 + 4 处实施 blocker): - (2) D24 重构:v3.5 用 card-run-registry 内存反查 approvalId 的方案受重启 / 多 worker / TTL 影响(registry 是进程内 Map,src/card/card-run-registry.ts:1-9 明确多进程约束)。v3.6 改为 v3 模板再加 approval_id 变量 + 三按钮 params 加 id 绑定到该变量;callback 自带 approvalId 为主链路,registry pendingApprovalId 降级为兜底; + (2) D24 重构:v3.5 用 card-run-registry 内存反查 approvalId 的方案受重启 / 多 worker / TTL 影响(registry 是进程内 Map,src/card/card-run-registry.ts:1-9 明确多进程约束)。v3.6 改为 v3 模板再加 approveId 变量 + 三按钮 params 加 id 绑定到该变量;callback 自带 approvalId 为主链路,registry pendingApprovalId 降级为兜底(3) §10 阶段 0 措辞收紧:v3 模板"必须替换默认 templateId"(不再写"替代还是并存"),v3 是 v2 字段超集向后兼容,并存会让边界不清; - (4) §3.3 加 src/card-service.ts 改动面:createAICard 必须显式写 show_approve_btns:"false" + approval_id:"",否则 v3 模板默认值会让 agent reply 一上线就显示未绑 approval 的按钮;finalize/stop 路径同样确保 false; - (5) §10 阶段 2 把 src/card-callback-service.ts 扩展明确标 BLOCKER——当前 src/card-callback-service.ts:6 接口只暴露 actionId,approval handler 拿不到 params.action / params.id,必须扩展才能跑; + (4) §3.3 加 src/card-service.ts 改动面:createAICard 必须显式写 show_approve_btns:"false" + approveId:"",否则 v3 模板默认值会让 agent reply 一上线就显示未绑 approval 的按钮;finalize/stop 路径同样确保 false; + (5) §10 阶段 2 把 src/card-callback-service.ts 扩展明确标 BLOCKER——当前 src/card-callback-service.ts:6 接口只暴露 actionId,approval handler 拿不到 params.action / params.approveId,必须扩展才能跑; (6) 清理 v3.3 残留:顶部核心原则段、D9、D22、错误矩阵、§12 references 等多处"PUT btns / JSON.stringify(CardBtn[]) / 复用 CardBtn / actionIds[0] 不是 approval 前缀"老语义,统一对齐 v3.5+v3.6
  • v3.5.1(2026-05-19 流程审核):用户重审 approval 完整流程时发现 v3.5 改动没扫干净的 6 处残留—— (1) §6.2 场景 A 的 deliverPending 描述还在 PUT btns + hasAction:"true"(v3.3 风格),改成 toggle show_approve_btns=true + hasAction=false + markCardRunPendingApproval; @@ -530,7 +535,7 @@

    3.2 模块单一职责表

    approval-card-patcher.ts
    ★ v3.5 大幅简化(不再构造按钮) v3.5:按钮在 v2 模板内置("按钮组来源:指定",3 按钮 actionId/params 都固化),patcher 只 toggle 变量 + 维护 D24 反查映射。 -
    applyPendingPatch(outTrackId, approvalId, token):PUT updateCardVariables({ show_approve_btns: "true", hasAction: "false", approval_id: "<id>" }) + markCardRunPendingApproval(outTrackId, approvalId)(D24 v3.6:主链路靠 approval_id 变量带到按钮回调,registry 写入仅作 fallback); +
    applyPendingPatch(outTrackId, approvalId, token):PUT updateCardVariables({ show_approve_btns: "true", hasAction: "false", approveId: "<id>" }) + markCardRunPendingApproval(outTrackId, approvalId)(D24 v3.6:主链路靠 approveId 变量带到按钮回调,registry 写入仅作 fallback);
    applyResolvedPatch(outTrackId, decision, token, cardStillActive):PUT updateCardVariables({ show_approve_btns: "false", hasAction: cardStillActive ? "true" : "false" }) + clearCardRunPendingApproval(outTrackId)
    applyExpiredPatch(outTrackId, token, cardStillActive):同上但语义不同。
    @@ -629,8 +634,8 @@

    3.3 与现有代码的接触面

    src/card-service.ts
    (v3.6 新增改动面) - createAICard 必须显式设置 show_approve_btns:"false" + approval_id:""(D9 v3 模板会让 mock 默认 show_approve_btns=true,运行时若不显式覆盖可能出现"agent reply card 一上线就显示 3 个未绑 approval 的按钮")。 -
    finalize / stop / 错误兜底路径也确保 show_approve_btns:"false" + approval_id:""——避免遗留 approval 按钮卡在终态卡上。 + createAICard 必须显式设置 show_approve_btns:"false" + approveId:""(D9 v3 模板会让 mock 默认 show_approve_btns=true,运行时若不显式覆盖可能出现"agent reply card 一上线就显示 3 个未绑 approval 的按钮")。 +
    finalize / stop / 错误兜底路径也确保 show_approve_btns:"false" + approveId:""——避免遗留 approval 按钮卡在终态卡上。
    当前 src/card-service.ts:802 只写了 hasAction:"true",PR-2 必须补这两个新字段 低(在现有 createAndDeliver/finalize 调用处 cardParamMap 增加两个 KV) @@ -926,7 +931,7 @@

    场景 A:群里 @ agent,agent AI Card 流式中触发 approval(card ├─ PUT updateCardVariables("ai_card_xxx", { │ show_approve_btns: "true", // 显示 approval 3 按钮(D15) │ hasAction: "false", // 隐藏 btn_stop(D23) - │ approval_id: "abc123", // D24 v3.6 主链路:按钮 params.id + │ approveId: "abc123", // D24 v3.6 主链路:按钮 params.approveId │ // 绑定到此变量,callback 自带 │ }, token) │ (按钮 actionId/颜色都在 v3 模板的 approve_btns 按钮组里固化, @@ -934,7 +939,7 @@

    场景 A:群里 @ agent,agent AI Card 流式中触发 approval(card │ └─ markCardRunPendingApproval("ai_card_xxx", "abc123") (D24 v3.6 fallback:approvalId 也写到 registry,应对老卡片 / - 平台异常等 callback 没带 params.id 的情况) + 平台异常等 callback 没带 params.approveId 的情况) → entry = { approvalId:"abc123", accountId:"default", mode:"card", outTrackId:"ai_card_xxx" } @@ -1168,7 +1173,7 @@

    6.6 失败 / 边界场景

    用户重复点击(按钮看起来还在但已 resolved)

      -
    1. callback-handler 解析按钮 → 主链路读 params.id(D24 v3.6);缺失 fallback 到 resolveCardRun(outTrackId).pendingApprovalId
    2. +
    3. callback-handler 解析按钮 → 主链路读 params.approveId(D24 v3.6);缺失 fallback 到 resolveCardRun(outTrackId).pendingApprovalId
    4. 反查可能命中(resolved 后还没来得及 clear)或未命中(已 clear):
      • 命中:调 resolver → 上游返回 already-resolved → catch 分支 PUT { show_approve_btns: "false" } 把按钮再次隐藏
      • @@ -1779,13 +1784,19 @@

        阶段 0 · 前置依赖(必须先满足)

      • 验证 tsconfig.json path 解析顺序——开发期 monorepo / linked checkout 场景可临时调高 ../openclaw/src/plugin-sdk 在 paths 中的优先级做 unblock,但 release 前必须切回 lockfile 路径以保证发布包正确
  • 本仓库 PR #480 已合并(MERGED;2026-05-18 核实):AI Card v2 模板的 sendCardRequest 回调格式已在当前 main 可用。v3.6 本设计在此基础上发布 v3 模板(v2 字段超集,按钮在模板内置,channel 不构造按钮);详见 §10 阶段 0
  • -
  • AI Card v3 模板部署(v3.6 定死必做):用户已配好 schema 并导出到 docs/assets/card-template-v3.json(267KB,schemaVersion 3.0.0,含 approve_btns 按钮组定义 + show_approve_btns Boolean 变量 + 三按钮 actionId/params 全部固化)。v3.6 还要再加一个变量 approval_id(D24 v3.6 重构主链路)+ 三按钮回传参数加 id 绑定到 approval_id。 +
  • AI Card v3 模板部署(v3.7 用户实配确认):用户已在 v3 schema 配齐三个新变量(v3.7 实测确认): +
      +
    • approve_btns(按钮组):内置 3 按钮,actionId 分别为 allow-once / allow-always / deny,每个按钮回传参数 action(静态值 = decision)+ approveId(变量,绑定到下面的 approveId 变量)
    • +
    • show_approve_btns(Boolean):按钮组可见性条件
    • +
    • approveId(普通文本):v3.6 新增的 D24 主链路载体(v3.7 用户实际命名 camelCase 不是 snake_case)
    • +
    + schema id 已变更(之前 876de.schema → 当前 05061.schema,因加了 approveId 变量重新发布)。
    阶段 0 任务(PR-2 前置 BLOCKER):
      -
    1. 用户在 open-dev.dingtalk.com 给 v3 schema 加 approval_id 字符串变量 + 三按钮 params 加 id 绑定,重新导出更新 docs/assets/card-template-v3.json
    2. +
    3. 用户重新 export 完整 schema 到 docs/assets/card-template-v3.json(含 approveId 变量 + 三按钮 params 绑定的最终状态)
    4. 把 v3 schema 发布为预置卡片模板,拿到正式 templateId
    5. -
    6. 必须替换 src/card/card-template.tsBUILTIN_DINGTALK_CARD_TEMPLATE_ID 默认值为 v3 templateId(不并存——v3 是 v2 的超集向后兼容,并存会让"哪个场景用哪个模板"的决策复杂化,且 createAICard 不知道哪种 cardParamMap 字段集生效)
    7. -
    8. 真机回归确认:(a) v3 模板下既有 AI Card 流式行为不变(v2 是 v3 子集,向后兼容);(b) show_approve_btns toggle 行为符合 §6.2 / §6.4;(c) callback payload 含 params.id
    9. +
    10. 必须替换 src/card/card-template.tsBUILTIN_DINGTALK_CARD_TEMPLATE_ID 默认值为 v3 templateId(不并存——v3 是 v2 的超集向后兼容,并存会让"哪个场景用哪个模板"的决策复杂化)
    11. +
    12. 真机回归确认:(a) v3 模板下既有 AI Card 流式行为不变;(b) show_approve_btns toggle 行为符合 §6.2 / §6.4;(c) callback payload 含 cardPrivateData.params.approveId 字符串(与 patch 时设的值一致)
  • @@ -1808,8 +1819,8 @@

    阶段 2 · 完整 native runtime(PR-2)

    • 实现 v3.3 渲染层:approval-card-patcher.ts(D22 落地:在原 agent card 上 patch 按钮 / 终态指示)+ approval-markdown-render.ts(markdown 路径主路径)
    • 实现 approval-callback-handler.ts(用 parseApprovalFromCardPrivateData → 调阶段 1 已有的 approval-resolver.resolveApproval——零新增权限逻辑;resolved 后调 approval-card-patcher.applyResolvedPatch 更新原 agent card)、approval-native-runtime.ts(4 子 adapter;interactions 不实现;transport 内部按 preparedTarget.route 分支调 patcher 或 markdown-render)
    • -
    • 【BLOCKER】修改 src/card-callback-service.ts(D16):当前 src/card-callback-service.ts:6CardCallbackAnalysis 接口只暴露 actionId,approval handler 拿不到 params.actionparams.id。必须扩展接口加 cardPrivateData?: { actionIds?: string[]; params?: Record<string, unknown> } 字段,analyzeCardCallbackcontent / value 嵌套 JSON 抽 params 并附到 analysis。这是 callback handler 能工作的硬前置——没这步什么都跑不通
    • -
    • 修改 src/card-service.ts(D24 v3.6):createAICard 显式写 show_approve_btns:"false" + approval_id:"";finalize / stop / 错误路径也写 false——避免 v3 模板默认值导致 agent reply 一上线就显示未绑 approval 的按钮
    • +
    • 【BLOCKER】修改 src/card-callback-service.ts(D16):当前 src/card-callback-service.ts:6CardCallbackAnalysis 接口只暴露 actionId,approval handler 拿不到 params.actionparams.approveId。必须扩展接口加 cardPrivateData?: { actionIds?: string[]; params?: Record<string, unknown> } 字段,analyzeCardCallbackcontent / value 嵌套 JSON 抽 params 并附到 analysis。这是 callback handler 能工作的硬前置——没这步什么都跑不通
    • +
    • 修改 src/card-service.ts(D24 v3.6):createAICard 显式写 show_approve_btns:"false" + approveId:"";finalize / stop / 错误路径也写 false——避免 v3 模板默认值导致 agent reply 一上线就显示未绑 approval 的按钮
    • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支(在 feedback / btn_stop 之前;callback 命中后通过 outTrackId 找到原 agent card 调 patcher)
    • 在阶段 1 的 capability 里挂上 nativeRuntime(4 子 adapter;v1 不实现 interactions)
    • 测试:approval-card-patcher(核心:按钮注入 + 终态 patch + btn_stop 恢复策略 D23)+ approval-markdown-render(文案 + alias) + callback-handler(cardPrivateData 解析 → resolver → patcher);resolver 的 unit test 在 PR-1 已完成,PR-2 仅加 callback 入口集成测试覆盖"按钮 → resolver → patcher"链路 + agent card 状态正确同步
    • From eccfac1bef2ccfb84a2c5018666dc34d016151da Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 00:24:31 +0800 Subject: [PATCH 13/44] =?UTF-8?q?docs(spec):=20=E9=94=81=E5=AE=9A=20v3=20?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E6=AD=A3=E5=BC=8F=20templateId?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户已在开发者平台发布 v3 schema,拿到正式 templateId: 58f73932-fc3b-46ae-8e90-93313e405061.schema spec 修订: - D9 行:把这个 templateId 作为权威值列出,明确 src/card/card-template.ts:6 的 BUILTIN_DINGTALK_CARD_TEMPLATE_ID 默认值需替换为该 v3 ID - §10 阶段 0 任务:把 "拿到正式 templateId" 改为 "用户已完成 ✓,正式 templateId = 58f73932-...05061.schema",并显式给出新旧 ID 对照(旧 v2 675cde2f-... 替换为新 v3 58f73932-...) PR-2 实施时实现层只需做: - 替换 src/card/card-template.ts:6 默认 templateId 常量 - createAICard 调用时 cardParamMap 显式带 show_approve_btns:"false" + approveId:"" (D24 v3.6) - 扩展 src/card-callback-service.ts CardCallbackAnalysis 加 cardPrivateData 字段 (D16 BLOCKER) Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index 430e26ad..a6f62a04 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -211,11 +211,17 @@

      2. 已确认的决策清单

      D9 - 卡片模板(v3.3 大改) - 不再新建 approval 专用模板——card 路径用 v3 模板(v2 字段超集 + 新加 approve_btns / show_approve_btns / approveId 三变量;按钮在模板内置)。 -
      v3.3 移除:BUILTIN_APPROVAL_CARD_TEMPLATE_ID 常量、DINGTALK_APPROVAL_CARD_TEMPLATE_ID env、docs/assets/card-template-approval-v1.json 源、阶段 0 模板上传任务。 -
      实施时确认 AI Card v2 模板的 cardParamMap.btns + hasAction 字段在当前 main 行为符合 §7.2 描述(默认应该 OK,PR #480 设计就这样) - v3.3 用户拍板:approval 挂到原 card + 卡片模板(v3.7 锁定正式 templateId) + 不新建 approval 专用模板——card 路径用 v3 模板(v2 字段超集 + 新加 approve_btns / show_approve_btns / approveId 三变量;按钮在模板内置)。 +
      +
      正式 templateId(v3.7 用户确认,唯一权威值): +
          58f73932-fc3b-46ae-8e90-93313e405061.schema +
      +
      PR-2 实施时把 src/card/card-template.ts:6BUILTIN_DINGTALK_CARD_TEMPLATE_ID 默认值从旧 v2 675cde2f-... 替换为上面 v3 ID。 +
      JSON schema 源:docs/assets/card-template-v3.json(用户重新 export 后覆盖)。 +
      +
      v3.3 移除:BUILTIN_APPROVAL_CARD_TEMPLATE_ID 常量、DINGTALK_APPROVAL_CARD_TEMPLATE_ID env、docs/assets/card-template-approval-v1.json 源、阶段 0 模板上传任务(v3.7 上传任务已由用户完成) + v3.7 用户实测发布 D10 @@ -1794,8 +1800,11 @@

      阶段 0 · 前置依赖(必须先满足)


      阶段 0 任务(PR-2 前置 BLOCKER):
      1. 用户重新 export 完整 schema 到 docs/assets/card-template-v3.json(含 approveId 变量 + 三按钮 params 绑定的最终状态)
      2. -
      3. 把 v3 schema 发布为预置卡片模板,拿到正式 templateId
      4. -
      5. 必须替换 src/card/card-template.tsBUILTIN_DINGTALK_CARD_TEMPLATE_ID 默认值为 v3 templateId(不并存——v3 是 v2 的超集向后兼容,并存会让"哪个场景用哪个模板"的决策复杂化)
      6. +
      7. 把 v3 schema 发布为预置卡片模板(用户已完成 ✓,正式 templateId = 58f73932-fc3b-46ae-8e90-93313e405061.schema
      8. +
      9. 必须替换 src/card/card-template.ts:6BUILTIN_DINGTALK_CARD_TEMPLATE_ID 默认值: +
            旧:"675cde2f-f526-40cb-b828-f5b2b57b8b77.schema"(v2) +
            新:"58f73932-fc3b-46ae-8e90-93313e405061.schema"(v3,含 approve_btns / show_approve_btns / approveId) +
        不并存——v3 是 v2 的超集向后兼容,并存会让"哪个场景用哪个模板"的决策复杂化)
      10. 真机回归确认:(a) v3 模板下既有 AI Card 流式行为不变;(b) show_approve_btns toggle 行为符合 §6.2 / §6.4;(c) callback payload 含 cardPrivateData.params.approveId 字符串(与 patch 时设的值一致)
    From 722c0088dbb64fd366db4cb96dddb7f8691cba15 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 00:25:51 +0800 Subject: [PATCH 14/44] =?UTF-8?q?docs(assets):=20=E6=9B=B4=E6=96=B0=20v3?= =?UTF-8?q?=20=E6=A8=A1=E6=9D=BF=20JSON=20=E5=90=AB=20approveId=20?= =?UTF-8?q?=E5=8F=98=E9=87=8F=20+=20=E4=B8=89=E6=8C=89=E9=92=AE=20params?= =?UTF-8?q?=20=E7=BB=91=E5=AE=9A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户重新 export 的最新 v3 schema(268KB,approve-release_1779121432897.json) 覆盖 docs/assets/card-template-v3.json。 新版相对于之前提交的差异(用户实配进展): - 新增 approveId 字符串变量(D24 v3.6 主链路载体) - 三按钮(allow-once / allow-always / deny)回传参数 2 都加了 approveId 绑定(参数名=approveId, 类型=变量, 值=approveId) - 用户确认三按钮均配齐,spec 删除 PR-2 前置 "其它两按钮也配齐" 的 caveat PR-2 实施前置 BLOCKER 状态更新: - ✓ approveId 变量已加 - ✓ 三按钮 params 已绑定 - ✓ v3 模板已发布(templateId 58f73932-fc3b-46ae-8e90-93313e405061.schema) - ✓ JSON 资产已同步 - ⏳ 待 PR-2 实施:替换 src/card/card-template.ts:6 默认值 + 扩 card-callback-service.ts CardCallbackAnalysis + 改 card-service.ts createAICard 显式带 show_approve_btns/approveId Co-Authored-By: Claude Opus 4.7 (1M context) --- docs/assets/card-template-v3.json | 2 +- docs/features/2026-05-18-gap-01-approval-native-design.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/assets/card-template-v3.json b/docs/assets/card-template-v3.json index 39cba483..e88a171f 100644 --- a/docs/assets/card-template-v3.json +++ b/docs/assets/card-template-v3.json @@ -1 +1 @@ -{"editorData":"{\"schemaVersion\":\"3.0.0\",\"schema\":{\"componentsMap\":[{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AIPending\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AIPending\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardStatusContainer\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardStatusContainer\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"BaseText\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"BaseText\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Divider\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Divider\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"ButtonGroup\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"ButtonGroup\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"MarkdownBlock\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"MarkdownBlock\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Image\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Image\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Grid\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Grid\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Loop\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Loop\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Input\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Input\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Column\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Column\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"ColumnLayout\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"ColumnLayout\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"FixedSingleButton\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"FixedSingleButton\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardContent\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardContent\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Icon\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Icon\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AIGenerationProcessing\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AIGenerationProcessing\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Feedback\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Feedback\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardContainer\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardContainer\"}],\"componentsTree\":[{\"componentName\":\"AICardContainer\",\"id\":\"node_ocmncu094g1\",\"props\":{\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePending\":true,\"enableWriting\":true,\"enableFailed\":false,\"enableDoing\":false,\"enableTitle\":false,\"operationPenalType\":\"prompt\",\"summaryContent\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"enableGradientBorder\":true,\"flowStatusVar\":{\"variable\":\"\",\"variableType\":\"global\",\"type\":\"variableValue\"},\"cardSizeMode\":\"adaptive\",\"cardSizeHeightMode\":\"adaptive\",\"cardSizeWidthMode\":\"adaptive\",\"cardSizeHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":226,\"variable\":\"\",\"variableType\":\"global\"},\"hasBackground\":false,\"backgroundType\":\"Standard\",\"standardBackgroundColor\":\"gray\",\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"enableFlowAbort\":true,\"enableEngineUpgrade\":false,\"enableExposeStatPoint\":false,\"enableDebugTool\":false},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_ocmncu094g2\",\"props\":{\"status\":1,\"enableExtend\":false,\"enableCollapse\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0},\"title\":\"处理中状态\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AIPending\",\"id\":\"node_ocmncu094g3\",\"props\":{\"pendingTip\":{\"i18n\":true,\"type\":\"dynamicString\",\"content\":{\"zh_Hans\":\"思考中...\",\"zh_Hant\":\"Progressing...\",\"en_US\":\"Progressing...\",\"ja_JP\":\"進行中...\",\"vi_VN\":\"Progressing...\",\"th_TH\":\"Progressing...\",\"id_ID\":\"Progressing...\"}},\"style\":\"embed\",\"hideIcon\":false},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_ocmncu094g4\",\"props\":{\"status\":2,\"enableExtend\":false,\"enableCollapse\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0},\"title\":\"输出中状态\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardContent\",\"id\":\"node_ocmncu094g5\",\"props\":{\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0,\"transformToEventChain\":false,\"disabledWhileForward\":false,\"enableStatPoint\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}]},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmncu094gf\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${quoteContent}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_DTXX_message_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":true,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Divider\",\"id\":\"node_ocmncu094gg\",\"props\":{\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"height\":30,\"direction\":\"horizontal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"color\":\"#1F111F2C\",\"margin\":-2,\"innerOffset\":0},\"title\":\"分割线\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Loop\",\"id\":\"node_ocmncu094gh\",\"props\":{\"listData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"blockList\"},\"direction\":\"vertical\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"isFixedWidth\":false,\"isAutoWidth\":false,\"width\":50,\"equalSpace\":false,\"flowLayout\":false,\"childGap\":false,\"childGapSize\":4,\"childDivider\":false,\"childDividerMarginLeft\":0,\"childDividerMarginRight\":0,\"childDividerMarginBottom\":0,\"childDividerMarginTop\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"scrollable\":false,\"childWidth\":\"match_content\",\"childDividerWidth\":0.5,\"childDividerColorDark\":\"rgba(255, 255, 255, 0.12)\",\"childDividerColorLight\":\"rgba(17, 31, 44, 0.12)\",\"paging\":false,\"hasMore\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onLoadMore\":{\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false}},\"isFixedHeight\":false,\"height\":100,\"flowDirection\":\"x\",\"margin\":0,\"innerOffset\":0},\"title\":\"循环渲染容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"ButtonGroup\",\"id\":\"node_ocmnee6af5g\",\"props\":{\"dynamicButtons\":{\"variable\":\"blockList[0].btns\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"blockList[0].btns\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"responsiveLayoutWidth\":350,\"buttonsSource\":\"variable\",\"fixedButtonIds\":[],\"fixedButtons\":[],\"enableResponsiveLayout\":false,\"matchContent\":false,\"buttonSpacing\":8,\"margin\":-2,\"innerOffset\":0},\"title\":\"审批按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"MarkdownBlock\",\"id\":\"node_ocmncu094g6\",\"props\":{\"isStreaming\":false,\"content\":{\"variable\":\"blockList[0].markdown\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"lt\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}}},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Image\",\"id\":\"node_ocmnd5z3kwq\",\"props\":{\"images\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"@lALPDfmVRLVVAm_NBdzNBdw\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"darkModeImages\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"single\":true,\"height\":{\"type\":\"dynamicWidth\",\"valueType\":\"fixed\",\"value\":200,\"variableType\":\"global\",\"variable\":\"\",\"full\":false,\"adaptive\":false},\"width\":{\"type\":\"dynamicWidth\",\"valueType\":\"full\",\"value\":100,\"variableType\":\"global\",\"variable\":\"\",\"full\":true,\"adaptive\":false},\"cornerRadius\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":6,\"variable\":\"\",\"variableType\":\"global\"},\"scaleType\":\"centerCrop\",\"enablePreview\":true,\"enableBorder\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"offsetLeft\":0,\"offsetRight\":0,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"adaptiveSize\":false,\"fixedRatio\":false,\"borderColor\":\"#297f8790\",\"enableStatPoint\":false,\"imageType\":\"adaptiveSize\",\"aspectRatio\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"margin\":-2,\"innerOffset\":0},\"title\":\"图片\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"BaseText\",\"id\":\"node_ocmnlxveuu8\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":0,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"图片标注\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Grid\",\"id\":\"node_ocmnd5z3kwx\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":true,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":5,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"childGravity\":\"center\",\"hasBackground\":true,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":-2,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Custom\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"extended_green0_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":16,\"cornerRadiusLeftTop\":16,\"cornerRadiusRightTop\":16,\"cornerRadiusRightBottom\":16,\"cornerRadiusLeftBottom\":16,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#cbf797\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#a4e191\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"enableClickEvent\":true,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"主题布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnd5z3kwy\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level2_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_tiny_bold_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]},{\"componentName\":\"Grid\",\"id\":\"node_ocmnlxveuu1\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"leftTop\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"MarkdownBlock\",\"id\":\"node_ocmnj1dw4n1\",\"props\":{\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"content\":{\"variable\":\"content\",\"variableType\":\"global\",\"type\":\"variableValue\",\"varType\":\"markdown\"},\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isMarkdownNotEmpty\",\"variable\":\"content\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"isStreaming\":true,\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"marginTop\":0,\"marginBottom\":0,\"marginLeft\":0,\"marginRight\":0},\"title\":\"流式答案\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"ColumnLayout\",\"id\":\"node_ocmnee6af51\",\"props\":{\"columnCount\":2,\"columnWidth\":[{\"widthMode\":\"weighted\",\"weight\":1,\"width\":50},{\"widthMode\":\"fixed\",\"weight\":1,\"width\":80}],\"columnSpacing\":8,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isTrue\",\"variable\":\"hasAction\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"childGravity\":\"center\",\"enableResponsiveLayout\":false,\"responsiveLayout\":1,\"margin\":-2,\"innerOffset\":0},\"title\":\"Action按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Column\",\"id\":\"node_ocmnee6af52\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"center\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Input\",\"id\":\"node_ocmnee6af5i\",\"props\":{\"placeholder\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"添加指引\"},\"currentValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"message\":{\"type\":\"dynamicString\",\"content\":\"请输入内容\",\"i18n\":false},\"title\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"添加指引\"},\"id\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"guideInput\"},\"params\":[{\"type\":\"builtIn\",\"variable\":\"\",\"value\":\"选中会话列表\",\"name\":\"guideInput\",\"variableType\":\"global\",\"id\":\"__built_in_inputResult__\"}],\"visible\":{\"type\":\"dynamicVisible\",\"value\":false,\"valueType\":\"fixed\",\"condition\":{\"op\":\"or\",\"conditions\":[]}},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"actionType\":\"request\",\"localVarAction\":{\"variable\":\"guideString\",\"variableType\":\"global\",\"varType\":\"string\",\"type\":\"variableValue\"},\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"inlineMode\":false,\"textArea\":false,\"minRows\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"maxRows\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":6,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"margin\":0,\"innerOffset\":0},\"title\":\"指引文本输入\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"Column\",\"id\":\"node_ocmnee6af53\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"rightCenter\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Grid\",\"id\":\"node_ocmnee6af55\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":true,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"rightCenter\",\"hasBackground\":true,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Custom\",\"backgroundType\":\"Custom\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"extended_red0_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"rgba(255,53,0,0.95)\",\"darkModeBackgroundColor\":\"#ca0505\",\"cornerRadius\":20,\"cornerRadiusLeftTop\":20,\"cornerRadiusRightTop\":20,\"cornerRadiusRightBottom\":20,\"cornerRadiusLeftBottom\":20,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#fcbb50\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f28326\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"actionType\":\"actionSheet\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":true,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"确认\"},\"confirmMessage\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"是否立即中止运行?\"},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"},\"actionSheetMessage\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"是否中止任务?\"},\"actionSheetItems\":[{\"id\":\"0\",\"actionSheetStyle\":\"destructive\",\"actionSheetName\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"立即中止\"},\"actionSheetDesc\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"},\"actionIconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_raise_hand\"},\"actionSheetAction\":\"request\",\"actionSheetRequestItemActionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"btn_stop\"},\"actionSheetRequestItemParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"true\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetRequestItemSuccessToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetUrlItemUrlType\":\"all\",\"actionSheetUrlItemAndroidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemPcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIsDtmd\":false,\"actionSheetUrlItemUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetOpenModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetOpenModalHeight\":500,\"actionSheetOpenModalWidth\":400,\"actionSheetApiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetApiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetApiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"}},{\"id\":\"1\",\"actionSheetStyle\":\"default\",\"actionSheetName\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"取消\"},\"actionSheetDesc\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"},\"actionIconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"actionSheetAction\":\"none\",\"actionSheetRequestItemActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetRequestItemParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetRequestItemSuccessToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetUrlItemUrlType\":\"all\",\"actionSheetUrlItemAndroidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemPcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIsDtmd\":false,\"actionSheetUrlItemUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetOpenModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetOpenModalHeight\":500,\"actionSheetOpenModalWidth\":400,\"actionSheetApiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetApiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetApiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"}}],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[{\"event\":{\"actionType\":\"url\",\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false}}],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"enableClickEvent\":true,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnee6af56\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"中止\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_raise_hand\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":6,\"marginRight\":6,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Custom\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level2_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_action_bold_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":false,\"enableIcon\":true,\"margin\":6,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]},{\"componentName\":\"BaseText\",\"id\":\"node_ocmncu094gn\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${statusLine}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_ai_diagonal_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":true,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"ButtonGroup\",\"id\":\"node_ocmpbcor1i2\",\"props\":{\"dynamicButtons\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"btns\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"and\",\"conditions\":[{\"value\":\"\",\"op\":\"isTrue\",\"variable\":\"show_approve_btns\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"responsiveLayoutWidth\":350,\"buttonsSource\":\"fixed\",\"fixedButtonIds\":[\"1\",\"2\",\"3\"],\"fixedButtons\":[{\"id\":\"1\",\"type\":\"button\"},{\"id\":\"2\",\"type\":\"button\"},{\"id\":\"3\",\"type\":\"button\"}],\"enableResponsiveLayout\":false,\"matchContent\":false,\"buttonSpacing\":8,\"margin\":-2,\"innerOffset\":0},\"title\":\"按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"FixedSingleButton\",\"id\":\"node_ocmpbcor1i3\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"允许一次\"},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"blue\",\"variable\":\"\",\"variableType\":\"global\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"$iwElAqNwbmcDBgTRBOYF0QTmBrDiyibODEbGnAngitJlqmwABwAIAAm3aW50ZXJhY3RpdmUtY2FyZC1lZGl0b3IKAAsA\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkModeIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"enableIcon\":true,\"iconPosition\":\"left\",\"iconType\":\"image\",\"iconFont\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"\"},\"variable\":\"\",\"variableType\":\"global\"},\"enableCornerMark\":false,\"cornerMarkPosition\":\"right\",\"cornerMarkText\":{\"type\":\"dynamicString\",\"content\":\"按钮角标文案\",\"i18n\":false},\"cornerMarkTextColor\":\"white\",\"cornerBackgroundColorType\":\"standard\",\"standardCornerBackgroundColor\":\"RedGradient\",\"customCornerBackgroundColor\":\"#FF6A00\",\"customCornerDarkModeBackgroundColor\":\"#FF6A00\",\"actionType\":\"request\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"allow-once\"},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"allow-once\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableClickEvent\":true,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"widthMode\":\"match_parent\",\"width\":200,\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePadButtonText\":false,\"cornerMarkVisible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"按钮1\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"FixedSingleButton\",\"id\":\"node_ocmpbcor1i4\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"总是允许\"},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"blue\",\"variable\":\"\",\"variableType\":\"global\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"$iwELAqNwbmcDBgTRBOYF0QTmBrAVv0YTQHioCwngjGjME4IABwAIAAm3aW50ZXJhY3RpdmUtY2FyZC1lZGl0b3IKAAsA\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkModeIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"enableIcon\":true,\"iconPosition\":\"left\",\"iconType\":\"image\",\"iconFont\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"variable\":\"\",\"variableType\":\"global\"},\"enableCornerMark\":false,\"cornerMarkPosition\":\"right\",\"cornerMarkText\":{\"type\":\"dynamicString\",\"content\":\"按钮角标文案\",\"i18n\":false},\"cornerMarkTextColor\":\"white\",\"cornerBackgroundColorType\":\"standard\",\"standardCornerBackgroundColor\":\"RedGradient\",\"customCornerBackgroundColor\":\"#FF6A00\",\"customCornerDarkModeBackgroundColor\":\"#FF6A00\",\"actionType\":\"request\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"allow-always\"},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"allow-always\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableClickEvent\":true,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"widthMode\":\"match_parent\",\"width\":200,\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePadButtonText\":false,\"cornerMarkVisible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"按钮2\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"FixedSingleButton\",\"id\":\"node_ocmpbcor1i5\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"拒绝\"},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"blue\",\"variable\":\"\",\"variableType\":\"global\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"$iwElAqNwbmcDBgTRBOYF0QTmBrBhBQt9e-UVYgngjO_OkvEABwAIAAm3aW50ZXJhY3RpdmUtY2FyZC1lZGl0b3IKAAsA\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkModeIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"enableIcon\":true,\"iconPosition\":\"left\",\"iconType\":\"image\",\"iconFont\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"variable\":\"\",\"variableType\":\"global\"},\"enableCornerMark\":false,\"cornerMarkPosition\":\"right\",\"cornerMarkText\":{\"type\":\"dynamicString\",\"content\":\"按钮角标文案\",\"i18n\":false},\"cornerMarkTextColor\":\"white\",\"cornerBackgroundColorType\":\"standard\",\"standardCornerBackgroundColor\":\"RedGradient\",\"customCornerBackgroundColor\":\"#FF6A00\",\"customCornerDarkModeBackgroundColor\":\"#FF6A00\",\"actionType\":\"request\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"deny\"},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"deny\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableClickEvent\":true,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"widthMode\":\"match_parent\",\"width\":200,\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePadButtonText\":false,\"cornerMarkVisible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"按钮3\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]},{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_ocmncu094g7\",\"props\":{\"status\":3,\"enableExtend\":false,\"enableCollapse\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0},\"title\":\"完成状态\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardContent\",\"id\":\"node_ocmncu094g8\",\"props\":{\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0,\"transformToEventChain\":false,\"disabledWhileForward\":false,\"enableStatPoint\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}]},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnd5z3kwa\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${quoteContent}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_DTXX_message_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":true,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"Divider\",\"id\":\"node_ocmnd5z3kwb\",\"props\":{\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"height\":30,\"direction\":\"horizontal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"color\":\"#1F111F2C\",\"margin\":-2,\"innerOffset\":0},\"title\":\"分割线\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"Loop\",\"id\":\"node_ocmnd5z3kwc\",\"props\":{\"listData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"blockList\"},\"direction\":\"vertical\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"isFixedWidth\":false,\"isAutoWidth\":false,\"width\":50,\"equalSpace\":false,\"flowLayout\":false,\"childGap\":false,\"childGapSize\":4,\"childDivider\":false,\"childDividerMarginLeft\":0,\"childDividerMarginRight\":0,\"childDividerMarginBottom\":0,\"childDividerMarginTop\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"scrollable\":false,\"childWidth\":\"match_content\",\"childDividerWidth\":0.5,\"childDividerColorDark\":\"rgba(255, 255, 255, 0.12)\",\"childDividerColorLight\":\"rgba(17, 31, 44, 0.12)\",\"paging\":false,\"hasMore\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onLoadMore\":{\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false}},\"isFixedHeight\":false,\"height\":100,\"flowDirection\":\"x\",\"margin\":0,\"innerOffset\":0},\"title\":\"循环渲染容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true,\"children\":[{\"componentName\":\"MarkdownBlock\",\"id\":\"node_ocmnd5z3kwf\",\"props\":{\"isStreaming\":false,\"content\":{\"variable\":\"blockList[0].markdown\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"lt\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}}},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Image\",\"id\":\"node_ocmnd5z3kws\",\"props\":{\"images\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"@lALPDfmVRLVVAm_NBdzNBdw\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"darkModeImages\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"single\":true,\"height\":{\"type\":\"dynamicWidth\",\"valueType\":\"fixed\",\"value\":200,\"variableType\":\"global\",\"variable\":\"\",\"full\":false,\"adaptive\":false},\"width\":{\"type\":\"dynamicWidth\",\"valueType\":\"full\",\"value\":100,\"variableType\":\"global\",\"variable\":\"\",\"full\":true,\"adaptive\":false},\"cornerRadius\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":6,\"variable\":\"\",\"variableType\":\"global\"},\"scaleType\":\"centerCrop\",\"enablePreview\":true,\"enableBorder\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"offsetLeft\":0,\"offsetRight\":0,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"adaptiveSize\":false,\"fixedRatio\":false,\"borderColor\":\"#297f8790\",\"enableStatPoint\":false,\"imageType\":\"adaptiveSize\",\"aspectRatio\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"margin\":-2,\"innerOffset\":0},\"title\":\"图片\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"BaseText\",\"id\":\"node_ocmnlxveuu9\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":0,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"图片标注\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"Grid\",\"id\":\"node_ocmndcaakx3\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":true,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":5,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"childGravity\":\"center\",\"hasBackground\":true,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":-2,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Custom\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"extended_green0_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":16,\"cornerRadiusLeftTop\":16,\"cornerRadiusRightTop\":16,\"cornerRadiusRightBottom\":16,\"cornerRadiusLeftBottom\":16,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#cbf797\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#a4e191\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"enableClickEvent\":true,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"主题布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true,\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmndcaakx4\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level2_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_tiny_bold_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"ButtonGroup\",\"id\":\"node_ocmnd5z3kwt\",\"props\":{\"dynamicButtons\":{\"variable\":\"blockList[0].btns\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\",\"value\":4}]}},\"responsiveLayoutWidth\":350,\"buttonsSource\":\"variable\",\"fixedButtonIds\":[],\"fixedButtons\":[],\"enableResponsiveLayout\":false,\"matchContent\":false,\"buttonSpacing\":8,\"margin\":-2,\"innerOffset\":0},\"title\":\"审批按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true}]},{\"componentName\":\"ColumnLayout\",\"id\":\"node_ocmnd5z3kwg\",\"props\":{\"columnCount\":2,\"columnWidth\":[{\"widthMode\":\"weighted\",\"weight\":1,\"width\":50},{\"widthMode\":\"fixed\",\"weight\":1,\"width\":30}],\"columnSpacing\":5,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":12,\"marginBottom\":12,\"childGravity\":\"leftBottom\",\"enableResponsiveLayout\":false,\"responsiveLayout\":1,\"margin\":12,\"innerOffset\":0},\"title\":\"分栏\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true,\"children\":[{\"componentName\":\"Column\",\"id\":\"node_ocmnd5z3kwh\",\"props\":{\"isAutoHeight\":true,\"height\":26,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"leftCenter\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnd5z3kwi\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${statusLine}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_ai_diagonal_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":true,\"enableIcon\":false,\"margin\":0,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"Column\",\"id\":\"node_ocmnd5z3kwj\",\"props\":{\"isAutoHeight\":true,\"height\":25,\"direction\":\"horizontal\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"rightCenter\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0,\"enableClickEvent\":true,\"actionType\":\"copy\",\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyType\":\"common\",\"copyValue\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${copy_content}\"},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"customHandler\":\"\",\"customDurboHandler\":\"\",\"enableCustomLocalData\":false,\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"customLocalData\":\"{}\",\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[{\"event\":{\"actionType\":\"copy\",\"copyType\":\"full-content\",\"copyValue\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"}}}],\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectParams\":[],\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentMode\":\"predefine\",\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false}},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Icon\",\"id\":\"node_ocmnlxveuu7\",\"props\":{\"marginLeft\":5,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"styleType\":\"custom\",\"styleToken\":\"common_body_text_style\",\"icon\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_duplicate\"},\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"size\":\"large\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"margin\":-2,\"innerOffset\":0},\"title\":\"图标\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]},{\"componentName\":\"Feedback\",\"id\":\"node_ocmncu094ga\",\"props\":{\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"visible\":{\"type\":\"dynamicVisible\",\"value\":false,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"marginConfigurable\":false,\"enableLikeDislike\":false,\"enableCopy\":true,\"copyType\":\"variable\",\"copyVariable\":{\"type\":\"variableValue\",\"variable\":\"content\",\"variableType\":\"global\",\"varType\":\"markdown\"},\"enableDivider\":false,\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0,\"enableRefresh\":false,\"actionType\":\"none\",\"refreshParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"refreshSuccessCondition\":{\"op\":\"and\",\"conditions\":[]},\"refreshSuccessToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"refreshFailureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"refreshVisible\":{\"type\":\"dynamicVisible\",\"value\":false,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"customRightArea\":{\"type\":\"JSSlot\",\"title\":\"插槽容器\",\"id\":\"node_ocmncu094gb\"}},\"title\":\"操作区\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AIGenerationProcessing\",\"id\":\"node_ocmncu094gc\",\"props\":{},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]}],\"i18n\":{},\"version\":\"1.0.0\"},\"mockData\":{\"cardData\":{\"approve_btns\":[{\"text\":\"次按钮\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"openLink\",\"params\":{\"url\":\"https://www.dingtalk.com\"}}},{\"text\":\"主按钮\",\"color\":\"blue\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}}],\"show_approve_btns\":true,\"statusLine\":\"gpt-5.4 | low | main\\n2m13s | ↑1.2k(C:800) ↓350 | DAPI+12\",\"copy_content\":\"\",\"hasAction\":true,\"content\":\"# 流式富文本内容\",\"quoteContent\":\"这是一条测试引用文本\",\"blockList\":[{\"text\":\"思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"type\":1,\"markdown\":\"> 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"mediaId\":\"\"},{\"text\":\"测试主题\",\"type\":5,\"markdown\":\"# markdown 内容\",\"mediaId\":\"\"},{\"markdown\":\"## 富文本二级标题\\n富文本正文\\n![外链图片测试](https://static.dingtalk.com/media/lADPDetfXH_Pn3HNAbrNBDg_1080_442.jpg)\",\"type\":0,\"text\":\"\",\"mediaId\":\"\"},{\"text\":\" Exec: 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"type\":2,\"markdown\":\"> Exec: 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"mediaId\":\"\"},{\"type\":4,\"btns\":[{\"text\":\"允许\",\"color\":\"blue\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}},{\"text\":\"本次允许\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}},{\"text\":\"驳回\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}}],\"text\":\"\",\"markdown\":\"# markdown 内容\",\"mediaId\":\"\"},{\"type\":3,\"mediaId\":\"@lALPDfmVRLVVAm_NBdzNBdw\",\"text\":\"图片标注123\",\"markdown\":\"# markdown 内容\"}],\"version\":1,\"flowStatus\":2,\"_IC_AIGC_DETAIL_URL\":{\"mob\":\"https://dingtalk.com\",\"pc\":\"https://dingtalk.com\"},\"btns\":[{\"text\":\"次按钮\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"openLink\",\"params\":{\"url\":\"https://www.dingtalk.com\"}}},{\"text\":\"主按钮\",\"color\":\"blue\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}}]},\"cardPrivateData\":{},\"localData\":{\"copy_content\":\"\",\"hasGuide1\":true,\"hasQuote\":true,\"quoteContent\":\"\",\"blockList\":[{\"text\":\"\",\"markdown\":\"# markdown 内容\",\"isTool\":true,\"type\":0,\"mediaId\":\"\",\"topic\":\"\",\"topics\":[{\"text\":\"\"}]}],\"taskInfo\":{\"model\":\"\",\"effort\":\"\",\"dap_usage\":0,\"taskTime\":0,\"dapi_usage\":0,\"agent\":\"\"},\"hasAction\":true,\"topic\":{\"text\":\"\",\"color\":\"\"},\"hasTopic\":true,\"version\":0,\"hasGuide\":false,\"guideString\":\"\",\"statusline\":\"\"},\"richTextData\":{\"cardData\":{\"content\":{\"items\":[{\"data\":{\"text\":\"流式富文本内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"},\"blockList\":[{\"markdown\":{\"items\":[{\"data\":{\"content\":[{\"data\":{\"text\":\"思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\"},\"style\":{\"colorTokenV2\":\"common_level2_base_color\",\"darkColor\":\"#A5A5A6\",\"lightColor\":\"#747576\",\"lineHeightToken\":\"common_body_text_style__line_height\",\"sizeToken\":\"common_h5_text_style__font_size\"},\"type\":\"text\"}]},\"type\":\"quote\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"富文本二级标题\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h2_text_style__line_height\",\"size\":19,\"sizeToken\":\"common_h2_text_style__font_size\"},\"type\":\"text\"},{\"data\":{},\"style\":{\"gap\":14},\"type\":\"paragraphSpace\"},{\"data\":{\"text\":\"富文本正文 \"},\"style\":{\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.5,\"lineHeightToken\":\"common_body_text_style__line_height\",\"size\":14,\"sizeToken\":\"common_body_text_style__font_size\"},\"type\":\"text\"},{\"data\":{\"alt\":\"外链图片测试\",\"url\":\"https://static.dingtalk.com/media/lADPDetfXH_Pn3HNAbrNBDg_1080_442.jpg\"},\"style\":{},\"type\":\"image\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"content\":[{\"data\":{\"text\":\"Exec: 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\"},\"style\":{\"colorTokenV2\":\"common_level2_base_color\",\"darkColor\":\"#A5A5A6\",\"lightColor\":\"#747576\",\"lineHeightToken\":\"common_body_text_style__line_height\",\"sizeToken\":\"common_h5_text_style__font_size\"},\"type\":\"text\"}]},\"type\":\"quote\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}}]},\"localData\":{\"blockList\":[{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}}]}}},\"renderContext\":{\"regenerateEnabled\":\"1\",\"regenerateIndex\":\"2\",\"regenerateTotal\":\"5\"},\"editVersion\":0,\"customWidgetInfo\":\"\",\"useCustomWidgetInfo\":false,\"variableList\":[{\"name\":\"version\",\"private\":false,\"type\":\"number\",\"id\":\"version\",\"description\":\"配置版本号\",\"editorVarType\":\"variables\"},{\"id\":\"content\",\"type\":\"markdown\",\"name\":\"content\",\"description\":\"大模型输出的流式markdown内容\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":false},{\"name\":\"quoteContent\",\"private\":false,\"type\":\"string\",\"id\":\"quoteContent\",\"description\":\"引用文本\",\"editorVarType\":\"variables\"},{\"name\":\"blockList\",\"private\":false,\"type\":\"loopArray\",\"id\":\"blockList\",\"description\":\"动态消息组\",\"editorVarType\":\"variables\",\"disabled\":false,\"schema\":[{\"id\":\"blockList[0].text\",\"type\":\"string\",\"name\":\"text\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"示例文本\"},{\"id\":\"blockList[0].markdown\",\"type\":\"markdown\",\"name\":\"markdown\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"正文\"},{\"id\":\"blockList[0].type\",\"type\":\"number\",\"name\":\"type\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"消息类型\"},{\"id\":\"blockList[0].mediaId\",\"type\":\"string\",\"name\":\"mediaId\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"图片id\"},{\"id\":\"blockList[0].btns\",\"type\":\"buttonGroup\",\"name\":\"btns\",\"private\":false,\"editorVarType\":\"variables\",\"schema\":[{\"id\":\"blockList[0].btns[0].text\",\"type\":\"string\",\"name\":\"text\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮文案\"},{\"id\":\"blockList[0].btns[0].color\",\"type\":\"string\",\"name\":\"color\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮颜色\"},{\"id\":\"blockList[0].btns[0].status\",\"type\":\"string\",\"name\":\"status\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮状态\"},{\"id\":\"blockList[0].btns[0].event\",\"type\":\"dynamicEvent\",\"name\":\"event\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮点击事件\",\"schema\":[{\"id\":\"blockList[0].btns[0].event.type\",\"type\":\"string\",\"name\":\"type\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件类型,支持有 openLink,sendCardRequest\"},{\"id\":\"blockList[0].btns[0].event.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件参数\",\"schema\":[{\"id\":\"blockList[0].btns[0].event.params.url\",\"type\":\"string\",\"name\":\"url\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"点击时打开的链接,可以配置为对象 { android: string; ios: string; pc: string } 分别给不同的平台设置不同的链接,当 type 为 openLink 时生效\"},{\"id\":\"blockList[0].btns[0].event.params.actionId\",\"type\":\"string\",\"name\":\"actionId\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"标记回传请求的 id,当 type 为 sendCardRequest 时生效\"},{\"id\":\"blockList[0].btns[0].event.params.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"回传请求的参数,当 type 为 sendCardRequest 时生效\"}]}]}],\"description\":\"动态按钮组\"}]},{\"name\":\"hasAction\",\"private\":false,\"type\":\"boolean\",\"id\":\"hasAction\",\"description\":\"有互动按钮\",\"editorVarType\":\"variables\"},{\"name\":\"statusLine\",\"private\":false,\"type\":\"string\",\"id\":\"statusLine\",\"description\":\"脚标字符串\",\"editorVarType\":\"variables\"},{\"name\":\"copy_content\",\"private\":false,\"type\":\"string\",\"id\":\"copy_content\",\"description\":\"回答转义字串\",\"editorVarType\":\"variables\"},{\"name\":\"approve_btns\",\"private\":false,\"type\":\"buttonGroup\",\"id\":\"approve_btns\",\"description\":\"审批按钮组\",\"editorVarType\":\"variables\",\"disabled\":false,\"schema\":[{\"id\":\"approve_btns[0].text\",\"type\":\"string\",\"name\":\"text\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮文案\"},{\"id\":\"approve_btns[0].color\",\"type\":\"string\",\"name\":\"color\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮颜色\"},{\"id\":\"approve_btns[0].status\",\"type\":\"string\",\"name\":\"status\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮状态\"},{\"id\":\"approve_btns[0].event\",\"type\":\"dynamicEvent\",\"name\":\"event\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮点击事件\",\"schema\":[{\"id\":\"approve_btns[0].event.type\",\"type\":\"string\",\"name\":\"type\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件类型,支持有 openLink,sendCardRequest\"},{\"id\":\"approve_btns[0].event.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件参数\",\"schema\":[{\"id\":\"approve_btns[0].event.params.url\",\"type\":\"string\",\"name\":\"url\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"点击时打开的链接,可以配置为对象 { android: string; ios: string; pc: string } 分别给不同的平台设置不同的链接,当 type 为 openLink 时生效\"},{\"id\":\"approve_btns[0].event.params.actionId\",\"type\":\"string\",\"name\":\"actionId\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"标记回传请求的 id,当 type 为 sendCardRequest 时生效\"},{\"id\":\"approve_btns[0].event.params.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"回传请求的参数,当 type 为 sendCardRequest 时生效\"}]}]}]},{\"name\":\"show_approve_btns\",\"private\":false,\"type\":\"boolean\",\"id\":\"show_approve_btns\",\"description\":\"是否有审批按钮组\",\"editorVarType\":\"variables\"},{\"id\":\"_IC_AIGC_DETAIL_URL\",\"name\":\"_IC_AIGC_DETAIL_URL\",\"editorVarType\":\"variables\",\"type\":\"object\",\"disabled\":true,\"visible\":false,\"private\":false,\"description\":\"内容生成过程链接\",\"schema\":[{\"id\":\"_IC_AIGC_DETAIL_URL.mob\",\"type\":\"string\",\"name\":\"mob\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"移动端链接\"},{\"id\":\"_IC_AIGC_DETAIL_URL.pc\",\"type\":\"string\",\"name\":\"pc\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"桌面端链接\"}]},{\"id\":\"flowStatus\",\"type\":\"string\",\"name\":\"flowStatus\",\"description\":\"AI卡片状态,包含 pending(1)、writing(2)、done(3)、doing(4)、failed(5)\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"visible\":false}],\"formList\":[],\"customContextList\":[],\"expList\":[],\"localList\":[],\"hsfList\":[],\"lwpList\":[],\"pageData\":{},\"extension\":{\"extendType\":\"AI\",\"aiStatusList\":[1,2,3],\"fileTypeList\":[{\"fileName\":\"event_chain\",\"fileSource\":\"{\\\"node_ocmnee6af5i_input\\\":{\\\"main_success_left_callback\\\":{\\\"type\\\":\\\"dtSendOutData\\\",\\\"params\\\":{\\\"actionType\\\":\\\"0\\\",\\\"cardInstanceId\\\":\\\"@data{data.cardInstanceId}\\\",\\\"actionId\\\":\\\"@toStr{'guideInput'}\\\",\\\"dataKey\\\":\\\"main_success_left_callback\\\",\\\"actionData\\\":{\\\"context\\\":\\\"@dtMapAppend{@data{data.renderContext},'platform','im','platformBizId',@data{data.renderContext.mid}}\\\",\\\"cardPrivateData\\\":{\\\"params\\\":\\\"@dtMapAppend{@dtMapAppend{null},@toStr{'guideInput'},@dtGetEventChainData{'main_success.value'}}\\\",\\\"actionIds\\\":[\\\"@toStr{'guideInput'}\\\"]}},\\\"requestStatusKey\\\":\\\"@concat{@toStr{'guideInput'},'_request_status'}\\\"},\\\"callback\\\":{}},\\\"main\\\":{\\\"type\\\":\\\"dtOpenLink\\\",\\\"params\\\":{},\\\"next\\\":\\\"@triple{@equal{'normal','normal'},'$(main_success)',''}\\\"},\\\"main_success\\\":{\\\"type\\\":\\\"dtCallAPI\\\",\\\"params\\\":{\\\"type\\\":\\\"callJSAPI\\\",\\\"params\\\":{\\\"apiName\\\":\\\"device.notification.prompt\\\",\\\"params\\\":{\\\"message\\\":\\\"@toStr{'请输入内容'}\\\",\\\"title\\\":\\\"@toStr{'添加指引'}\\\",\\\"buttonLabels\\\":\\\"@triple{@equal{@data{env},'pc'},@dtArrayAppend{null,@dti18NAdapter{'提交','提交','Submit'},@dti18NAdapter{'取消','取消','Cancel'}},@dtArrayAppend{null,@dti18NAdapter{'取消','取消','Cancel'},@dti18NAdapter{'提交','提交','Submit'}}}\\\",\\\"defaultText\\\":\\\"@toStr{''}\\\"}},\\\"dataKey\\\":\\\"main_success\\\"},\\\"dataKey\\\":\\\"main_success\\\",\\\"callback\\\":{\\\"success\\\":\\\"@triple{@equal{@toStr{@dtGetEventChainData{'main_success.buttonIndex'}},@triple{@equal{@data{env},'pc'},0,1}},'$(main_success_left_callback)',''}\\\"}}},\\\"ai_card_share_node_ocmncu094ga\\\":{\\\"main\\\":{\\\"type\\\":\\\"dtSendOutData\\\",\\\"params\\\":{\\\"actionType\\\":\\\"0\\\",\\\"cardInstanceId\\\":\\\"@data{data.cardInstanceId}\\\",\\\"actionId\\\":\\\"sys_action_ai_card_share\\\",\\\"requestStatusKey\\\":\\\"sys_action_ai_card_share_loading_status\\\",\\\"actionData\\\":{\\\"context\\\":\\\"@dtMapAppend{@data{data.renderContext},'platform','im','platformBizId',@data{data.renderContext.mid}}\\\",\\\"cardPrivateData\\\":{\\\"params\\\":{\\\"command\\\":\\\"shareAIConv\\\",\\\"params\\\":\\\"@data{data.cardData._IC_SHARE_CARD.params}\\\"},\\\"actionIds\\\":[\\\"sys_action_ai_card_share\\\"]}},\\\"dataKey\\\":\\\"main\\\"},\\\"dataKey\\\":\\\"main\\\",\\\"callback\\\":{\\\"success\\\":\\\"@triple{@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.isSuccess},'$(main_success_left_callback)','$(main_success_right_callback)'}\\\",\\\"failure\\\":\\\"$(main_success_right_callback)\\\"}},\\\"main_success_left_callback\\\":{\\\"type\\\":\\\"dtOpenLink\\\",\\\"params\\\":{\\\"url\\\":\\\"@triple{@and{@equal{@data{env},'android'},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.androidUrl}},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.androidUrl},@triple{@and{@equal{@data{env},'ios'},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.iosUrl}},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.iosUrl},@triple{@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.pcUrl},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.pcUrl},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.url}}}}\\\"}},\\\"main_success_right_callback\\\":{\\\"type\\\":\\\"dtToast\\\",\\\"params\\\":{\\\"type\\\":2,\\\"text\\\":\\\"@triple{@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.errorMsg},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.errorMsg},@dti18NAdapter{'操作失败,请稍候重试','操作失敗,請稍候重試','Operation failed, please try again later','Operation failed, please try again later','Operation failed, please try again later','Operation failed, please try again later','Operation failed, please try again later'}}\\\"}}}}\",\"fileType\":\"json\"}]}}","widgetInfo":"\n \n \n\n \n \n\n \n \n \n \n \n \n\n \n\n \n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n\n \n \n\n \n\n \n \n\n \n \n \n \n \n \n \n\n \n\n \n \n \n\n \n \n\n \n\n \n \n \n\n \n\n \n \n \n\n \n\n \n \n \n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n \n \n \n\n \n \n\n \n\n \n\n \n \n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n\n \n \n\n \n\n \n\n \n\n \n\n \n \n \n\n \n\n \n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n \n\n \n \n\n \n\n \n \n\n \n \n \n \n \n \n \n\n \n \n\n \n\n \n \n \n\n \n\n \n \n \n\n \n\n \n \n \n \n\n \n\n \n\n \n \n \n\n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n\n \n\n \n\n \n \n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n \n \n \n\n \n \n \n\n \n\n \n\n \n \n \n\n \n \n\n \n \n \n\n \n\n \n\n \n \n\n \n\n \n\n \n \n \n \n\n \n \n\n \n\n \n\n \n \n \n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n\n","type":"im","mode":"card"} \ No newline at end of file +{"editorData":"{\"schemaVersion\":\"3.0.0\",\"schema\":{\"componentsMap\":[{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AIPending\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AIPending\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardStatusContainer\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardStatusContainer\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"BaseText\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"BaseText\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Divider\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Divider\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"ButtonGroup\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"ButtonGroup\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"MarkdownBlock\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"MarkdownBlock\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Image\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Image\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Grid\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Grid\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Loop\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Loop\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Input\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Input\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Column\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Column\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"ColumnLayout\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"ColumnLayout\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"FixedSingleButton\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"FixedSingleButton\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardContent\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardContent\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Icon\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Icon\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AIGenerationProcessing\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AIGenerationProcessing\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"Feedback\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"Feedback\"},{\"package\":\"@ali/dxComponent\",\"version\":\"1.0.0\",\"exportName\":\"AICardContainer\",\"main\":\"./src/index.tsx\",\"destructuring\":false,\"subName\":\"\",\"componentName\":\"AICardContainer\"}],\"componentsTree\":[{\"componentName\":\"AICardContainer\",\"id\":\"node_ocmncu094g1\",\"props\":{\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePending\":true,\"enableWriting\":true,\"enableFailed\":false,\"enableDoing\":false,\"enableTitle\":false,\"operationPenalType\":\"prompt\",\"summaryContent\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"enableGradientBorder\":true,\"flowStatusVar\":{\"variable\":\"\",\"variableType\":\"global\",\"type\":\"variableValue\"},\"cardSizeMode\":\"adaptive\",\"cardSizeHeightMode\":\"adaptive\",\"cardSizeWidthMode\":\"adaptive\",\"cardSizeHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":226,\"variable\":\"\",\"variableType\":\"global\"},\"hasBackground\":false,\"backgroundType\":\"Standard\",\"standardBackgroundColor\":\"gray\",\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"enableFlowAbort\":true,\"enableEngineUpgrade\":false,\"enableExposeStatPoint\":false,\"enableDebugTool\":false},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_ocmncu094g2\",\"props\":{\"status\":1,\"enableExtend\":false,\"enableCollapse\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0},\"title\":\"处理中状态\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AIPending\",\"id\":\"node_ocmncu094g3\",\"props\":{\"pendingTip\":{\"i18n\":true,\"type\":\"dynamicString\",\"content\":{\"zh_Hans\":\"思考中...\",\"zh_Hant\":\"Progressing...\",\"en_US\":\"Progressing...\",\"ja_JP\":\"進行中...\",\"vi_VN\":\"Progressing...\",\"th_TH\":\"Progressing...\",\"id_ID\":\"Progressing...\"}},\"style\":\"embed\",\"hideIcon\":false},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_ocmncu094g4\",\"props\":{\"status\":2,\"enableExtend\":false,\"enableCollapse\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0},\"title\":\"输出中状态\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardContent\",\"id\":\"node_ocmncu094g5\",\"props\":{\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0,\"transformToEventChain\":false,\"disabledWhileForward\":false,\"enableStatPoint\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}]},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmncu094gf\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${quoteContent}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_DTXX_message_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":true,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Divider\",\"id\":\"node_ocmncu094gg\",\"props\":{\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"height\":30,\"direction\":\"horizontal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"color\":\"#1F111F2C\",\"margin\":-2,\"innerOffset\":0},\"title\":\"分割线\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Loop\",\"id\":\"node_ocmncu094gh\",\"props\":{\"listData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"blockList\"},\"direction\":\"vertical\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"isFixedWidth\":false,\"isAutoWidth\":false,\"width\":50,\"equalSpace\":false,\"flowLayout\":false,\"childGap\":false,\"childGapSize\":4,\"childDivider\":false,\"childDividerMarginLeft\":0,\"childDividerMarginRight\":0,\"childDividerMarginBottom\":0,\"childDividerMarginTop\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"scrollable\":false,\"childWidth\":\"match_content\",\"childDividerWidth\":0.5,\"childDividerColorDark\":\"rgba(255, 255, 255, 0.12)\",\"childDividerColorLight\":\"rgba(17, 31, 44, 0.12)\",\"paging\":false,\"hasMore\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onLoadMore\":{\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false}},\"isFixedHeight\":false,\"height\":100,\"flowDirection\":\"x\",\"margin\":0,\"innerOffset\":0},\"title\":\"循环渲染容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"ButtonGroup\",\"id\":\"node_ocmnee6af5g\",\"props\":{\"dynamicButtons\":{\"variable\":\"blockList[0].btns\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"blockList[0].btns\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"responsiveLayoutWidth\":350,\"buttonsSource\":\"variable\",\"fixedButtonIds\":[],\"fixedButtons\":[],\"enableResponsiveLayout\":false,\"matchContent\":false,\"buttonSpacing\":8,\"margin\":-2,\"innerOffset\":0},\"title\":\"审批按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"MarkdownBlock\",\"id\":\"node_ocmncu094g6\",\"props\":{\"isStreaming\":false,\"content\":{\"variable\":\"blockList[0].markdown\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"lt\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}}},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Image\",\"id\":\"node_ocmnd5z3kwq\",\"props\":{\"images\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"@lALPDfmVRLVVAm_NBdzNBdw\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"darkModeImages\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"single\":true,\"height\":{\"type\":\"dynamicWidth\",\"valueType\":\"fixed\",\"value\":200,\"variableType\":\"global\",\"variable\":\"\",\"full\":false,\"adaptive\":false},\"width\":{\"type\":\"dynamicWidth\",\"valueType\":\"full\",\"value\":100,\"variableType\":\"global\",\"variable\":\"\",\"full\":true,\"adaptive\":false},\"cornerRadius\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":6,\"variable\":\"\",\"variableType\":\"global\"},\"scaleType\":\"centerCrop\",\"enablePreview\":true,\"enableBorder\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"offsetLeft\":0,\"offsetRight\":0,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"adaptiveSize\":false,\"fixedRatio\":false,\"borderColor\":\"#297f8790\",\"enableStatPoint\":false,\"imageType\":\"adaptiveSize\",\"aspectRatio\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"margin\":-2,\"innerOffset\":0},\"title\":\"图片\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"BaseText\",\"id\":\"node_ocmnlxveuu8\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":0,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"图片标注\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Grid\",\"id\":\"node_ocmnd5z3kwx\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":true,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":5,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"childGravity\":\"center\",\"hasBackground\":true,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":-2,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Custom\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"extended_green0_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":16,\"cornerRadiusLeftTop\":16,\"cornerRadiusRightTop\":16,\"cornerRadiusRightBottom\":16,\"cornerRadiusLeftBottom\":16,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#cbf797\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#a4e191\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"enableClickEvent\":true,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"主题布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnd5z3kwy\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level2_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_tiny_bold_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]},{\"componentName\":\"Grid\",\"id\":\"node_ocmnlxveuu1\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"leftTop\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"MarkdownBlock\",\"id\":\"node_ocmnj1dw4n1\",\"props\":{\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"content\":{\"variable\":\"content\",\"variableType\":\"global\",\"type\":\"variableValue\",\"varType\":\"markdown\"},\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isMarkdownNotEmpty\",\"variable\":\"content\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"isStreaming\":true,\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"marginTop\":0,\"marginBottom\":0,\"marginLeft\":0,\"marginRight\":0},\"title\":\"流式答案\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"ColumnLayout\",\"id\":\"node_ocmnee6af51\",\"props\":{\"columnCount\":2,\"columnWidth\":[{\"widthMode\":\"weighted\",\"weight\":1,\"width\":50},{\"widthMode\":\"fixed\",\"weight\":1,\"width\":80}],\"columnSpacing\":8,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isTrue\",\"variable\":\"hasAction\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"childGravity\":\"center\",\"enableResponsiveLayout\":false,\"responsiveLayout\":1,\"margin\":-2,\"innerOffset\":0},\"title\":\"Action按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Column\",\"id\":\"node_ocmnee6af52\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"center\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Input\",\"id\":\"node_ocmnee6af5i\",\"props\":{\"placeholder\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"添加指引\"},\"currentValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"message\":{\"type\":\"dynamicString\",\"content\":\"请输入内容\",\"i18n\":false},\"title\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"添加指引\"},\"id\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"guideInput\"},\"params\":[{\"type\":\"builtIn\",\"variable\":\"\",\"value\":\"选中会话列表\",\"name\":\"guideInput\",\"variableType\":\"global\",\"id\":\"__built_in_inputResult__\"}],\"visible\":{\"type\":\"dynamicVisible\",\"value\":false,\"valueType\":\"fixed\",\"condition\":{\"op\":\"or\",\"conditions\":[]}},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"actionType\":\"request\",\"localVarAction\":{\"variable\":\"guideString\",\"variableType\":\"global\",\"varType\":\"string\",\"type\":\"variableValue\"},\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"inlineMode\":false,\"textArea\":false,\"minRows\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"maxRows\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":6,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"margin\":0,\"innerOffset\":0},\"title\":\"指引文本输入\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"Column\",\"id\":\"node_ocmnee6af53\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"rightCenter\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Grid\",\"id\":\"node_ocmnee6af55\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":true,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"rightCenter\",\"hasBackground\":true,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Custom\",\"backgroundType\":\"Custom\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"extended_red0_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"rgba(255,53,0,0.95)\",\"darkModeBackgroundColor\":\"#ca0505\",\"cornerRadius\":20,\"cornerRadiusLeftTop\":20,\"cornerRadiusRightTop\":20,\"cornerRadiusRightBottom\":20,\"cornerRadiusLeftBottom\":20,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#fcbb50\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f28326\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"actionType\":\"actionSheet\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":true,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"确认\"},\"confirmMessage\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"是否立即中止运行?\"},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"},\"actionSheetMessage\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"是否中止任务?\"},\"actionSheetItems\":[{\"id\":\"0\",\"actionSheetStyle\":\"destructive\",\"actionSheetName\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"立即中止\"},\"actionSheetDesc\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"},\"actionIconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_raise_hand\"},\"actionSheetAction\":\"request\",\"actionSheetRequestItemActionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"btn_stop\"},\"actionSheetRequestItemParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"true\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetRequestItemSuccessToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetUrlItemUrlType\":\"all\",\"actionSheetUrlItemAndroidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemPcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIsDtmd\":false,\"actionSheetUrlItemUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetOpenModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetOpenModalHeight\":500,\"actionSheetOpenModalWidth\":400,\"actionSheetApiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetApiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetApiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"}},{\"id\":\"1\",\"actionSheetStyle\":\"default\",\"actionSheetName\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"取消\"},\"actionSheetDesc\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"},\"actionIconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"actionSheetAction\":\"none\",\"actionSheetRequestItemActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetRequestItemParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetRequestItemSuccessToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetUrlItemUrlType\":\"all\",\"actionSheetUrlItemAndroidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemPcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetUrlItemIsDtmd\":false,\"actionSheetUrlItemUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"actionSheetOpenModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetOpenModalHeight\":500,\"actionSheetOpenModalWidth\":400,\"actionSheetApiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetApiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"actionSheetApiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"}}],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[{\"event\":{\"actionType\":\"url\",\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false}}],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"enableClickEvent\":true,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnee6af56\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"中止\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_raise_hand\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":6,\"marginRight\":6,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Custom\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level2_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_action_bold_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":false,\"enableIcon\":true,\"margin\":6,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]},{\"componentName\":\"BaseText\",\"id\":\"node_ocmncu094gn\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${statusLine}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_ai_diagonal_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":true,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"ButtonGroup\",\"id\":\"node_ocmpbcor1i2\",\"props\":{\"dynamicButtons\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"btns\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"and\",\"conditions\":[{\"value\":\"\",\"op\":\"isTrue\",\"variable\":\"show_approve_btns\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"responsiveLayoutWidth\":350,\"buttonsSource\":\"fixed\",\"fixedButtonIds\":[\"1\",\"2\",\"3\"],\"fixedButtons\":[{\"id\":\"1\",\"type\":\"button\"},{\"id\":\"2\",\"type\":\"button\"},{\"id\":\"3\",\"type\":\"button\"}],\"enableResponsiveLayout\":false,\"matchContent\":false,\"buttonSpacing\":8,\"margin\":-2,\"innerOffset\":0},\"title\":\"按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"FixedSingleButton\",\"id\":\"node_ocmpbcor1i3\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"允许一次\"},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"blue\",\"variable\":\"\",\"variableType\":\"global\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"$iwElAqNwbmcDBgTRBOYF0QTmBrDiyibODEbGnAngitJlqmwABwAIAAm3aW50ZXJhY3RpdmUtY2FyZC1lZGl0b3IKAAsA\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkModeIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"enableIcon\":true,\"iconPosition\":\"left\",\"iconType\":\"image\",\"iconFont\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"\"},\"variable\":\"\",\"variableType\":\"global\"},\"enableCornerMark\":false,\"cornerMarkPosition\":\"right\",\"cornerMarkText\":{\"type\":\"dynamicString\",\"content\":\"按钮角标文案\",\"i18n\":false},\"cornerMarkTextColor\":\"white\",\"cornerBackgroundColorType\":\"standard\",\"standardCornerBackgroundColor\":\"RedGradient\",\"customCornerBackgroundColor\":\"#FF6A00\",\"customCornerDarkModeBackgroundColor\":\"#FF6A00\",\"actionType\":\"request\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"allow-once\"},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"allow-once\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"},{\"type\":\"variable\",\"variable\":\"approveId\",\"value\":\"\",\"name\":\"approveId\",\"id\":\"6017\",\"variableType\":\"global\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableClickEvent\":true,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"widthMode\":\"match_parent\",\"width\":200,\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePadButtonText\":false,\"cornerMarkVisible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"按钮1\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"FixedSingleButton\",\"id\":\"node_ocmpbcor1i4\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"总是允许\"},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"blue\",\"variable\":\"\",\"variableType\":\"global\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"$iwELAqNwbmcDBgTRBOYF0QTmBrAVv0YTQHioCwngjGjME4IABwAIAAm3aW50ZXJhY3RpdmUtY2FyZC1lZGl0b3IKAAsA\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkModeIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"enableIcon\":true,\"iconPosition\":\"left\",\"iconType\":\"image\",\"iconFont\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"variable\":\"\",\"variableType\":\"global\"},\"enableCornerMark\":false,\"cornerMarkPosition\":\"right\",\"cornerMarkText\":{\"type\":\"dynamicString\",\"content\":\"按钮角标文案\",\"i18n\":false},\"cornerMarkTextColor\":\"white\",\"cornerBackgroundColorType\":\"standard\",\"standardCornerBackgroundColor\":\"RedGradient\",\"customCornerBackgroundColor\":\"#FF6A00\",\"customCornerDarkModeBackgroundColor\":\"#FF6A00\",\"actionType\":\"request\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"allow-always\"},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"allow-always\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"},{\"type\":\"variable\",\"variable\":\"approveId\",\"value\":\"app\",\"name\":\"approveId\",\"id\":\"4466\",\"variableType\":\"global\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableClickEvent\":true,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"widthMode\":\"match_parent\",\"width\":200,\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePadButtonText\":false,\"cornerMarkVisible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"按钮2\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"FixedSingleButton\",\"id\":\"node_ocmpbcor1i5\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"拒绝\"},\"status\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"normal\",\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"blue\",\"variable\":\"\",\"variableType\":\"global\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"$iwElAqNwbmcDBgTRBOYF0QTmBrBhBQt9e-UVYgngjO_OkvEABwAIAAm3aW50ZXJhY3RpdmUtY2FyZC1lZGl0b3IKAAsA\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkModeIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"enableIcon\":true,\"iconPosition\":\"left\",\"iconType\":\"image\",\"iconFont\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"variable\":\"\",\"variableType\":\"global\"},\"enableCornerMark\":false,\"cornerMarkPosition\":\"right\",\"cornerMarkText\":{\"type\":\"dynamicString\",\"content\":\"按钮角标文案\",\"i18n\":false},\"cornerMarkTextColor\":\"white\",\"cornerBackgroundColorType\":\"standard\",\"standardCornerBackgroundColor\":\"RedGradient\",\"customCornerBackgroundColor\":\"#FF6A00\",\"customCornerDarkModeBackgroundColor\":\"#FF6A00\",\"actionType\":\"request\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"deny\"},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"deny\",\"name\":\"action\",\"variableType\":\"global\",\"id\":\"1\"},{\"type\":\"variable\",\"variable\":\"approveId\",\"value\":\"app\",\"name\":\"approveId\",\"id\":\"6252\",\"variableType\":\"global\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableClickEvent\":true,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"widthMode\":\"match_parent\",\"width\":200,\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"enablePadButtonText\":false,\"cornerMarkVisible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"按钮3\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]},{\"componentName\":\"AICardStatusContainer\",\"id\":\"node_ocmncu094g7\",\"props\":{\"status\":3,\"enableExtend\":false,\"enableCollapse\":false,\"autoFoldConfig\":{\"needFold\":true,\"heightLimit\":480,\"foldStatusLocalDataKey\":\"_cardFoldStatusLocalDataKey\"},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0},\"title\":\"完成状态\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AICardContent\",\"id\":\"node_ocmncu094g8\",\"props\":{\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0,\"transformToEventChain\":false,\"disabledWhileForward\":false,\"enableStatPoint\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}]},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnd5z3kwa\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${quoteContent}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_DTXX_message_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":true,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"Divider\",\"id\":\"node_ocmnd5z3kwb\",\"props\":{\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"height\":30,\"direction\":\"horizontal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"quoteContent\",\"variableType\":\"global\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"color\":\"#1F111F2C\",\"margin\":-2,\"innerOffset\":0},\"title\":\"分割线\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"Loop\",\"id\":\"node_ocmnd5z3kwc\",\"props\":{\"listData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"blockList\"},\"direction\":\"vertical\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"isFixedWidth\":false,\"isAutoWidth\":false,\"width\":50,\"equalSpace\":false,\"flowLayout\":false,\"childGap\":false,\"childGapSize\":4,\"childDivider\":false,\"childDividerMarginLeft\":0,\"childDividerMarginRight\":0,\"childDividerMarginBottom\":0,\"childDividerMarginTop\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"scrollable\":false,\"childWidth\":\"match_content\",\"childDividerWidth\":0.5,\"childDividerColorDark\":\"rgba(255, 255, 255, 0.12)\",\"childDividerColorLight\":\"rgba(17, 31, 44, 0.12)\",\"paging\":false,\"hasMore\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onLoadMore\":{\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false}},\"isFixedHeight\":false,\"height\":100,\"flowDirection\":\"x\",\"margin\":0,\"innerOffset\":0},\"title\":\"循环渲染容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true,\"children\":[{\"componentName\":\"MarkdownBlock\",\"id\":\"node_ocmnd5z3kwf\",\"props\":{\"isStreaming\":false,\"content\":{\"variable\":\"blockList[0].markdown\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"mdVer\":0,\"icon\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"emoji\"},\"enableLinkStatPoint\":false,\"linkStatPoint\":{\"type\":\"dynamicString\",\"content\":\"Page_InteractiveCard__Click_markdownOpenlink\",\"i18n\":false},\"linkStatPointParams\":[],\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"lt\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}}},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"},{\"componentName\":\"Image\",\"id\":\"node_ocmnd5z3kws\",\"props\":{\"images\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"@lALPDfmVRLVVAm_NBdzNBdw\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"darkModeImages\":{\"defaultLang\":\"zh_Hans\",\"i18n\":false,\"content\":{\"zh_Hans\":{\"value\":\"\",\"valueType\":\"variable\",\"type\":\"dynamicImage\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\"}}},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"single\":true,\"height\":{\"type\":\"dynamicWidth\",\"valueType\":\"fixed\",\"value\":200,\"variableType\":\"global\",\"variable\":\"\",\"full\":false,\"adaptive\":false},\"width\":{\"type\":\"dynamicWidth\",\"valueType\":\"full\",\"value\":100,\"variableType\":\"global\",\"variable\":\"\",\"full\":true,\"adaptive\":false},\"cornerRadius\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":6,\"variable\":\"\",\"variableType\":\"global\"},\"scaleType\":\"centerCrop\",\"enablePreview\":true,\"enableBorder\":false,\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"offsetLeft\":0,\"offsetRight\":0,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":3,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"adaptiveSize\":false,\"fixedRatio\":false,\"borderColor\":\"#297f8790\",\"enableStatPoint\":false,\"imageType\":\"adaptiveSize\",\"aspectRatio\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"margin\":-2,\"innerOffset\":0},\"title\":\"图片\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"BaseText\",\"id\":\"node_ocmnlxveuu9\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_parent\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":0,\"marginBottom\":6,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":\"\",\"op\":\"isNotEmpty\",\"variable\":\"blockList[0].mediaId\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"图片标注\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true},{\"componentName\":\"Grid\",\"id\":\"node_ocmndcaakx3\",\"props\":{\"isAutoHeight\":true,\"height\":100,\"direction\":\"vertical\",\"isAutoWidth\":true,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"value\":5,\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\"}]}},\"childGravity\":\"center\",\"hasBackground\":true,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":-2,\"marginLeft\":12,\"marginRight\":12,\"marginTop\":2,\"marginBottom\":2,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":12,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":4,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Custom\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"extended_green0_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":16,\"cornerRadiusLeftTop\":16,\"cornerRadiusRightTop\":16,\"cornerRadiusRightBottom\":16,\"cornerRadiusLeftBottom\":16,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#cbf797\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#a4e191\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"actionType\":\"url\",\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"copyValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"copyType\":\"common\",\"customHandler\":\"\",\"customDurboHandler\":\"\",\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableCustomLocalData\":false,\"customLocalData\":\"{}\",\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[],\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectParams\":[],\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentMode\":\"predefine\",\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"enableClickEvent\":true,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"主题布局容器\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true,\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmndcaakx4\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${loop.text}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"icon\":\"\",\"iconType\":\"ddIcon\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level2_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"center\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_tiny_bold_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":false,\"enableIcon\":false,\"margin\":-2,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"ButtonGroup\",\"id\":\"node_ocmnd5z3kwt\",\"props\":{\"dynamicButtons\":{\"variable\":\"blockList[0].btns\",\"variableType\":\"loop\",\"type\":\"variableValue\"},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":6,\"marginBottom\":6,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"condition\",\"condition\":{\"op\":\"or\",\"conditions\":[{\"op\":\"equal\",\"variable\":\"blockList[0].type\",\"variableType\":\"loop\",\"type\":\"variable\",\"valueType\":\"fixed\",\"valueVariableType\":\"global\",\"value\":4}]}},\"responsiveLayoutWidth\":350,\"buttonsSource\":\"variable\",\"fixedButtonIds\":[],\"fixedButtons\":[],\"enableResponsiveLayout\":false,\"matchContent\":false,\"buttonSpacing\":8,\"margin\":-2,\"innerOffset\":0},\"title\":\"审批按钮组\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true}]},{\"componentName\":\"ColumnLayout\",\"id\":\"node_ocmnd5z3kwg\",\"props\":{\"columnCount\":2,\"columnWidth\":[{\"widthMode\":\"weighted\",\"weight\":1,\"width\":50},{\"widthMode\":\"fixed\",\"weight\":1,\"width\":30}],\"columnSpacing\":5,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"marginLeft\":12,\"marginRight\":12,\"marginTop\":12,\"marginBottom\":12,\"childGravity\":\"leftBottom\",\"enableResponsiveLayout\":false,\"responsiveLayout\":1,\"margin\":12,\"innerOffset\":0},\"title\":\"分栏\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"isFromCopy\":true,\"children\":[{\"componentName\":\"Column\",\"id\":\"node_ocmnd5z3kwh\",\"props\":{\"isAutoHeight\":true,\"height\":26,\"direction\":\"vertical\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"leftCenter\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"BaseText\",\"id\":\"node_ocmnd5z3kwi\",\"props\":{\"text\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${statusLine}\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"iconType\":\"iconCode\",\"iconFont\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_ai_diagonal_outlined\"},\"icon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"darkIcon\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"widthMode\":\"match_content\",\"maxWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"fixedWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"fontColorType\":\"Standard\",\"enableHighlight\":false,\"maxLine\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":2,\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"customLightColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#35404b\",\"variable\":\"\",\"variableType\":\"global\"},\"customDarkColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#f6f6f6\",\"variable\":\"\",\"variableType\":\"global\"},\"gravity\":\"left\",\"fontSizeType\":\"Standard\",\"styleType\":\"standard\",\"styleToken\":\"common_footnote_text_style\",\"size\":\"middle\",\"customFontSize\":15,\"customFontLineHeight\":22,\"bold\":false,\"italic\":false,\"strikeThrough\":false,\"lineHeight\":\"normal\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"autoMaxWidth\":true,\"enableIcon\":false,\"margin\":0,\"innerOffset\":0},\"title\":\"基础文本\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]},{\"componentName\":\"Column\",\"id\":\"node_ocmnd5z3kwj\",\"props\":{\"isAutoHeight\":true,\"height\":25,\"direction\":\"horizontal\",\"isAutoWidth\":false,\"width\":50,\"isFixedWidth\":false,\"enableColSpan\":false,\"colSpan\":0,\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"childGravity\":\"rightCenter\",\"hasBackground\":false,\"hasHoverBackground\":false,\"hoverBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_overlay_hover_color\",\"variable\":\"\",\"variableType\":\"global\"},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"hasBorder\":false,\"borderWidth\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":1,\"variable\":\"\",\"variableType\":\"global\"},\"borderColor\":\"#F6F6F6\",\"darkModeBorderColor\":\"#3C3C3C\",\"margin\":0,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"paddingBottom\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingLeft\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingRight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"paddingTop\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":8,\"variable\":\"\",\"variableType\":\"global\"},\"standardGradientBackground\":\"AIGradient\",\"hasGradientBackground\":false,\"gradientBackgroundType\":\"Standard\",\"backgroundType\":\"Standard\",\"standardBackgroundColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_bg_color\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientStartColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"enableGradientMiddleColor\":false,\"backgroundGradientMiddleColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientEndColor\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundGradientDirection\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"backgroundColor\":\"#F6F6F6\",\"darkModeBackgroundColor\":\"#3C3C3C\",\"cornerRadius\":4,\"cornerRadiusLeftTop\":4,\"cornerRadiusRightTop\":4,\"cornerRadiusRightBottom\":4,\"cornerRadiusLeftBottom\":4,\"blur\":false,\"isFullHeight\":false,\"bgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"enableDarkBgGradient\":false,\"darkBgGradient\":[{\"type\":\"linear\",\"direction\":{\"type\":\"dynamicSelect\",\"valueType\":\"fixed\",\"value\":\"toBottom\",\"variable\":\"\",\"variableType\":\"global\"},\"colors\":[{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"},{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"#ffffff\",\"variable\":\"\",\"variableType\":\"global\"}]}],\"statPoint\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"statPointParams\":[],\"disabledWhileForward\":false,\"enableOnAppear\":false,\"enableMouseEvent\":false,\"onMouseEnterActionType\":\"setLocalState\",\"onMouseEnterLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseEnterBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseEnterStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"onMouseLeaveActionType\":\"setLocalState\",\"onMouseLeaveLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"onMouseLeaveBooleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveNumberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"onMouseLeaveStringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"enableExposeStatPoint\":false,\"minHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"maxHeight\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"transformToEventChain\":false,\"enableStatPoint\":false,\"innerOffset\":0,\"enableClickEvent\":true,\"actionType\":\"copy\",\"actionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"successCondition\":{\"op\":\"and\",\"conditions\":[]},\"successToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"failureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"copyType\":\"common\",\"copyValue\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"${copy_content}\"},\"actionSheetTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"actionSheetMessage\":{\"type\":\"dynamicString\",\"content\":\"面板描述\",\"i18n\":false},\"actionSheetItems\":[],\"confirmTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"confirmMessage\":{\"type\":\"dynamicString\",\"content\":\"内容\",\"i18n\":false},\"confirmOkText\":{\"type\":\"dynamicString\",\"content\":\"确认\",\"i18n\":false},\"confirmOkAction\":\"request\",\"confirmOkReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmOkReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmOkReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelText\":{\"type\":\"dynamicString\",\"content\":\"取消\",\"i18n\":false},\"confirmCancelAction\":\"none\",\"confirmCancelReqActionActionId\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"confirmCancelReqActionParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"confirmCancelReqActionToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastType\":1,\"toastActionText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"toastActionUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"customHandler\":\"\",\"customDurboHandler\":\"\",\"enableCustomLocalData\":false,\"localVarAction\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"keyOfDynamicObject\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"booleanLocalValue\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"numberLocalValue\":{\"type\":\"dynamicNumber\",\"valueType\":\"fixed\",\"value\":0,\"variable\":\"\",\"variableType\":\"global\"},\"stringLocalValue\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"customLocalData\":\"{}\",\"openModalTitle\":{\"type\":\"dynamicString\",\"content\":\"标题\",\"i18n\":false},\"openModalHeight\":500,\"openModalWidth\":400,\"events\":[{\"event\":{\"actionType\":\"copy\",\"copyType\":\"full-content\",\"copyValue\":{\"i18n\":false,\"type\":\"dynamicString\",\"content\":\"\"}}}],\"apiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"apiParams\":[{\"type\":\"variable\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"apiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectActionType\":\"setLocalState\",\"selectDataKey\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"selectParams\":[],\"selectOptions\":{\"type\":\"dynamicSelectOptions\",\"valueType\":\"fixed\",\"value\":[],\"variable\":\"\",\"variableType\":\"global\"},\"selectIndex\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fixWidth\":false,\"fixedWidth\":0,\"autoWidth\":false,\"dataSources\":[],\"updateTargetVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"updateTargetValue\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"jsapiName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"jsapiParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"jsapiRetLocalVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"videoUrl\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"dynamicEventVar\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"fold\":{\"type\":\"dynamicBoolean\",\"valueType\":\"fixed\",\"value\":false,\"variable\":\"\",\"variableType\":\"global\"},\"commentData\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"commentMode\":\"predefine\",\"commentContent\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"commentExtraParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"urlType\":\"all\",\"url\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"androidUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"pcUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"iosUrl\":{\"type\":\"dynamicLink\",\"value\":\"\",\"valueType\":\"fixed\",\"variable\":\"\",\"variableType\":\"global\"},\"isDtmd\":false,\"copyVariable\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"callAPIName\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"callAPIType\":\"jsapi\",\"callAPIParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"isCallAPIParamsVariable\":false,\"callAPIVariableParams\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupCreateResult\":{\"type\":\"variableValue\",\"variableType\":\"global\",\"variable\":\"\"},\"groupName\":{\"type\":\"dynamicString\",\"content\":\"群名称\",\"i18n\":false},\"groupTemplateID\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false}},\"title\":\"列\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"Icon\",\"id\":\"node_ocmnlxveuu7\",\"props\":{\"marginLeft\":5,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"styleType\":\"custom\",\"styleToken\":\"common_body_text_style\",\"icon\":{\"type\":\"dynamicIcon\",\"valueType\":\"fixed\",\"value\":{\"type\":\"icon\",\"iconType\":\"ddIcon\",\"icon\":\"icon_duplicate\"},\"variable\":\"\",\"variableType\":\"global\"},\"color\":{\"type\":\"dynamicColor\",\"valueType\":\"fixed\",\"value\":\"common_level3_base_color\",\"variable\":\"\",\"variableType\":\"global\"},\"size\":\"large\",\"visible\":{\"type\":\"dynamicVisible\",\"value\":true,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"hoverText\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"margin\":-2,\"innerOffset\":0},\"title\":\"图标\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]},{\"componentName\":\"Feedback\",\"id\":\"node_ocmncu094ga\",\"props\":{\"params\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"visible\":{\"type\":\"dynamicVisible\",\"value\":false,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"marginConfigurable\":false,\"enableLikeDislike\":false,\"enableCopy\":true,\"copyType\":\"variable\",\"copyVariable\":{\"type\":\"variableValue\",\"variable\":\"content\",\"variableType\":\"global\",\"varType\":\"markdown\"},\"enableDivider\":false,\"margin\":-2,\"marginLeft\":0,\"marginRight\":0,\"marginTop\":0,\"marginBottom\":0,\"innerOffset\":0,\"enableRefresh\":false,\"actionType\":\"none\",\"refreshParams\":[{\"type\":\"fixed\",\"variable\":\"\",\"value\":\"\",\"name\":\"\",\"variableType\":\"global\",\"id\":\"1\"}],\"refreshSuccessCondition\":{\"op\":\"and\",\"conditions\":[]},\"refreshSuccessToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"refreshFailureToast\":{\"type\":\"dynamicString\",\"content\":\"\",\"i18n\":false},\"refreshVisible\":{\"type\":\"dynamicVisible\",\"value\":false,\"valueType\":\"fixed\",\"condition\":{\"op\":\"and\",\"conditions\":[]}},\"customRightArea\":{\"type\":\"JSSlot\",\"title\":\"插槽容器\",\"id\":\"node_ocmncu094gb\"}},\"title\":\"操作区\",\"hidden\":false,\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\",\"children\":[{\"componentName\":\"AIGenerationProcessing\",\"id\":\"node_ocmncu094gc\",\"props\":{},\"hidden\":false,\"title\":\"\",\"isLocked\":false,\"condition\":true,\"conditionGroup\":\"\"}]}]}]}],\"i18n\":{},\"version\":\"1.0.0\"},\"mockData\":{\"cardData\":{\"approveId\":\"\",\"approve_btns\":[{\"text\":\"次按钮\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"openLink\",\"params\":{\"url\":\"https://www.dingtalk.com\"}}},{\"text\":\"主按钮\",\"color\":\"blue\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}}],\"show_approve_btns\":true,\"statusLine\":\"gpt-5.4 | low | main\\n2m13s | ↑1.2k(C:800) ↓350 | DAPI+12\",\"copy_content\":\"\",\"hasAction\":true,\"content\":\"# 流式富文本内容\",\"quoteContent\":\"这是一条测试引用文本\",\"blockList\":[{\"text\":\"思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"type\":1,\"markdown\":\"> 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"mediaId\":\"\"},{\"text\":\"测试主题\",\"type\":5,\"markdown\":\"# markdown 内容\",\"mediaId\":\"\"},{\"markdown\":\"## 富文本二级标题\\n富文本正文\\n![外链图片测试](https://static.dingtalk.com/media/lADPDetfXH_Pn3HNAbrNBDg_1080_442.jpg)\",\"type\":0,\"text\":\"\",\"mediaId\":\"\"},{\"text\":\" Exec: 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"type\":2,\"markdown\":\"> Exec: 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\",\"mediaId\":\"\"},{\"type\":4,\"btns\":[{\"text\":\"允许\",\"color\":\"blue\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}},{\"text\":\"本次允许\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}},{\"text\":\"驳回\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}}],\"text\":\"\",\"markdown\":\"# markdown 内容\",\"mediaId\":\"\"},{\"type\":3,\"mediaId\":\"@lALPDfmVRLVVAm_NBdzNBdw\",\"text\":\"图片标注123\",\"markdown\":\"# markdown 内容\"}],\"version\":1,\"flowStatus\":2,\"_IC_AIGC_DETAIL_URL\":{\"mob\":\"https://dingtalk.com\",\"pc\":\"https://dingtalk.com\"},\"btns\":[{\"text\":\"次按钮\",\"color\":\"gray\",\"status\":\"normal\",\"event\":{\"type\":\"openLink\",\"params\":{\"url\":\"https://www.dingtalk.com\"}}},{\"text\":\"主按钮\",\"color\":\"blue\",\"status\":\"normal\",\"event\":{\"type\":\"sendCardRequest\",\"params\":{\"actionId\":\"request\",\"params\":{\"key\":\"value\"}}}}]},\"cardPrivateData\":{},\"localData\":{\"copy_content\":\"\",\"hasGuide1\":true,\"hasQuote\":true,\"quoteContent\":\"\",\"blockList\":[{\"text\":\"\",\"markdown\":\"# markdown 内容\",\"isTool\":true,\"type\":0,\"mediaId\":\"\",\"topic\":\"\",\"topics\":[{\"text\":\"\"}]}],\"taskInfo\":{\"model\":\"\",\"effort\":\"\",\"dap_usage\":0,\"taskTime\":0,\"dapi_usage\":0,\"agent\":\"\"},\"hasAction\":true,\"topic\":{\"text\":\"\",\"color\":\"\"},\"hasTopic\":true,\"version\":0,\"hasGuide\":false,\"guideString\":\"\",\"statusline\":\"\"},\"richTextData\":{\"cardData\":{\"content\":{\"items\":[{\"data\":{\"text\":\"流式富文本内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"},\"blockList\":[{\"markdown\":{\"items\":[{\"data\":{\"content\":[{\"data\":{\"text\":\"思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\"},\"style\":{\"colorTokenV2\":\"common_level2_base_color\",\"darkColor\":\"#A5A5A6\",\"lightColor\":\"#747576\",\"lineHeightToken\":\"common_body_text_style__line_height\",\"sizeToken\":\"common_h5_text_style__font_size\"},\"type\":\"text\"}]},\"type\":\"quote\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"富文本二级标题\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h2_text_style__line_height\",\"size\":19,\"sizeToken\":\"common_h2_text_style__font_size\"},\"type\":\"text\"},{\"data\":{},\"style\":{\"gap\":14},\"type\":\"paragraphSpace\"},{\"data\":{\"text\":\"富文本正文 \"},\"style\":{\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.5,\"lineHeightToken\":\"common_body_text_style__line_height\",\"size\":14,\"sizeToken\":\"common_body_text_style__font_size\"},\"type\":\"text\"},{\"data\":{\"alt\":\"外链图片测试\",\"url\":\"https://static.dingtalk.com/media/lADPDetfXH_Pn3HNAbrNBDg_1080_442.jpg\"},\"style\":{},\"type\":\"image\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"content\":[{\"data\":{\"text\":\"Exec: 思考消息,注意单段无法换行的思考消息。思考消息,注意单段无法换行的思考消息\"},\"style\":{\"colorTokenV2\":\"common_level2_base_color\",\"darkColor\":\"#A5A5A6\",\"lightColor\":\"#747576\",\"lineHeightToken\":\"common_body_text_style__line_height\",\"sizeToken\":\"common_h5_text_style__font_size\"},\"type\":\"text\"}]},\"type\":\"quote\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}},{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}}]},\"localData\":{\"blockList\":[{\"markdown\":{\"items\":[{\"data\":{\"text\":\"markdown 内容\"},\"style\":{\"bold\":1,\"colorTokenV2\":\"common_level1_base_color\",\"darkColor\":\"#FFFFFF\",\"lightColor\":\"#171A1D\",\"lineHeight\":1.4,\"lineHeightToken\":\"common_h1_text_style__line_height\",\"size\":20,\"sizeToken\":\"common_h1_text_style__font_size\"},\"type\":\"text\"}],\"version\":\"1.1\"}}]}}},\"renderContext\":{\"regenerateEnabled\":\"1\",\"regenerateIndex\":\"2\",\"regenerateTotal\":\"5\"},\"editVersion\":0,\"customWidgetInfo\":\"\",\"useCustomWidgetInfo\":false,\"variableList\":[{\"name\":\"version\",\"private\":false,\"type\":\"number\",\"id\":\"version\",\"description\":\"配置版本号\",\"editorVarType\":\"variables\"},{\"id\":\"content\",\"type\":\"markdown\",\"name\":\"content\",\"description\":\"大模型输出的流式markdown内容\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":false},{\"name\":\"quoteContent\",\"private\":false,\"type\":\"string\",\"id\":\"quoteContent\",\"description\":\"引用文本\",\"editorVarType\":\"variables\"},{\"name\":\"blockList\",\"private\":false,\"type\":\"loopArray\",\"id\":\"blockList\",\"description\":\"动态消息组\",\"editorVarType\":\"variables\",\"disabled\":false,\"schema\":[{\"id\":\"blockList[0].text\",\"type\":\"string\",\"name\":\"text\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"示例文本\"},{\"id\":\"blockList[0].markdown\",\"type\":\"markdown\",\"name\":\"markdown\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"正文\"},{\"id\":\"blockList[0].type\",\"type\":\"number\",\"name\":\"type\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"消息类型\"},{\"id\":\"blockList[0].mediaId\",\"type\":\"string\",\"name\":\"mediaId\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"图片id\"},{\"id\":\"blockList[0].btns\",\"type\":\"buttonGroup\",\"name\":\"btns\",\"private\":false,\"editorVarType\":\"variables\",\"schema\":[{\"id\":\"blockList[0].btns[0].text\",\"type\":\"string\",\"name\":\"text\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮文案\"},{\"id\":\"blockList[0].btns[0].color\",\"type\":\"string\",\"name\":\"color\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮颜色\"},{\"id\":\"blockList[0].btns[0].status\",\"type\":\"string\",\"name\":\"status\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮状态\"},{\"id\":\"blockList[0].btns[0].event\",\"type\":\"dynamicEvent\",\"name\":\"event\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮点击事件\",\"schema\":[{\"id\":\"blockList[0].btns[0].event.type\",\"type\":\"string\",\"name\":\"type\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件类型,支持有 openLink,sendCardRequest\"},{\"id\":\"blockList[0].btns[0].event.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件参数\",\"schema\":[{\"id\":\"blockList[0].btns[0].event.params.url\",\"type\":\"string\",\"name\":\"url\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"点击时打开的链接,可以配置为对象 { android: string; ios: string; pc: string } 分别给不同的平台设置不同的链接,当 type 为 openLink 时生效\"},{\"id\":\"blockList[0].btns[0].event.params.actionId\",\"type\":\"string\",\"name\":\"actionId\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"标记回传请求的 id,当 type 为 sendCardRequest 时生效\"},{\"id\":\"blockList[0].btns[0].event.params.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"回传请求的参数,当 type 为 sendCardRequest 时生效\"}]}]}],\"description\":\"动态按钮组\"}]},{\"name\":\"hasAction\",\"private\":false,\"type\":\"boolean\",\"id\":\"hasAction\",\"description\":\"有互动按钮\",\"editorVarType\":\"variables\"},{\"name\":\"statusLine\",\"private\":false,\"type\":\"string\",\"id\":\"statusLine\",\"description\":\"脚标字符串\",\"editorVarType\":\"variables\"},{\"name\":\"copy_content\",\"private\":false,\"type\":\"string\",\"id\":\"copy_content\",\"description\":\"回答转义字串\",\"editorVarType\":\"variables\"},{\"name\":\"approve_btns\",\"private\":false,\"type\":\"buttonGroup\",\"id\":\"approve_btns\",\"description\":\"审批按钮组\",\"editorVarType\":\"variables\",\"disabled\":false,\"schema\":[{\"id\":\"approve_btns[0].text\",\"type\":\"string\",\"name\":\"text\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮文案\"},{\"id\":\"approve_btns[0].color\",\"type\":\"string\",\"name\":\"color\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮颜色\"},{\"id\":\"approve_btns[0].status\",\"type\":\"string\",\"name\":\"status\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮状态\"},{\"id\":\"approve_btns[0].event\",\"type\":\"dynamicEvent\",\"name\":\"event\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"按钮点击事件\",\"schema\":[{\"id\":\"approve_btns[0].event.type\",\"type\":\"string\",\"name\":\"type\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件类型,支持有 openLink,sendCardRequest\"},{\"id\":\"approve_btns[0].event.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"动态事件参数\",\"schema\":[{\"id\":\"approve_btns[0].event.params.url\",\"type\":\"string\",\"name\":\"url\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"点击时打开的链接,可以配置为对象 { android: string; ios: string; pc: string } 分别给不同的平台设置不同的链接,当 type 为 openLink 时生效\"},{\"id\":\"approve_btns[0].event.params.actionId\",\"type\":\"string\",\"name\":\"actionId\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"标记回传请求的 id,当 type 为 sendCardRequest 时生效\"},{\"id\":\"approve_btns[0].event.params.params\",\"type\":\"object\",\"name\":\"params\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"description\":\"回传请求的参数,当 type 为 sendCardRequest 时生效\"}]}]}]},{\"name\":\"show_approve_btns\",\"private\":false,\"type\":\"boolean\",\"id\":\"show_approve_btns\",\"description\":\"是否有审批按钮组\",\"editorVarType\":\"variables\"},{\"name\":\"approveId\",\"private\":false,\"type\":\"string\",\"id\":\"approveId\",\"description\":\"审批ID\",\"editorVarType\":\"variables\"},{\"id\":\"_IC_AIGC_DETAIL_URL\",\"name\":\"_IC_AIGC_DETAIL_URL\",\"editorVarType\":\"variables\",\"type\":\"object\",\"disabled\":true,\"visible\":false,\"private\":false,\"description\":\"内容生成过程链接\",\"schema\":[{\"id\":\"_IC_AIGC_DETAIL_URL.mob\",\"type\":\"string\",\"name\":\"mob\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"移动端链接\"},{\"id\":\"_IC_AIGC_DETAIL_URL.pc\",\"type\":\"string\",\"name\":\"pc\",\"private\":false,\"editorVarType\":\"variables\",\"description\":\"桌面端链接\"}]},{\"id\":\"flowStatus\",\"type\":\"string\",\"name\":\"flowStatus\",\"description\":\"AI卡片状态,包含 pending(1)、writing(2)、done(3)、doing(4)、failed(5)\",\"private\":false,\"editorVarType\":\"variables\",\"disabled\":true,\"visible\":false}],\"formList\":[],\"customContextList\":[],\"expList\":[],\"localList\":[],\"hsfList\":[],\"lwpList\":[],\"pageData\":{},\"extension\":{\"extendType\":\"AI\",\"aiStatusList\":[1,2,3],\"fileTypeList\":[{\"fileName\":\"event_chain\",\"fileSource\":\"{\\\"node_ocmnee6af5i_input\\\":{\\\"main_success_left_callback\\\":{\\\"type\\\":\\\"dtSendOutData\\\",\\\"params\\\":{\\\"actionType\\\":\\\"0\\\",\\\"cardInstanceId\\\":\\\"@data{data.cardInstanceId}\\\",\\\"actionId\\\":\\\"@toStr{'guideInput'}\\\",\\\"dataKey\\\":\\\"main_success_left_callback\\\",\\\"actionData\\\":{\\\"context\\\":\\\"@dtMapAppend{@data{data.renderContext},'platform','im','platformBizId',@data{data.renderContext.mid}}\\\",\\\"cardPrivateData\\\":{\\\"params\\\":\\\"@dtMapAppend{@dtMapAppend{null},@toStr{'guideInput'},@dtGetEventChainData{'main_success.value'}}\\\",\\\"actionIds\\\":[\\\"@toStr{'guideInput'}\\\"]}},\\\"requestStatusKey\\\":\\\"@concat{@toStr{'guideInput'},'_request_status'}\\\"},\\\"callback\\\":{}},\\\"main\\\":{\\\"type\\\":\\\"dtOpenLink\\\",\\\"params\\\":{},\\\"next\\\":\\\"@triple{@equal{'normal','normal'},'$(main_success)',''}\\\"},\\\"main_success\\\":{\\\"type\\\":\\\"dtCallAPI\\\",\\\"params\\\":{\\\"type\\\":\\\"callJSAPI\\\",\\\"params\\\":{\\\"apiName\\\":\\\"device.notification.prompt\\\",\\\"params\\\":{\\\"message\\\":\\\"@toStr{'请输入内容'}\\\",\\\"title\\\":\\\"@toStr{'添加指引'}\\\",\\\"buttonLabels\\\":\\\"@triple{@equal{@data{env},'pc'},@dtArrayAppend{null,@dti18NAdapter{'提交','提交','Submit'},@dti18NAdapter{'取消','取消','Cancel'}},@dtArrayAppend{null,@dti18NAdapter{'取消','取消','Cancel'},@dti18NAdapter{'提交','提交','Submit'}}}\\\",\\\"defaultText\\\":\\\"@toStr{''}\\\"}},\\\"dataKey\\\":\\\"main_success\\\"},\\\"dataKey\\\":\\\"main_success\\\",\\\"callback\\\":{\\\"success\\\":\\\"@triple{@equal{@toStr{@dtGetEventChainData{'main_success.buttonIndex'}},@triple{@equal{@data{env},'pc'},0,1}},'$(main_success_left_callback)',''}\\\"}}},\\\"ai_card_share_node_ocmncu094ga\\\":{\\\"main\\\":{\\\"type\\\":\\\"dtSendOutData\\\",\\\"params\\\":{\\\"actionType\\\":\\\"0\\\",\\\"cardInstanceId\\\":\\\"@data{data.cardInstanceId}\\\",\\\"actionId\\\":\\\"sys_action_ai_card_share\\\",\\\"requestStatusKey\\\":\\\"sys_action_ai_card_share_loading_status\\\",\\\"actionData\\\":{\\\"context\\\":\\\"@dtMapAppend{@data{data.renderContext},'platform','im','platformBizId',@data{data.renderContext.mid}}\\\",\\\"cardPrivateData\\\":{\\\"params\\\":{\\\"command\\\":\\\"shareAIConv\\\",\\\"params\\\":\\\"@data{data.cardData._IC_SHARE_CARD.params}\\\"},\\\"actionIds\\\":[\\\"sys_action_ai_card_share\\\"]}},\\\"dataKey\\\":\\\"main\\\"},\\\"dataKey\\\":\\\"main\\\",\\\"callback\\\":{\\\"success\\\":\\\"@triple{@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.isSuccess},'$(main_success_left_callback)','$(main_success_right_callback)'}\\\",\\\"failure\\\":\\\"$(main_success_right_callback)\\\"}},\\\"main_success_left_callback\\\":{\\\"type\\\":\\\"dtOpenLink\\\",\\\"params\\\":{\\\"url\\\":\\\"@triple{@and{@equal{@data{env},'android'},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.androidUrl}},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.androidUrl},@triple{@and{@equal{@data{env},'ios'},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.iosUrl}},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.iosUrl},@triple{@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.pcUrl},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.pcUrl},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.result.url}}}}\\\"}},\\\"main_success_right_callback\\\":{\\\"type\\\":\\\"dtToast\\\",\\\"params\\\":{\\\"type\\\":2,\\\"text\\\":\\\"@triple{@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.errorMsg},@dtGetEventChainData{main.cardPrivateData._IC_SHARE_CARD_RESPONSE.errorMsg},@dti18NAdapter{'操作失败,请稍候重试','操作失敗,請稍候重試','Operation failed, please try again later','Operation failed, please try again later','Operation failed, please try again later','Operation failed, please try again later','Operation failed, please try again later'}}\\\"}}}}\",\"fileType\":\"json\"}]}}","widgetInfo":"\n \n \n\n \n \n\n \n \n \n \n \n \n\n \n\n \n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n\n \n \n\n \n\n \n \n\n \n \n \n \n \n \n \n\n \n\n \n \n \n\n \n \n\n \n\n \n \n \n\n \n\n \n \n \n\n \n\n \n \n \n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n \n \n \n\n \n \n\n \n\n \n\n \n \n \n \n \n \n \n\n \n \n\n \n \n\n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n\n \n \n\n \n\n \n\n \n\n \n\n \n \n \n\n \n\n \n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n \n\n \n \n\n \n\n \n \n\n \n \n \n \n \n \n \n\n \n \n\n \n\n \n \n \n\n \n\n \n \n \n\n \n\n \n \n \n \n\n \n\n \n\n \n \n \n\n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n\n \n\n \n\n \n \n \n \n\n \n\n \n\n \n\n \n\n \n \n \n \n \n \n \n \n\n \n \n \n\n \n\n \n\n \n \n \n\n \n \n\n \n \n \n\n \n\n \n\n \n \n\n \n\n \n\n \n \n \n \n\n \n \n\n \n\n \n\n \n \n \n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n \n\n\n","type":"im","mode":"card"} \ No newline at end of file diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index a6f62a04..53538242 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -384,7 +384,7 @@

    2. 已确认的决策清单

    (1) 变量名实际是 approveId(camelCase),不是 v3.6 spec 假设的 approval_id(snake_case); (2) 回传参数名也是 approveId(v3.6 spec 假设 id); (3) schema id 已从 v3.5 的 876de.schema 变到 05061.schema,用户需重新 export 更新 docs/assets/card-template-v3.json。 - spec 全局替换 approval_id → approveId、params.id → params.approveId;§10 阶段 0 描述同步用户实际进展(schema 已配 ✓,待 export + 发布);按钮 params 描述细化为"参数名=approveId, 类型=变量, 值=approveId";隐式假设 allow-once / allow-always 按钮也配同样的 params2(用户截图仅展示 deny 按钮,应在 PR-2 前置确认其它两个按钮也配齐) + spec 全局替换 approval_id → approveId、params.id → params.approveId;§10 阶段 0 描述同步用户实际进展(schema 已配 ✓,已发布 ✓ templateId=58f73932-...05061.schema,已重新 export JSON 覆盖 docs/assets/card-template-v3.json ✓,三按钮均配齐 approveId 回传参数 ✓);按钮 params 描述细化为"参数名=approveId, 类型=变量, 值=approveId"
  • v3.6(2026-05-19 第六轮 review,含 1 个结构性修订 + 4 处实施 blocker): (2) D24 重构:v3.5 用 card-run-registry 内存反查 approvalId 的方案受重启 / 多 worker / TTL 影响(registry 是进程内 Map,src/card/card-run-registry.ts:1-9 明确多进程约束)。v3.6 改为 v3 模板再加 approveId 变量 + 三按钮 params 加 id 绑定到该变量;callback 自带 approvalId 为主链路,registry pendingApprovalId 降级为兜底(3) §10 阶段 0 措辞收紧:v3 模板"必须替换默认 templateId"(不再写"替代还是并存"),v3 是 v2 字段超集向后兼容,并存会让边界不清; From f600fe570a97d0b186fed7cedf82ae7582664e54 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 00:49:23 +0800 Subject: [PATCH 15/44] =?UTF-8?q?docs(spec):=20v3.8=20=E4=BF=AE=E8=AE=A2?= =?UTF-8?q?=20=E2=80=94=205=20=E5=A4=84=E5=AE=9E=E6=96=BD=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E6=80=A7=20fix=20+=201=20=E5=A4=84=20limitation=20?= =?UTF-8?q?=E5=A4=87=E6=B3=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 第八轮 review 反馈,全部聚焦实施层一致性: (1) D24 主链路全局收敛:清理 §1.2 / D15 / §6.3 / §3.1 等多处 v3.5 老语义 残留 "approvalId 不在 payload 必须 outTrackId 反查",统一为 "params.approveId 主链路 + registry pendingApprovalId fallback"。 params.id → params.approveId(v3.7 已部分修,v3.8 收尾)。 (2) v1 limitation 备注(不改实现):上游 ExecApprovalRequest / PluginApprovalRequest 的 allowedDecisions 字段允许 per-request 限制可选 decision(ask=always 时 exec 只允许 allow-once+deny)。v3 模板固定 3 按钮全显示,点击不支持的 decision 会被上游拒绝并刷"已处理或已过期"。 §11.1 加 limitation 条目,指明未来若要支持需 v3 模板加 3 个 boolean 变量分别控制按钮显示 + channel 端按 allowedDecisions 设变量。 (3) markdown 路径强制 forceMarkdown:true:核实 src/send-service.ts:371-393, messageType=card 配置下 sendProactiveTextOrMarkdown 默认走 sendProactiveCardText 把 markdown 包成 AI Card。D10 / §6.2 场景 B / §6.8 intercept unauthorized 提示等所有调用点改成显式传 { forceMarkdown:true, accountId, log }。 (4) sub-adapter 数量描述统一:上游类型实际 3 必需(availability/ presentation/transport)+ 2 可选(interactions?/observe?)。v1 实现 4 个 = 3 必需 + observe,省略 interactions。§5 标题从 "5 个 Sub-Adapter 详解" 改为 "Sub-Adapter 详解(v1 实现 4 个)",正文同步。 (5) entry.state → record.card?.state:核实 src/card/card-run-registry.ts:13 + src/types.ts:689——CardRunRecord 顶层无 state 字段,state 在 record.card?.state。spec 多处 entry.state ∈ {PROCESSING, INPUTING} 改 成 record.card?.state ∈ {PROCESSING, INPUTING};建议加 helper isActiveCardRun(record) 封装。 (6) /approve intercept 插入点精确化:spec 之前 "command dispatch 之后、 reply 派发之前" 模糊。v3.8 明确为 "access control + content extract + senderId 确定之后;早于本地 command dispatch (src/inbound-handler.ts:874) + 早于 sub-agent targeted command 递归 + 早于 acquireSessionLock (src/inbound-handler.ts:2053)"。否则 @agent /approve 被 sub-agent 吞、 或 session lock 死锁。 Co-Authored-By: Claude Opus 4.7 (1M context) --- ...6-05-18-gap-01-approval-native-design.html | 63 +++++++++++++------ 1 file changed, 44 insertions(+), 19 deletions(-) diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index 53538242..51f85de2 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -105,7 +105,8 @@

    回调实测形态(用户配 schema 后的预期)

    "spaceType": "im" 或 "group", ... } -// 注意 approval id 不在 payload —— 必须通过 outTrackId 反查(D24) +// v3.6+ 主链路:approvalId 已通过 approveId 变量绑定到按钮 params, +// callback 自带;D24 registry 反查仅作 fallback(详见解码段)

    解码(callback 入口)

    // 第一步:扩展后的 analyzeCardCallback 把 cardPrivateData 整体放进 analysis
    @@ -227,8 +228,8 @@ 

    2. 已确认的决策清单

    D10 渲染策略(v3.3 重写) 不读 messageType 配置——按 card-run-registry 实际状态 分两路由: -
    card 路径:sessionKey 命中 registry 且 entry.state ∈ {PROCESSING, INPUTING} → 在 entry.outTrackId 上 PUT 注入 approval 按钮 -
    markdown 路径:其它所有情况(无 entry / entry 已 FINISHED/STOPPED/FAILED / entry 被 TTL sweep / messageType=markdown 时本来就没 entry / card 创建失败降级)→ sendProactiveTextOrMarkdown 发独立消息 + /approve 命令模板 +
    card 路径:sessionKey 命中 registry 且 record.card?.state ∈ {PROCESSING, INPUTING} → 在 record.outTrackId 上 PUT 注入 approval 按钮(实施细节:CardRunRecord.cardAICardInstance | undefined,state 在 record.card.state,不是 record 顶层 — src/card/card-run-registry.ts:13 + src/types.ts:689) +
    markdown 路径:其它所有情况(无 entry / entry 已 FINISHED/STOPPED/FAILED / entry 被 TTL sweep / messageType=markdown 时本来就没 entry / card 创建失败降级)→ sendProactiveTextOrMarkdown(config, target, text, { forceMarkdown: true, accountId, log }) 发独立消息 + /approve 命令模板。必须传 forceMarkdown:true——否则 messageType=card 配置下函数会走 sendProactiveCardText 把 markdown 包成另一张 AI Card(参 src/send-service.ts:371-393),等于完全失去 markdown 路径意义
    messageType 配置完全不参与决策——单一事实源是 registry 实际状态 v3.3 用户拍板:runtime 实际降级要 cover @@ -269,7 +270,7 @@

    2. 已确认的决策清单



    关键修正(v3.5 实测):DingTalk 仅在 actionId 重名时才追加 button index 后缀(消歧用)。独立命名时 callback 中 actionId 原样回传,无 index 后缀。之前 v3.2 看到的 "approve0/1/2" 是测试时 3 按钮都叫 "approve" 触发的消歧行为。
    -
    approval id 不在按钮 payload——只能通过 callback 的 outTrackId(agent reply card 的 id)反查(详见 D24) +
    approvalId 通过 approveId 变量带回(v3.6 D24 主链路):v3 模板的三按钮 params 都绑定到 approveId 变量,channel 端 patch 时 PUT 设值,callback 中 cardPrivateData.params.approveId 自动带回。D24 的 registry pendingApprovalId 仅作 fallback(应对老卡片 / 平台异常) 对齐用户实测 + OpenClaw 命名习惯 @@ -326,7 +327,7 @@

    2. 已确认的决策清单

    agent-card-coalesce(v3.3 新增核心) card 路径下,approval 按钮挂在 原 agent reply card 而非新建独立卡片:
    (1) transport.prepareTarget 内部调 approval-card-locator.findActiveAgentCard(request),按 request.sessionKeycard-run-registry; -
    (2) 命中且 entry.state ∈ {PROCESSING, INPUTING} → preparedTarget.route="card" 携带 entry.outTrackId;否则 route="markdown"; +
    (2) 命中且 record.card?.state ∈ {PROCESSING, INPUTING} → preparedTarget.route="card" 携带 record.outTrackId;否则 route="markdown"
    (3) transport.deliverPending 按 route 分支:card 走 PUT updateCardVariables(outTrackId, { show_approve_btns:"true", hasAction:"false", approveId:"<id>" })(v3.6 修订:按钮在模板内置;D24 v3.6 把 approvalId 通过 approveId 变量带到 callback);markdown 走 sendProactiveTextOrMarkdown
    (4) entry 记下 mode + outTrackId(如有),core 把 entry 带回 updateEntry 时按 mode 分支处理终态
    关键不变量:approval 只对 cardParamMap 做字段级 patch,不触碰 agent card 的 lifecycle 状态机(PROCESSING/INPUTING/FINISHED 仍由 agent reply 流控制) @@ -352,7 +353,7 @@

    2. 已确认的决策清单


    1) v3 模板新增字符串变量 approveId(PR-2 前置;用户回去给 schema 加,与 show_approve_btns 同级);
    2) 三个按钮各加一个回传参数:参数名="approveId", 参数类型=变量, 参数值=approveId(绑定到上一步新增的同名变量);
    3) channel 端 applyPendingPatch 时 PUT { show_approve_btns:"true", hasAction:"false", approveId:"<id>" }; -
    4) callback payload 自带 params:{ action:"allow-once", id:"<id>" },approvalId 不再依赖反查; +
    4) callback payload 自带 params:{ action:"allow-once", approveId:"<id>" },approvalId 主链路从 params.approveId 直接取;
    5) applyResolvedPatch/applyExpiredPatch 时 PUT { show_approve_btns:"false", approveId:"" } 清空。

    fallback:card-run-registry.pendingApprovalId 字段——保留为兜底,应对老版本卡片 / 平台行为异常等:当 callback 没带 params.approveId 或带的是空时,反查 resolveCardRun(outTrackId).pendingApprovalId。该字段仍由 patcher 在 pending/resolved 时 set/clear。 @@ -380,6 +381,13 @@

    2. 已确认的决策清单

    (6) §10 PR-1 措辞改"resolve 通道生效,approval id 可见性依赖外部界面,完整 UX 在 PR-2"; (7) §6.7 删除 store.register 残留; (8) §6.3 / §6.6 / §8 / §9 等所有 already-resolved 文案改"ℹ️ 已处理或已过期"并明确 catch 后 return 避免覆盖;§6.8 alias 范围显式列清;§11.2 风险表新增 kind 派发边界条目
  • +
  • v3.8(2026-05-19 第八轮 review):5 处实施一致性 fix + 1 处 limitation 备注—— + (1) D24 主链路全局收敛:清理 §1.2 / D15 / D24 / §6.3 等多处"approvalId 不在 payload,必须 outTrackId 反查"等 v3.5 老语义残留,统一为"params.approveId 主链路 + registry pendingApprovalId fallback"; + (2) 备注 v1 limitation:上游 ExecApprovalRequest / PluginApprovalRequest 的 allowedDecisions 允许 per-request 限制可选 decision(例 ask=always 时 exec 只允许 allow-once+deny),v3 模板固定 3 按钮全显示,点击不支持的 decision 会被上游拒绝、卡片刷"ℹ️ 已处理或已过期"——v1 不实现按 allowedDecisions 动态隐藏按钮,§11.1 加 limitation 条目; + (3) markdown 路径强制 forceMarkdown: true:核实 src/send-service.ts:371-393——messageType=card 配置下 sendProactiveTextOrMarkdown 默认走 sendProactiveCardText 把 markdown 包成 AI Card。v3.8 在 D10 / §6.2 / §6.3 多处明确传 forceMarkdown: true,避免 markdown 路径被卡片化; + (4) sub-adapter 数量描述统一:上游类型实际 3 必需(availability/presentation/transport)+ 2 可选(interactions?/observe?)。v1 实现 4 个(3 必需 + observe)。§5 标题从 "5 个 Sub-Adapter 详解" 改为 "Sub-Adapter 详解(v1 实现 4 个)",正文同步; + (5) entry.state → record.card?.state:核实 src/card/card-run-registry.ts:13 + src/types.ts:689——CardRunRecord 顶层无 state 字段,state 在 record.card?.state。spec 多处 entry.state 改成 record.card?.state;建议 card-run-registry 加一个 isActiveCardRun(record) helper 封装; + (6) /approve intercept 插入点精确化:spec 之前写"command dispatch 之后、reply 派发之前"模糊,v3.8 明确为"access control + content extract + senderId 确定之后;早于本地 command dispatch(src/inbound-handler.ts:874)+ 早于 sub-agent targeted command 递归 + 早于 acquireSessionLock(src/inbound-handler.ts:2053)"——否则 @agent /approve 被 sub-agent 吞 / session lock 死锁
  • v3.7(2026-05-19 用户实配 D24 确认):用户回去给 v3 schema 实际加 approveId 变量 + 在 deny 按钮配了第 2 个回传参数(参数名=approveId, 参数类型=变量, 参数值=approveId)。命名修正: (1) 变量名实际是 approveId(camelCase),不是 v3.6 spec 假设的 approval_id(snake_case); (2) 回传参数名也是 approveId(v3.6 spec 假设 id); @@ -403,7 +411,7 @@

    2. 已确认的决策清单

    (1) 模板新增 approve_btns(按钮组)+ show_approve_btns(Boolean)两个变量; (2) 三个按钮 actionId 独立命名 allow-once / allow-always / deny无 index 后缀(之前测试 "approve0/1/2" 是因为 3 按钮都叫 approve 重名,DingTalk 才加 index 消歧;唯一名时原样回); (3) 按钮在 template 内置("按钮组来源: 指定"),channel 不再构造 CardBtn[],只 toggle show_approve_btns 可见性; - (4) 回传参数仅 params.action(值 = decision),不带 approval id——必须通过 outTrackId 反查 approvalId; + (4) v3.5 阶段回传参数曾仅有 params.action,但 v3.6/v3.7 已加 approveId 变量绑定(D24 主链路);早期 v3.5 假设的"必须 outTrackId 反查"已被 v3.6 重构掉,registry 反查只剩 fallback; (5) 新增 D24:approvalId 反查机制——给 CardRunRecordpendingApprovalId?: string 字段 + markCardRunPendingApproval / clearCardRunPendingApproval API。
    影响:D15 改、新增 D24;§1.2 / §3.2 / §3.3 / §5.2 / §5.3 / §6.3 / §7.1 / §7.2 全部对齐;模块层面 approval-card-patcher 大幅简化(不构造按钮数组);approval 模块不再需要自定义 CardBtn 类型
  • v3.4(2026-05-18 第五轮 review):7 处修订—— @@ -533,7 +541,7 @@

    3.2 模块单一职责表

    approval-card-locator.ts
    ★ v3.3 新增(D22 落地核心) - 导出 findActiveAgentCard({ cfg, accountId, sessionKey }):按 sessionKey 查 card-run-registry,仅在 entry 存在且 entry.card.state ∈ {PROCESSING, INPUTING} 时返回 { outTrackId, sessionKey };否则返回 null(caller 走 markdown 路径)。 + 导出 findActiveAgentCard({ cfg, accountId, sessionKey }):按 sessionKey 查 card-run-registry,仅在 record 存在且 record.card?.state ∈ {PROCESSING, INPUTING} 时返回 { outTrackId, sessionKey };否则返回 null(caller 走 markdown 路径)。注意:state 在 record.card.statesrc/card/card-run-registry.ts:13 + src/types.ts:689),不是 record 顶层;建议在 card-run-registry 加一个 helper isActiveCardRun(record: CardRunRecord): boolean 把这个判断封装起来,approval-card-locator 与未来其它消费方共用。
    v3.4 关键依赖:card-run-registry 必须新增 sessionKey 查询 API——当前源码只导出 resolveCardRun(outTrackId) / resolveCardRunByConversation / resolveCardRunByOwnersrc/card/card-run-registry.ts:91-145),无 by-sessionKey 查询。需在 src/card/card-run-registry.ts 新增 resolveActiveCardRunBySession(accountId: string, sessionKey: string): CardRunRecord | null——遍历 records Map,按 accountId + sessionKey 精确匹配且 state ∈ active 集合。Record 已有 sessionKey 字段(card-run-registry.ts:16),实现仅是新增一个 export。不要让 locator 依赖 conversation contains 模糊匹配或私有 Map 访问 card-run-registry(既有模块;需新增 export) ~60(+ card-run-registry 内 ~20 行新 API) @@ -647,7 +655,7 @@

    3.3 与现有代码的接触面

    src/card/card-run-registry.ts
    (v3.4 + v3.5 新增改动面) - v3.4 改动:新增 resolveActiveCardRunBySession(accountId, sessionKey): CardRunRecord | null——遍历 records Map 按 accountId + sessionKey 精确匹配且 state ∈ {PROCESSING, INPUTING} 过滤。 + v3.4 改动:新增 resolveActiveCardRunBySession(accountId, sessionKey): CardRunRecord | null——遍历 records Map 按 accountId + sessionKey 精确匹配且 record.card?.state ∈ {PROCESSING, INPUTING} 过滤(state 在 record.card,不是 record 顶层)。同时新增 helper isActiveCardRun(record: CardRunRecord): boolean 封装 state 判断逻辑,供 approval-card-locator 与未来其它 active card 消费方共用。
    v3.5 新增(D24 落地)
    CardRunRecord 接口新增 pendingApprovalId?: string 字段;
    • 新增 markCardRunPendingApproval(outTrackId: string, approvalId: string): void; @@ -668,7 +676,9 @@

    3.3 与现有代码的接触面

    src/inbound-handler.ts
    (D2:v2 新增改动面) - 在 handleDingTalkMessage 入口(command dispatch 之后、reply 派发之前)插入 /approve 命令早期 intercept 分支,详见 §6.8 + 在 handleDingTalkMessage 早期插入 /approve 命令 intercept 分支。 +
    v3.8 精确定位:插入点必须在 access control + content extract + senderId 确定之后(需要这些上下文做 approver 权限校验),但必须 早于本地命令 dispatch(src/inbound-handler.ts:874)+ 早于 sub-agent targeted command 递归——否则 @agent /approve abc once 会被 sub-agent 路由先吃掉。 +
    更必须 早于 acquireSessionLock(src/inbound-handler.ts:2053——后者一旦持有锁,resolveApprovalOverGateway 内部的 plugin approval waitDecision 会等同一把锁就死锁。详见 §6.8 中(需谨慎确保 intercept 之前的 dedup/self-filter/content-extract 行为不变) @@ -757,8 +767,8 @@

    4.2 配置 schema(D7 落地)

    -

    5. 5 个 Sub-Adapter 详解

    -

    上游 ChannelApprovalNativeRuntimeAdapteropenclaw/src/infra/approval-handler-runtime-types.ts:216-235)定义了 5 个子接口,approval-runtime 按下面顺序调用它们完成"投递 / 更新 / 终态"。本节给每个子接口的 DingTalk 实现锚到具体函数与代码骨架。

    +

    5. Sub-Adapter 详解(v1 实现 4 个)

    +

    上游 ChannelApprovalNativeRuntimeAdapteropenclaw/src/infra/approval-handler-runtime-types.ts:216-235)类型定义为 3 个必需(availability / presentation / transport)+ 2 个可选(interactions? / observe?)。v1 实现 4 个:3 必需 + observe(用于投递日志);interactions 推迟到 v2(仅在 DM 投递启用后才有实际收益)。本节按上游调用顺序展开。

    5.1 availability

    @@ -827,7 +837,7 @@

    5.3 transport


    (4) 模糊失败(请求超时但可能已成功)→ WARN log + return null(不重发避免双消息)。

    route="markdown"
    (1) 调 approval-markdown-render 构造 markdown 文本(含 /approve 命令模板); -
    (2) sendProactiveTextOrMarkdown(config, target.to, text, ...) 发独立消息; +
    (2) sendProactiveTextOrMarkdown(config, target.to, text, { forceMarkdown:true, accountId, log }) 发独立消息(forceMarkdown:true 必传,否则 messageType=card 下函数会回退到 sendProactiveCardText 投递 AI Card——失去 markdown 路径本意,src/send-service.ts:371-393);
    (3) 成功 → 返回 entry = { approvalId, accountId, mode: "markdown" }
    (4) 失败 → WARN log + return null(markdown 已是最低保障路径,再降无意义)。

    两路径通用约束:在 catch 内调 observe.onDeliveryError——runtime 契约不允许 transport 自触发该钩子 @@ -976,7 +986,11 @@

    场景 B:用户在 messageType=markdown 模式触发 exec(markdown 路 批准(仅一次):`/approve abc123 allow-once` 批准(总是):`/approve abc123 allow-always` 拒绝:`/approve abc123 deny`" - └─ sendProactiveTextOrMarkdown(config, "group:cid_xxx", markdownText, opts) + └─ sendProactiveTextOrMarkdown(config, "group:cid_xxx", markdownText, { + forceMarkdown: true, // ← 必传,否则 messageType=card 下会被 + // sendProactiveCardText 接管发成另一张 + // AI Card(src/send-service.ts:371-393) + accountId: "default", log }) └─ entry = { approvalId:"abc123", accountId:"default", mode:"markdown" } ↑ 注意无 outTrackId @@ -1017,7 +1031,7 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    payload.content/value (内嵌 JSON, v3.5 实配 schema): { cardPrivateData: { actionIds: ["allow-once"], ← 唯一命名无 index 后缀(D15 v3.5) - params: { action: "allow-once", id: "abc123" } ← D24 v3.6 主链路 + params: { action: "allow-once", approveId: "abc123" } ← D24 主链路 } } payload.userId = "staffA" ← clicker staffId payload.spaceType = "im" 或 "group" @@ -1032,7 +1046,7 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ analysis.outTrackId = "ai_card_xxx" │ analysis.cardPrivateData = { ← D16 新增字段 │ actionIds: ["allow-once"], - │ params: { action: "allow-once", id: "abc123" } ← D24 v3.6 + │ params: { action: "allow-once", approveId: "abc123" } ← D24 │ } │ ├─ 【新增分支】tryHandleApprovalCallback(analysis, ...) @@ -1077,7 +1091,7 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ dingtalkConfig, │ `user:${analysis.userId}`, │ "⛔ 你不在 approver 名单,无权批准此请求", - │ { accountId, log }) + │ { forceMarkdown: true, accountId, log }) // 强制 markdown │ return { handled: true, reason: "unauthorized" } │ │ case "already-resolved": @@ -1284,8 +1298,14 @@

    解决方案:在 handleDingTalkMessage 入口最早处直接 // → session routing → command dispatch → reply 派发 // // v2 修订:在 "command dispatch" 之后、"reply 派发"之前,加入 /approve early intercept。 -// (放在 command dispatch 之后是为了让 dedup / self-filter / content extract -// 等基础设施先 run;放在 reply 派发之前是关键——避免 session lock 死锁。) +// 插入点(v3.8 精确): +// ✓ 必须在:dedup / self-filter / access control / content extract / +// senderId 确定 之后(需要 senderId 做 approver 权限校验) +// ✓ 必须早于:本地 command dispatch (src/inbound-handler.ts:874) +// + sub-agent targeted command 递归(@agent /approve 否则被吞) +// ✓ 必须早于:acquireSessionLock (src/inbound-handler.ts:2053) +// 否则 resolveApprovalOverGateway 内部 plugin waitDecision +// 会等同一把锁 → 死锁 // ---- Early /approve bypass:collapsed into approval-resolver ---- // 关键:parse 与 resolve 都走专用模块(D20),不在 inbound-handler 内堆 200 行 @@ -1879,6 +1899,11 @@

    11.1 明确不在本 spec 范围

  • 统一交互状态模型(gap 文档 #01 sub-6):v1 仅为 approval 实现 pending/resolved/expired 状态机;将之普适到其它交互需要更多用例验证,留待 #12 message tool action surface
  • 主动 rebind on restart(D12 B 选项):留待 v2
  • interactions sub-adapter:v1 不实现,仅在 DM 启用后才有实际收益
  • +
  • respect 上游 allowedDecisions 字段(v3.8 limitation 备注,明确不做): +
    上游 ExecApprovalRequest.allowedDecisionsopenclaw/src/infra/exec-approvals.ts:1241)与 PluginApprovalRequest.allowedDecisionsopenclaw/src/infra/plugin-approvals.ts:54)允许 per-request 限制可选 decision。例如 ask="always" 时 exec 只允许 allow-once + deny(不允许 allow-always)。 +
    v1 模板按钮组 固定 3 按钮全部显示,未读取 request 的 allowedDecisions 来动态隐藏不支持的选项。 +
    用户行为:若点击 approval 不支持的 decision(如 ask=always 时点 allow-always),上游 resolveApprovalOverGateway 会拒绝该 decision,channel 端 callback handler 会进 resolve-failed 分支 → 卡片刷"ℹ️ 已处理或已过期"。用户体感 = "点了一下卡片刷掉但没成功"。 +
    已知降级。修复需要:(a) v3 模板新增 3 个 boolean 变量(如 show_allow_once_btn / show_allow_always_btn / show_deny_btn)+ 按钮显示控制条件细化;(b) channel 端 patcher 按 request.allowedDecisions 设变量值。v1 不实现,留待用户反馈后再加
  • 11.2 已知风险

    From 44afa9a663865158202603a546f23e89b3d9cb55 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:00:18 +0800 Subject: [PATCH 16/44] chore(deps): bump openclaw approval SDK baseline Sync the OpenClaw peer and host manifest baseline to >=2026.4.7, align zod with the installed SDK package, and update message tool discovery to the current presentation capability contract. BREAKING CHANGE: openclaw peer baseline is now 2026.4.7+. --- package.json | 10 +- pnpm-lock.yaml | 2963 +++++++-------------- src/messaging/channel-actions.ts | 2 +- tests/unit/channel-actions-module.test.ts | 2 +- tests/unit/message-actions.test.ts | 2 +- tests/unit/plugin-manifest.test.ts | 8 +- tests/unit/sdk-import-structure.test.ts | 4 +- 7 files changed, 989 insertions(+), 2002 deletions(-) diff --git a/package.json b/package.json index a13619f7..3b29106e 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "form-data": "^4.0.0", "mammoth": "^1.12.0", "pdf-parse": "^2.4.5", - "zod": "^4.3.6" + "zod": "^4.4.3" }, "devDependencies": { "@types/node": "^25.2.0", @@ -82,7 +82,7 @@ "vitest": "^3.2.4" }, "peerDependencies": { - "openclaw": ">=2026.3.28" + "openclaw": ">=2026.4.7" }, "peerDependenciesMeta": { "openclaw": { @@ -91,10 +91,10 @@ }, "openclaw": { "compat": { - "pluginApi": ">=2026.3.28" + "pluginApi": ">=2026.4.7" }, "build": { - "openclawVersion": "2026.3.28" + "openclawVersion": "2026.4.7" }, "extensions": [ "./index.ts" @@ -120,7 +120,7 @@ ] }, "install": { - "minHostVersion": ">=2026.3.28", + "minHostVersion": ">=2026.4.7", "npmSpec": "@soimy/dingtalk", "localPath": ".", "defaultChoice": "npm" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d695341a..d5a29a5c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -21,21 +21,21 @@ importers: specifier: ^1.12.0 version: 1.12.0 openclaw: - specifier: '>=2026.3.28' - version: 2026.3.28(@napi-rs/canvas@0.1.97) + specifier: '>=2026.4.7' + version: 2026.5.18 pdf-parse: specifier: ^2.4.5 version: 2.4.5 zod: - specifier: ^4.3.6 - version: 4.3.6 + specifier: ^4.4.3 + version: 4.4.3 devDependencies: '@types/node': specifier: ^25.2.0 version: 25.2.0 '@vitest/coverage-v8': specifier: ^3.2.4 - version: 3.2.4(vitest@3.2.4(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3)) + version: 3.2.4(vitest@3.2.4(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0)) esbuild: specifier: ^0.27.3 version: 0.27.3 @@ -53,15 +53,15 @@ importers: version: 5.9.3 vitepress: specifier: 1.6.4 - version: 1.6.4(@algolia/client-search@5.50.0)(@types/node@25.2.0)(axios@1.13.6)(jwt-decode@4.0.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3) + version: 1.6.4(@algolia/client-search@5.50.0)(@types/node@25.2.0)(axios@1.13.6)(jwt-decode@4.0.0)(postcss@8.5.8)(qrcode@1.5.4)(search-insights@2.17.3)(typescript@5.9.3) vitest: specifier: ^3.2.4 - version: 3.2.4(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3) + version: 3.2.4(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0) packages: - '@agentclientprotocol/sdk@0.17.0': - resolution: {integrity: sha512-inBMYAEd9t4E+ULZK2os9kmLG5jbPvMLbPvY71XDDem1YteW/uDwkahg6OwsGR3tvvgVhYbRJ9mJCp2VXqG4xQ==} + '@agentclientprotocol/sdk@0.21.1': + resolution: {integrity: sha512-ZTLH+o9QxcZDLX/9ww+W7C2iExnXFM+vD/uGFVSlR61Kzj9FaxUqBC6Rv/kwgA7qVWYUEI9c5ZNqCuO9PM4rKg==} peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -145,8 +145,8 @@ packages: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} - '@anthropic-ai/sdk@0.73.0': - resolution: {integrity: sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==} + '@anthropic-ai/sdk@0.91.1': + resolution: {integrity: sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==} hasBin: true peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -154,9 +154,6 @@ packages: zod: optional: true - '@anthropic-ai/vertex-sdk@0.14.4': - resolution: {integrity: sha512-BZUPRWghZxfSFtAxU563wH+jfWBPoedAwsVxG35FhmNsjeV8tyfN+lFriWhCpcZApxA4NdT6Soov+PzfnxxD5g==} - '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -174,128 +171,80 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} - '@aws-sdk/client-bedrock-runtime@3.1014.0': - resolution: {integrity: sha512-K0TmX1D6dIh4J2QtqUuEXxbyMmtHD+kwHvUg1JwDXaLXC7zJJlR0p1692YBh/eze9tHbuKqP/VWzUy6XX9IPGw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/client-bedrock@3.1020.0': - resolution: {integrity: sha512-OIM38upZjWsi62070cOm2nZAJsIeZC26KhOFDt8T6gmzbfcoz7XgkJ6eK9/JFfFagoFykUvXX0nfbcqtryWY0A==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/core@3.973.26': - resolution: {integrity: sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-env@3.972.24': - resolution: {integrity: sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-http@3.972.26': - resolution: {integrity: sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/credential-provider-ini@3.972.28': - resolution: {integrity: sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==} + '@aws-sdk/client-bedrock-runtime@3.1049.0': + resolution: {integrity: sha512-YM8b2baoRY8ul47b4amQW2VlUthLmM8DnqdlGO20LJmmmRpjnT91SaQJai3OMehA6uE0Gig88VyDCT1vEACSww==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-login@3.972.28': - resolution: {integrity: sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==} + '@aws-sdk/core@3.974.12': + resolution: {integrity: sha512-qrqgioqYFjwR6LatVNS1L2Vk++EwRIxqSQXPKNv5Ofux2D8UNgqMQ1znnMyEImXquVPTtbf71fc128pvmU6y9A==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-node@3.972.29': - resolution: {integrity: sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==} + '@aws-sdk/credential-provider-env@3.972.38': + resolution: {integrity: sha512-m3WjZEgPtioMhPmwqUt+DhlTJ2i9ufR6DhfkyXojb9puEvfR+ur2U5shavu5/Cc9WHHsDCvALi6UFHgcqjhQ5w==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-process@3.972.24': - resolution: {integrity: sha512-Q2k/XLrFXhEztPHqj4SLCNID3hEPdlhh1CDLBpNnM+1L8fq7P+yON9/9M1IGN/dA5W45v44ylERfXtDAlmMNmw==} + '@aws-sdk/credential-provider-http@3.972.40': + resolution: {integrity: sha512-D78L/m2Dr6cJnnSvWoAudPhQmCwmJ7j6APXsPYmFpPaKfQTfCSu0rdm8j14Np+VmXF9z8Aj8HE3xFpsrwtfgeg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-sso@3.972.28': - resolution: {integrity: sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==} + '@aws-sdk/credential-provider-ini@3.972.42': + resolution: {integrity: sha512-Mu5ESvFXeinafVM8jTIvRqcvK2Ehj4kz3auT39yUcHwu1Vfxo6xRlmUafdKLW4tusjAJukQwK09sCSMgOm7OKg==} engines: {node: '>=20.0.0'} - '@aws-sdk/credential-provider-web-identity@3.972.28': - resolution: {integrity: sha512-d+6h0SD8GGERzKe27v5rOzNGKOl0D+l0bWJdqrxH8WSQzHzjsQFIAPgIeOTUwBHVsKKwtSxc91K/SWax6XgswQ==} + '@aws-sdk/credential-provider-login@3.972.42': + resolution: {integrity: sha512-O6WkZga3kf0yqyJYd1dbeJqVhEgJx/x1UaLgtbR+XuL/YP+K5y6QTxQKL7ka9z3jnQASESKGAPnRyt4D5hQrxA==} engines: {node: '>=20.0.0'} - '@aws-sdk/eventstream-handler-node@3.972.11': - resolution: {integrity: sha512-2IrLrOruRr1NhTK0vguBL1gCWv1pu4bf4KaqpsA+/vCJpFEbvXFawn71GvCzk1wyjnDUsemtKypqoKGv4cSGbA==} + '@aws-sdk/credential-provider-node@3.972.43': + resolution: {integrity: sha512-D/DJmbrWRP5BXEO3FH+ar4el+2n6OlGofiud7dQun2jES+AQEJjczenp1jBb4MBN7CpGpS8nsWGQLtuzc9tQbA==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-eventstream@3.972.8': - resolution: {integrity: sha512-r+oP+tbCxgqXVC3pu3MUVePgSY0ILMjA+aEwOosS77m3/DRbtvHrHwqvMcw+cjANMeGzJ+i0ar+n77KXpRA8RQ==} + '@aws-sdk/credential-provider-process@3.972.38': + resolution: {integrity: sha512-EnbYVajGgbkb24s0K1eo4VNAPV5mHIET7LSvirTaFCwkfrfaOJxtSE+wY/tJdKDS21cEYkZs2ruCaAm+W4iblg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-host-header@3.972.8': - resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==} + '@aws-sdk/credential-provider-sso@3.972.42': + resolution: {integrity: sha512-RVV/9NbFwI8ZHEH5dn39lGyFmSbSVj1+orZdr6QsOe1mW9DCglmlen0cFaNZmCcqkqc7erNRHNBduxbeZuHAnw==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-logger@3.972.8': - resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==} + '@aws-sdk/credential-provider-web-identity@3.972.42': + resolution: {integrity: sha512-/67fXX0ddllD4u2Nujc5PvT4byHgpMUfz6+RxIKi/0nFIckeorm7JvXgzBuDyVKw0s58EbofmETDWUf9vTEuHQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-recursion-detection@3.972.9': - resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==} + '@aws-sdk/eventstream-handler-node@3.972.16': + resolution: {integrity: sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-user-agent@3.972.28': - resolution: {integrity: sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==} + '@aws-sdk/middleware-eventstream@3.972.12': + resolution: {integrity: sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==} engines: {node: '>=20.0.0'} - '@aws-sdk/middleware-websocket@3.972.13': - resolution: {integrity: sha512-Gp6EWIqHX5wmsOR5ZxWyyzEU8P0xBdSxkm6VHEwXwBqScKZ7QWRoj6ZmHpr+S44EYb5tuzGya4ottsogSu2W3A==} + '@aws-sdk/middleware-websocket@3.972.20': + resolution: {integrity: sha512-LM6P0i+Lu6pi25oNw2nqxjRxiEOtLgPB7xIvHfa+FxHTRLg8wcgqu3qg2COl4QaT7Es2yCxYdeRLVYazKAwL8g==} engines: {node: '>= 14.0.0'} - '@aws-sdk/nested-clients@3.996.18': - resolution: {integrity: sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/region-config-resolver@3.972.10': - resolution: {integrity: sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1014.0': - resolution: {integrity: sha512-gHTHNUoaOGNrSWkl32A7wFsU78jlNTlqMccLu0byUk5CysYYXaxNMIonIVr4YcykC7vgtDS5ABuz83giy6fzJA==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1020.0': - resolution: {integrity: sha512-T61KA/VKl0zVUubdxigr1ut7SEpwE1/4CIKb14JDLyTAOne2yWKtQE1dDCSHl0UqrZNwW/bTt+EBHfQbslZJdw==} - engines: {node: '>=20.0.0'} - - '@aws-sdk/token-providers@3.1021.0': - resolution: {integrity: sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA==} + '@aws-sdk/nested-clients@3.997.10': + resolution: {integrity: sha512-FtQ/Bt327peZJuyo4WZSOLVUTw9ujRxntepiC7L65FxA2P82Xlq0g14T22BuqBUeMjDoxa9nvwiMHjLIfP3eUg==} engines: {node: '>=20.0.0'} - '@aws-sdk/types@3.973.6': - resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==} + '@aws-sdk/signature-v4-multi-region@3.996.27': + resolution: {integrity: sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-endpoints@3.996.5': - resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==} + '@aws-sdk/token-providers@3.1049.0': + resolution: {integrity: sha512-r7+d0lQMTHKypkmaF5jRTBYLYHCUHzt3gaVoN9SidLhQeWhCmHk3AKrboDTpPF5b7Pt7vKu3+oeMjznM2Eu1ow==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-format-url@3.972.8': - resolution: {integrity: sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==} + '@aws-sdk/types@3.973.8': + resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} engines: {node: '>=20.0.0'} '@aws-sdk/util-locate-window@3.965.5': resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} engines: {node: '>=20.0.0'} - '@aws-sdk/util-user-agent-browser@3.972.8': - resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==} - - '@aws-sdk/util-user-agent-node@3.973.14': - resolution: {integrity: sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw==} - engines: {node: '>=20.0.0'} - peerDependencies: - aws-crt: '>=1.0.0' - peerDependenciesMeta: - aws-crt: - optional: true - - '@aws-sdk/xml-builder@3.972.16': - resolution: {integrity: sha512-iu2pyvaqmeatIJLURLqx9D+4jKAdTH20ntzB6BFwjyN7V960r4jK32mx0Zf7YbtOYAbmbtQfDNuL60ONinyw7A==} + '@aws-sdk/xml-builder@3.972.24': + resolution: {integrity: sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==} engines: {node: '>=20.0.0'} '@aws/lambda-invoke-store@0.2.4': @@ -335,11 +284,13 @@ packages: '@borewit/text-codec@0.2.2': resolution: {integrity: sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==} - '@clack/core@1.1.0': - resolution: {integrity: sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==} + '@clack/core@1.3.1': + resolution: {integrity: sha512-fT1qHVGAag4IEkrupZ6lRRbNCs1vS9P01KB/sG8zKgvUztbYtFBtQpjSITNwooDZ83tpsPzP0mRNs1/KVszCRA==} + engines: {node: '>= 20.12.0'} - '@clack/prompts@1.1.0': - resolution: {integrity: sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==} + '@clack/prompts@1.4.0': + resolution: {integrity: sha512-S0My7XPGIgpRWMDG8uRqalbgT+a6FmCUdOW+HaIOVVpUPHOb7RrpvjTjiODadKp06fsrVDJZlIzc6yCTp4AnxA==} + engines: {node: '>= 20.12.0'} '@docsearch/css@3.8.2': resolution: {integrity: sha512-y05ayQFyUmCXze79+56v/4HpycYF3uFqB78pLPrSV5ZKAlDuIAAJNhaRi8tTdRNXh05yxX/TyNnzD6LwSM89vQ==} @@ -364,6 +315,24 @@ packages: search-insights: optional: true + '@earendil-works/pi-agent-core@0.75.1': + resolution: {integrity: sha512-JVpX/Zle/enBzEM6he9sE0ASMo8Yhm8q7nOuPQjR/BXhkTBUevrNz7wtTV8VFvgjyhsXzbAsNCP5A4LiCcDx/A==} + engines: {node: '>=22.19.0'} + + '@earendil-works/pi-ai@0.75.1': + resolution: {integrity: sha512-/bhCWS2R+qHLBDnN+d1t1QRUxtZk7sZpMcrlexPq3W++3bJ0Df0GjhM2FToTubhoCsjOBdBOuRYcV8FNPfRUVQ==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-coding-agent@0.75.1': + resolution: {integrity: sha512-QMbmv8lFQ8P98kpuMc/z1ATTq7t0lQ+Bo3GLiOKQ/HonO34n4E1+395FCqlmG8zJEhiMp4yqVTzlj7BALQMlqw==} + engines: {node: '>=22.19.0'} + hasBin: true + + '@earendil-works/pi-tui@0.75.1': + resolution: {integrity: sha512-IFDSvCXcXMoIxFKxdhqc7ybX8p86KpdxoTUTYEq3FHilMFkBqlXqZD0jZBitqxStBjjMkAlhjS1bKS0IOXSpsg==} + engines: {node: '>=22.19.0'} + '@emnapi/runtime@1.9.1': resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} @@ -670,8 +639,32 @@ packages: '@modelcontextprotocol/sdk': optional: true - '@homebridge/ciao@1.3.6': - resolution: {integrity: sha512-2F9N/15Q/GnoBXimr8PFg7fb1QrAQBvuZpaW2kseWOOy14Lzc3yZB1mT9N1Ju/4hlkboU33uHxtOxZkvkPoE/w==} + '@google/genai@2.3.0': + resolution: {integrity: sha512-rXDhXUBj31gZafcwQFbXvt8jMrMxZoK7ECjQpk88UfA/OkZls3PtZDprT9lM3jjqRtwRjQoNLoPoNq6MlV8qLw==} + engines: {node: '>=20.0.0'} + peerDependencies: + '@modelcontextprotocol/sdk': ^1.25.2 + peerDependenciesMeta: + '@modelcontextprotocol/sdk': + optional: true + + '@grammyjs/runner@2.0.3': + resolution: {integrity: sha512-nckmTs1dPWfVQteK9cxqxzE+0m1VRvluLWB8UgFzsjg62w3qthPJt0TYtJBEdG7OedvfQq4vnFAyE6iaMkR42A==} + engines: {node: '>=12.20.0 || >=14.13.1'} + peerDependencies: + grammy: ^1.13.1 + + '@grammyjs/transformer-throttler@1.2.1': + resolution: {integrity: sha512-CpWB0F3rJdUiKsq7826QhQsxbZi4wqfz1ccKX+fr+AOC+o8K7ZvS+wqX0suSu1QCsyUq2MDpNiKhyL2ZOJUS4w==} + engines: {node: ^12.20.0 || >=14.13.1} + peerDependencies: + grammy: ^1.0.0 + + '@grammyjs/types@3.26.0': + resolution: {integrity: sha512-jlnyfxfev/2o68HlvAGRocAXgdPPX5QabG7jZlbqC2r9DZyWBfzTlg+nu3O3Fy4EhgLWu28hZ/8wr7DsNamP9A==} + + '@homebridge/ciao@1.3.8': + resolution: {integrity: sha512-lNhpCsZVbdbjz2trFjQdzQ3cUIMZQMIMksi7wd3ntTIYgdaGLqT1Ms97DfVIJYHzRuduf56ISvgU8RRLTpK/ng==} hasBin: true '@hono/node-server@1.19.11': @@ -864,146 +857,112 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} - '@line/bot-sdk@10.6.0': - resolution: {integrity: sha512-4hSpglL/G/cW2JCcohaYz/BS0uOSJNV9IEYdMm0EiPEvDLayoI2hGq2D86uYPQFD2gvgkyhmzdShpWLG3P5r3w==} - engines: {node: '>=20'} - - '@lydell/node-pty-darwin-arm64@1.2.0-beta.3': - resolution: {integrity: sha512-owcv+e1/OSu3bf9ZBdUQqJsQF888KyuSIiPYFNn0fLhgkhm9F3Pvha76Kj5mCPnodf7hh3suDe7upw7GPRXftQ==} + '@lydell/node-pty-darwin-arm64@1.2.0-beta.12': + resolution: {integrity: sha512-tqaifcY9Cr41SblO1+FLzh8oxxtkNhuW9Dhl22lKme9BreYvKvxEZcdPIXTuqkJc5tagOEC4QHShKmJjLyLXLQ==} cpu: [arm64] os: [darwin] - '@lydell/node-pty-darwin-x64@1.2.0-beta.3': - resolution: {integrity: sha512-k38O+UviWrWdxtqZBBc/D8NJU11Rey8Y2YMwSWNxLv3eXZZdF5IVpbBkI/2RmLsV5nCcciqLPbukxeZnEfPlwA==} + '@lydell/node-pty-darwin-x64@1.2.0-beta.12': + resolution: {integrity: sha512-4LrS5pCJwqHKDVf1zS2gyNV0m4hKAXch+XZNhbZ6LY8uwVL8BhchzQBO40Os5anuRxRCWzHpw4Sp64Ie8q7E4Q==} cpu: [x64] os: [darwin] - '@lydell/node-pty-linux-arm64@1.2.0-beta.3': - resolution: {integrity: sha512-HUwRpGu3O+4sv9DAQFKnyW5LYhyYu2SDUa/bdFO/t4dIFCM4uDJEq47wfRM7+aYtJTi1b3lakN8SlWeuFQqJQQ==} + '@lydell/node-pty-linux-arm64@1.2.0-beta.12': + resolution: {integrity: sha512-Sx+A71x5BDGHt9ansfrtGxwq2VFVDWvJUAdlUL0Hv0qeiJUfts+hgopx+CgT4PSwahKjdEgtu0+FAfY9rICKRw==} cpu: [arm64] os: [linux] - '@lydell/node-pty-linux-x64@1.2.0-beta.3': - resolution: {integrity: sha512-+RRY0PoCUeQaCvPR7/UnkGbxulwbFtoTWJfe+o4T1RcNtngrgaI55I9nl8CD8uqhGrB3smKuyvPM5UtwGhASUw==} + '@lydell/node-pty-linux-x64@1.2.0-beta.12': + resolution: {integrity: sha512-bJzs94njofYhGg/UDqW1nj0dtvvu+2OvxMY+RlLS1T17VgcktKoIR6PuenTwE5HJ/D6StCPADmXcT0nNsCKmIQ==} cpu: [x64] os: [linux] - '@lydell/node-pty-win32-arm64@1.2.0-beta.3': - resolution: {integrity: sha512-UEDd9ASp2M3iIYpIzfmfBlpyn4+K1G4CAjYcHWStptCkefoSVXWTiUBIa1KjBjZi3/xmsHIDpBEYTkGWuvLt2Q==} + '@lydell/node-pty-win32-arm64@1.2.0-beta.12': + resolution: {integrity: sha512-p7POgjVEiFaBC3/y+AKuV1FzePCsJ6HmZDv2XK+jBZSfwP8+uBAw181ZiKYN1YuRa/XpmBGaWezcI8hZkbW++g==} cpu: [arm64] os: [win32] - '@lydell/node-pty-win32-x64@1.2.0-beta.3': - resolution: {integrity: sha512-TpdqSFYx7/Rj+68tuP6F/lkRYrHCYAIJgaS1bx3SctTkb5QAQCFwOKHd4xlsivmEOMT2LdhkJggPxwX9PAO5pQ==} + '@lydell/node-pty-win32-x64@1.2.0-beta.12': + resolution: {integrity: sha512-IDFa00g7qUDGUYgByrUBJtC+mOjYVt/8KYyWivCg5JjGOHbBUACUQZLl0jTWmnr+tld/UyTpX90a2PY6oTVtRw==} cpu: [x64] os: [win32] - '@lydell/node-pty@1.2.0-beta.3': - resolution: {integrity: sha512-ngGAItlRhmJXrhspxt8kX13n1dVFqzETOq0m/+gqSkO8NJBvNMwP7FZckMwps2UFySdr4yxCXNGu/bumg5at6A==} + '@lydell/node-pty@1.2.0-beta.12': + resolution: {integrity: sha512-qIK890UwPupoj07osVvgOIa++1mxeHbcGry4PKRHhNVNs81V2SCG34eJr46GybiOmBtc8Sj5PB1/GGM5PL549g==} - '@mariozechner/clipboard-darwin-arm64@0.3.2': - resolution: {integrity: sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==} + '@mariozechner/clipboard-darwin-arm64@0.3.6': + resolution: {integrity: sha512-HjaisYCAbHi/1+N1yDAQHc8ZXGffufIUT5NSOSVR3f3AuMDusxTtnbK8tZ7JFDkShua1oNGZoNwQHsc8MPtE0Q==} engines: {node: '>= 10'} cpu: [arm64] os: [darwin] - '@mariozechner/clipboard-darwin-universal@0.3.2': - resolution: {integrity: sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==} + '@mariozechner/clipboard-darwin-universal@0.3.6': + resolution: {integrity: sha512-8BWtPjOtJOJoykml3w0fx0zRrfWP31mXrJwfoA7xzNprkZw1uolCNfgmjDiVBseoKjp16EGITz7bN+61qn8dWA==} engines: {node: '>= 10'} os: [darwin] - '@mariozechner/clipboard-darwin-x64@0.3.2': - resolution: {integrity: sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==} + '@mariozechner/clipboard-darwin-x64@0.3.6': + resolution: {integrity: sha512-p9syiZD1kU4I+1ya7f7g+zD1GiUvR8fdlRlNmgsZNWlyjtc8rlV2EjTLd/35x1LsdBq020GVvtzp0ZmPgBI09Q==} engines: {node: '>= 10'} cpu: [x64] os: [darwin] - '@mariozechner/clipboard-linux-arm64-gnu@0.3.2': - resolution: {integrity: sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==} + '@mariozechner/clipboard-linux-arm64-gnu@0.3.6': + resolution: {integrity: sha512-5JFf5rGofrm+V29HNF+wLthXphHdQpMbKDUYJ5tML6/Z5DLlLOV/9Ak4kDPtYyZ+Dzf+kAusE0VsFg4+tfP1IA==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [glibc] - '@mariozechner/clipboard-linux-arm64-musl@0.3.2': - resolution: {integrity: sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==} + '@mariozechner/clipboard-linux-arm64-musl@0.3.6': + resolution: {integrity: sha512-JlVjxxw0GbGC0djXYWRIqyteO3J1KZ/QG3udlEFaOD5TLOM1FnmXXAPDQBqr+aBVr720ef9K00dirYnJ0LDCtw==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] libc: [musl] - '@mariozechner/clipboard-linux-riscv64-gnu@0.3.2': - resolution: {integrity: sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==} + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.6': + resolution: {integrity: sha512-4t8BUi5zZ+L77otFQVnVSlaTyAX4TVk9EqQm4syMrEQp96trFEHEwwNHcNEBGzYv5+K7mxay50TthYkz47OWzQ==} engines: {node: '>= 10'} cpu: [riscv64] os: [linux] libc: [glibc] - '@mariozechner/clipboard-linux-x64-gnu@0.3.2': - resolution: {integrity: sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==} + '@mariozechner/clipboard-linux-x64-gnu@0.3.6': + resolution: {integrity: sha512-trtPwcNLW37irwQCJLtCxLw757jjJZk3TSnY/MU9bhtWtA3K9b/eLW0e4RGhUXDoFRds9opNWWaUDuFLa8dm0w==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [glibc] - '@mariozechner/clipboard-linux-x64-musl@0.3.2': - resolution: {integrity: sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==} + '@mariozechner/clipboard-linux-x64-musl@0.3.6': + resolution: {integrity: sha512-WfnzIvOCCWQiN0MmltCEo6cLceUDbYe+I7xyFZjaps5A+2Op/M2CY7Rey+C4ucQhrvmpoHmTSFgY9ODWk7snoA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] libc: [musl] - '@mariozechner/clipboard-win32-arm64-msvc@0.3.2': - resolution: {integrity: sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==} + '@mariozechner/clipboard-win32-arm64-msvc@0.3.6': + resolution: {integrity: sha512-+8+1aHYsBPUjmW3otmWlg+Hijt0iJvoBBs5e0mxFeUd4gDaKMB8Bn6x7c6KVtscg7E5j5NFXnwQqNSIAO4p8zQ==} engines: {node: '>= 10'} cpu: [arm64] os: [win32] - '@mariozechner/clipboard-win32-x64-msvc@0.3.2': - resolution: {integrity: sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==} + '@mariozechner/clipboard-win32-x64-msvc@0.3.6': + resolution: {integrity: sha512-S4xfPmERC8ZkiLHe3vekZCjdDwNEETCuvCgQK2kP6/TnvmUkq1y2Pk+DjM4t8uh9KMX9bH4zs5ePcKa8GTXmfg==} engines: {node: '>= 10'} cpu: [x64] os: [win32] - '@mariozechner/clipboard@0.3.2': - resolution: {integrity: sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA==} + '@mariozechner/clipboard@0.3.6': + resolution: {integrity: sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==} engines: {node: '>= 10'} - '@mariozechner/jiti@2.6.5': - resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==} - hasBin: true - - '@mariozechner/pi-agent-core@0.63.1': - resolution: {integrity: sha512-h0B20xfs/iEVR2EC4gwiE8hKI1TPeB8REdRJMgV+uXKH7gpeIZ9+s8Dp9nX35ZR0QUjkNey2+ULk2DxQtdg14Q==} - engines: {node: '>=20.0.0'} - - '@mariozechner/pi-ai@0.63.1': - resolution: {integrity: sha512-wjgwY+yfrFO6a9QdAfjWpH7iSrDean6GsKDDMohNcLCy6PreMxHOZvNM0NwJARL1tZoZovr7ikAQfLGFZbnjsw==} - engines: {node: '>=20.0.0'} - hasBin: true - - '@mariozechner/pi-coding-agent@0.63.1': - resolution: {integrity: sha512-XSoMyLtuMA7ePK1UBWqSJ/BBdtBdJUHY9nbtnNyG6GeW7Gbgd+iqljIuwmAUf8wlYL981UIfYM/WIPQ6t/dIxw==} - engines: {node: '>=20.6.0'} - hasBin: true - - '@mariozechner/pi-tui@0.63.1': - resolution: {integrity: sha512-G5p+eh1EPkFCNaaggX6vRrqttnDscK6npgmEOknoCQXZtch8XNgh9Lf3VJ0A2lZXSgR7IntG5dfXHPH/Ki64wA==} - engines: {node: '>=20.0.0'} + '@mistralai/mistralai@2.2.1': + resolution: {integrity: sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==} - '@matrix-org/matrix-sdk-crypto-nodejs@0.4.0': - resolution: {integrity: sha512-+qqgpn39XFSbsD0dFjssGO9vHEP7sTyfs8yTpt8vuqWpUpF20QMwpCZi0jpYw7GxjErNTsMshopuo8677DfGEA==} - engines: {node: '>= 22'} - - '@matrix-org/matrix-sdk-crypto-wasm@18.0.0': - resolution: {integrity: sha512-88+n+dvxLI1cjS10UIlKXVYK7TGWbpAnnaDC9fow7ch/hCvdu3dFhJ3tS3/13N9s9+1QFXB4FFuommj+tHJPhQ==} - engines: {node: '>= 18'} - - '@mistralai/mistralai@1.14.1': - resolution: {integrity: sha512-IiLmmZFCCTReQgPAT33r7KQ1nYo5JPdvGkrkZqA8qQ2qB1GHgs5LoP5K2ICyrjnpw2n8oSxMM/VP+liiKcGNlQ==} - - '@modelcontextprotocol/sdk@1.28.0': - resolution: {integrity: sha512-gmloF+i+flI8ouQK7MWW4mOwuMh4RePBuPFAEPC6+pdqyWOUMDOixb6qZ69owLJpz6XmyllCouc4t8YWO+E2Nw==} + '@modelcontextprotocol/sdk@1.29.0': + resolution: {integrity: sha512-zo37mZA9hJWpULgkRpowewez1y6ML5GsXJPY8FI0tBBCd77HEvza4jDqRKOXgHNn867PVGCyTdzqpz0izu5ZjQ==} engines: {node: '>=18'} peerDependencies: '@cfworker/json-schema': ^4.1.1 @@ -1016,17 +975,23 @@ packages: resolution: {integrity: sha512-juG5VWh4qAivzTAeMzvY9xs9HY5rAcr2E4I7tiSSCokRFi7XIZCAu92ZkSTsIj1OPceCifL3cpfteP3pDT9/QQ==} engines: {node: '>=14.0.0'} + '@napi-rs/canvas-android-arm64@0.1.100': + resolution: {integrity: sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + '@napi-rs/canvas-android-arm64@0.1.80': resolution: {integrity: sha512-sk7xhN/MoXeuExlggf91pNziBxLPVUqF2CAVnB57KLG/pz7+U5TKG8eXdc3pm0d7Od0WreB6ZKLj37sX9muGOQ==} engines: {node: '>= 10'} cpu: [arm64] os: [android] - '@napi-rs/canvas-android-arm64@0.1.97': - resolution: {integrity: sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==} + '@napi-rs/canvas-darwin-arm64@0.1.100': + resolution: {integrity: sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==} engines: {node: '>= 10'} cpu: [arm64] - os: [android] + os: [darwin] '@napi-rs/canvas-darwin-arm64@0.1.80': resolution: {integrity: sha512-O64APRTXRUiAz0P8gErkfEr3lipLJgM6pjATwavZ22ebhjYl/SUbpgM0xcWPQBNMP1n29afAC/Us5PX1vg+JNQ==} @@ -1034,10 +999,10 @@ packages: cpu: [arm64] os: [darwin] - '@napi-rs/canvas-darwin-arm64@0.1.97': - resolution: {integrity: sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==} + '@napi-rs/canvas-darwin-x64@0.1.100': + resolution: {integrity: sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==} engines: {node: '>= 10'} - cpu: [arm64] + cpu: [x64] os: [darwin] '@napi-rs/canvas-darwin-x64@0.1.80': @@ -1046,11 +1011,11 @@ packages: cpu: [x64] os: [darwin] - '@napi-rs/canvas-darwin-x64@0.1.97': - resolution: {integrity: sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==} + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': + resolution: {integrity: sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==} engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] + cpu: [arm] + os: [linux] '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80': resolution: {integrity: sha512-eyWz0ddBDQc7/JbAtY4OtZ5SpK8tR4JsCYEZjCE3dI8pqoWUC8oMwYSBGCYfsx2w47cQgQCgMVRVTFiiO38hHQ==} @@ -1058,11 +1023,12 @@ packages: cpu: [arm] os: [linux] - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.97': - resolution: {integrity: sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==} + '@napi-rs/canvas-linux-arm64-gnu@0.1.100': + resolution: {integrity: sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==} engines: {node: '>= 10'} - cpu: [arm] + cpu: [arm64] os: [linux] + libc: [glibc] '@napi-rs/canvas-linux-arm64-gnu@0.1.80': resolution: {integrity: sha512-qwA63t8A86bnxhuA/GwOkK3jvb+XTQaTiVML0vAWoHyoZYTjNs7BzoOONDgTnNtr8/yHrq64XXzUoLqDzU+Uuw==} @@ -1071,12 +1037,12 @@ packages: os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-arm64-gnu@0.1.97': - resolution: {integrity: sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==} + '@napi-rs/canvas-linux-arm64-musl@0.1.100': + resolution: {integrity: sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==} engines: {node: '>= 10'} cpu: [arm64] os: [linux] - libc: [glibc] + libc: [musl] '@napi-rs/canvas-linux-arm64-musl@0.1.80': resolution: {integrity: sha512-1XbCOz/ymhj24lFaIXtWnwv/6eFHXDrjP0jYkc6iHQ9q8oXKzUX1Lc6bu+wuGiLhGh2GS/2JlfORC5ZcXimRcg==} @@ -1085,12 +1051,12 @@ packages: os: [linux] libc: [musl] - '@napi-rs/canvas-linux-arm64-musl@0.1.97': - resolution: {integrity: sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==} + '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': + resolution: {integrity: sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==} engines: {node: '>= 10'} - cpu: [arm64] + cpu: [riscv64] os: [linux] - libc: [musl] + libc: [glibc] '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': resolution: {integrity: sha512-XTzR125w5ZMs0lJcxRlS1K3P5RaZ9RmUsPtd1uGt+EfDyYMu4c6SEROYsxyatbbu/2+lPe7MPHOO/0a0x7L/gw==} @@ -1099,10 +1065,10 @@ packages: os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-riscv64-gnu@0.1.97': - resolution: {integrity: sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==} + '@napi-rs/canvas-linux-x64-gnu@0.1.100': + resolution: {integrity: sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==} engines: {node: '>= 10'} - cpu: [riscv64] + cpu: [x64] os: [linux] libc: [glibc] @@ -1113,12 +1079,12 @@ packages: os: [linux] libc: [glibc] - '@napi-rs/canvas-linux-x64-gnu@0.1.97': - resolution: {integrity: sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==} + '@napi-rs/canvas-linux-x64-musl@0.1.100': + resolution: {integrity: sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==} engines: {node: '>= 10'} cpu: [x64] os: [linux] - libc: [glibc] + libc: [musl] '@napi-rs/canvas-linux-x64-musl@0.1.80': resolution: {integrity: sha512-x0XvZWdHbkgdgucJsRxprX/4o4sEed7qo9rCQA9ugiS9qE2QvP0RIiEugtZhfLH3cyI+jIRFJHV4Fuz+1BHHMg==} @@ -1127,17 +1093,16 @@ packages: os: [linux] libc: [musl] - '@napi-rs/canvas-linux-x64-musl@0.1.97': - resolution: {integrity: sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==} + '@napi-rs/canvas-win32-arm64-msvc@0.1.100': + resolution: {integrity: sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==} engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - libc: [musl] + cpu: [arm64] + os: [win32] - '@napi-rs/canvas-win32-arm64-msvc@0.1.97': - resolution: {integrity: sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==} + '@napi-rs/canvas-win32-x64-msvc@0.1.100': + resolution: {integrity: sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==} engines: {node: '>= 10'} - cpu: [arm64] + cpu: [x64] os: [win32] '@napi-rs/canvas-win32-x64-msvc@0.1.80': @@ -1146,19 +1111,26 @@ packages: cpu: [x64] os: [win32] - '@napi-rs/canvas-win32-x64-msvc@0.1.97': - resolution: {integrity: sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==} + '@napi-rs/canvas@0.1.100': + resolution: {integrity: sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==} engines: {node: '>= 10'} - cpu: [x64] - os: [win32] '@napi-rs/canvas@0.1.80': resolution: {integrity: sha512-DxuT1ClnIPts1kQx8FBmkk4BQDTfI5kIzywAaMjQSXfNnra5UFU9PwurXrl+Je3bJ6BGsp/zmshVVFbCmyI+ww==} engines: {node: '>= 10'} - '@napi-rs/canvas@0.1.97': - resolution: {integrity: sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==} - engines: {node: '>= 10'} + '@nodable/entities@2.1.0': + resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} + + '@openclaw/fs-safe@0.2.4': + resolution: {integrity: sha512-Fo3WTQhxu0asD/rZqIKBqhX6fuZfjyHxSW5yTKfcRx+D9BRAcz0AGoVh+3ur/4XRvZkvsh3Ud8XTw006yRYLgg==} + engines: {node: '>=20.11'} + + '@openclaw/proxyline@0.3.3': + resolution: {integrity: sha512-sftHnW69NHQqLjCxBTvQ8f/eQl+peZ5pHCBQtuTWBbeuYRHZ0/GXVTmw/O/YKsShMbqPWhJB0UYtPPdvCUSS8w==} + engines: {node: '>=22.19.0'} + peerDependencies: + undici: '>=8.3.0 <9' '@oxfmt/binding-android-arm-eabi@0.34.0': resolution: {integrity: sha512-sqkqjh/Z38l+duOb1HtVqJTAj1grt2ttkobCopC/72+a4Xxz4xUgZPFyQ4HxrYMvyqO/YA0tvM1QbfOu70Gk1Q==} @@ -1633,203 +1605,42 @@ packages: '@silvia-odwyer/photon-node@0.3.4': resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} - '@sinclair/typebox@0.34.48': - resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==} - - '@sinclair/typebox@0.34.49': - resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==} - - '@smithy/config-resolver@4.4.13': - resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==} - engines: {node: '>=18.0.0'} - - '@smithy/core@3.23.13': - resolution: {integrity: sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==} - engines: {node: '>=18.0.0'} - - '@smithy/credential-provider-imds@4.2.12': - resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-codec@4.2.12': - resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-browser@4.2.12': - resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-config-resolver@4.3.12': - resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==} - engines: {node: '>=18.0.0'} - - '@smithy/eventstream-serde-node@4.2.12': - resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==} + '@smithy/core@3.24.3': + resolution: {integrity: sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==} engines: {node: '>=18.0.0'} - '@smithy/eventstream-serde-universal@4.2.12': - resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==} + '@smithy/credential-provider-imds@4.3.3': + resolution: {integrity: sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==} engines: {node: '>=18.0.0'} - '@smithy/fetch-http-handler@5.3.15': - resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==} - engines: {node: '>=18.0.0'} - - '@smithy/hash-node@4.2.12': - resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==} - engines: {node: '>=18.0.0'} - - '@smithy/invalid-dependency@4.2.12': - resolution: {integrity: sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==} + '@smithy/fetch-http-handler@5.4.3': + resolution: {integrity: sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==} engines: {node: '>=18.0.0'} '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} - '@smithy/is-array-buffer@4.2.2': - resolution: {integrity: sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-content-length@4.2.12': - resolution: {integrity: sha512-YE58Yz+cvFInWI/wOTrB+DbvUVz/pLn5mC5MvOV4fdRUc6qGwygyngcucRQjAhiCEbmfLOXX0gntSIcgMvAjmA==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-endpoint@4.4.28': - resolution: {integrity: sha512-p1gfYpi91CHcs5cBq982UlGlDrxoYUX6XdHSo91cQ2KFuz6QloHosO7Jc60pJiVmkWrKOV8kFYlGFFbQ2WUKKQ==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-retry@4.4.46': - resolution: {integrity: sha512-SpvWNNOPOrKQGUqZbEPO+es+FRXMWvIyzUKUOYdDgdlA6BdZj/R58p4umoQ76c2oJC44PiM7mKizyyex1IJzow==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-serde@4.2.16': - resolution: {integrity: sha512-beqfV+RZ9RSv+sQqor3xroUUYgRFCGRw6niGstPG8zO9LgTl0B0MCucxjmrH/2WwksQN7UUgI7KNANoZv+KALA==} - engines: {node: '>=18.0.0'} - - '@smithy/middleware-stack@4.2.12': - resolution: {integrity: sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==} - engines: {node: '>=18.0.0'} - - '@smithy/node-config-provider@4.3.12': - resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==} + '@smithy/node-http-handler@4.7.3': + resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} engines: {node: '>=18.0.0'} - '@smithy/node-http-handler@4.5.1': - resolution: {integrity: sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==} + '@smithy/signature-v4@5.4.3': + resolution: {integrity: sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==} engines: {node: '>=18.0.0'} - '@smithy/property-provider@4.2.12': - resolution: {integrity: sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==} - engines: {node: '>=18.0.0'} - - '@smithy/protocol-http@5.3.12': - resolution: {integrity: sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-builder@4.2.12': - resolution: {integrity: sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==} - engines: {node: '>=18.0.0'} - - '@smithy/querystring-parser@4.2.12': - resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==} - engines: {node: '>=18.0.0'} - - '@smithy/service-error-classification@4.2.12': - resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==} - engines: {node: '>=18.0.0'} - - '@smithy/shared-ini-file-loader@4.4.7': - resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==} - engines: {node: '>=18.0.0'} - - '@smithy/signature-v4@5.3.12': - resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==} - engines: {node: '>=18.0.0'} - - '@smithy/smithy-client@4.12.8': - resolution: {integrity: sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==} - engines: {node: '>=18.0.0'} - - '@smithy/types@4.13.1': - resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==} - engines: {node: '>=18.0.0'} - - '@smithy/url-parser@4.2.12': - resolution: {integrity: sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-base64@4.3.2': - resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-browser@4.2.2': - resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-body-length-node@4.2.3': - resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==} + '@smithy/types@4.14.2': + resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} engines: {node: '>=18.0.0'} '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} - '@smithy/util-buffer-from@4.2.2': - resolution: {integrity: sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==} - engines: {node: '>=18.0.0'} - - '@smithy/util-config-provider@4.2.2': - resolution: {integrity: sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-browser@4.3.44': - resolution: {integrity: sha512-eZg6XzaCbVr2S5cAErU5eGBDaOVTuTo1I65i4tQcHENRcZ8rMWhQy1DaIYUSLyZjsfXvmCqZrstSMYyGFocvHA==} - engines: {node: '>=18.0.0'} - - '@smithy/util-defaults-mode-node@4.2.48': - resolution: {integrity: sha512-FqOKTlqSaoV3nzO55pMs5NBnZX8EhoI0DGmn9kbYeXWppgHD6dchyuj2HLqp4INJDJbSrj6OFYJkAh/WhSzZPg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-endpoints@3.3.3': - resolution: {integrity: sha512-VACQVe50j0HZPjpwWcjyT51KUQ4AnsvEaQ2lKHOSL4mNLD0G9BjEniQ+yCt1qqfKfiAHRAts26ud7hBjamrwig==} - engines: {node: '>=18.0.0'} - - '@smithy/util-hex-encoding@4.2.2': - resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==} - engines: {node: '>=18.0.0'} - - '@smithy/util-middleware@4.2.12': - resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-retry@4.2.13': - resolution: {integrity: sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==} - engines: {node: '>=18.0.0'} - - '@smithy/util-stream@4.5.21': - resolution: {integrity: sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==} - engines: {node: '>=18.0.0'} - - '@smithy/util-uri-escape@4.2.2': - resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==} - engines: {node: '>=18.0.0'} - '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} - '@smithy/util-utf8@4.2.2': - resolution: {integrity: sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==} - engines: {node: '>=18.0.0'} - - '@smithy/uuid@1.1.2': - resolution: {integrity: sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==} - engines: {node: '>=18.0.0'} - - '@telegraf/types@7.1.0': - resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==} - '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -1837,9 +1648,6 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} - '@tootallnate/quickjs-emscripten@0.23.0': - resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} - '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} @@ -1849,9 +1657,6 @@ packages: '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} - '@types/events@3.0.3': - resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==} - '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -1867,12 +1672,6 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} - '@types/mime-types@2.1.4': - resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} - - '@types/node@24.12.0': - resolution: {integrity: sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==} - '@types/node@25.2.0': resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} @@ -1885,9 +1684,6 @@ packages: '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} - '@types/yauzl@2.10.3': - resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} - '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -2048,16 +1844,13 @@ packages: ajv: optional: true - ajv@8.18.0: - resolution: {integrity: sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} algoliasearch@5.50.0: resolution: {integrity: sha512-yE5I83Q2s8euVou8Y3feXK08wyZInJWLYXgWO6Xti9jBUEZAGUahyeQ7wSZWkifLWVnQVKEz5RAmBlXG5nqxog==} engines: {node: '>= 14.0.0'} - another-json@0.2.0: - resolution: {integrity: sha512-/Ndrl68UQLhnCdsAzEXLMFuOR546o2qbYRqCglaNHbjXrwG1ayTcdwr3zkSGOGtGXDyR5X9nCFfnyG2AFJIsqg==} - ansi-regex@5.0.1: resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} engines: {node: '>=8'} @@ -2074,23 +1867,19 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} - any-promise@1.3.0: - resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} - argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + asn1.js@5.4.1: + resolution: {integrity: sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} - ast-types@0.13.4: - resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} - engines: {node: '>=4'} - ast-v8-to-istanbul@0.3.11: resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==} @@ -2104,16 +1893,9 @@ packages: resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} engines: {node: 20 || >=22} - base-x@5.0.1: - resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} - base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} - basic-ftp@5.2.0: - resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==} - engines: {node: '>=10.0.0'} - bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -2123,6 +1905,9 @@ packages: bluebird@3.4.7: resolution: {integrity: sha512-iD3898SR7sWVRHbiQv+sHUtHnMvC1o3nW5rAcqnq3uOn07DSAppZYUkIGslDz6gXC7HfunPe7YVBgoEJASPcHA==} + bn.js@4.12.3: + resolution: {integrity: sha512-fGTi3gxV/23FTYdAoUtLYp6qySe2KE3teyZitipKNRuVYcBkoP/bB3guXN/XVKUe9mxCHXnc9C4ocyz8OmgN0g==} + body-parser@2.2.2: resolution: {integrity: sha512-oP5VkATKlNwcgvxi0vM0p/D3n2C3EReYVX+DNYs5TjZFn/oQt2j+4sVJtSMr18pdRr8wjTcBl6LoV+FUwzPmNA==} engines: {node: '>=18'} @@ -2130,6 +1915,9 @@ packages: boolbase@1.0.0: resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==} + bottleneck@2.19.5: + resolution: {integrity: sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==} + bowser@2.14.1: resolution: {integrity: sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==} @@ -2137,24 +1925,9 @@ packages: resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} engines: {node: 20 || >=22} - bs58@6.0.0: - resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} - - buffer-alloc-unsafe@1.1.0: - resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} - - buffer-alloc@1.2.0: - resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} - - buffer-crc32@0.2.13: - resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} - buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} - buffer-fill@1.0.0: - resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} - buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -2174,6 +1947,10 @@ packages: resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} engines: {node: '>= 0.4'} + camelcase@5.3.1: + resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==} + engines: {node: '>=6'} + ccount@2.0.1: resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} @@ -2181,10 +1958,6 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} - chalk@4.1.2: - resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} - engines: {node: '>=10'} - chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -2207,13 +1980,8 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} - cli-highlight@2.1.11: - resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} - engines: {node: '>=8.0.0', npm: '>=5.0.0'} - hasBin: true - - cliui@7.0.4: - resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@6.0.0: + resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} @@ -2289,10 +2057,6 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} - data-uri-to-buffer@6.0.2: - resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} - engines: {node: '>= 14'} - debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2302,14 +2066,14 @@ packages: supports-color: optional: true + decamelize@1.2.0: + resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} + engines: {node: '>=0.10.0'} + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} - degenerator@5.0.1: - resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} - engines: {node: '>= 14'} - delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2333,6 +2097,9 @@ packages: resolution: {integrity: sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ==} engines: {node: '>=0.3.1'} + dijkstrajs@1.0.3: + resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==} + dingbat-to-unicode@1.0.1: resolution: {integrity: sha512-98l0sW87ZT58pU4i61wa2OHwxbiYSbuxsCBozaVnYX2iCnr3bLM3fIes1/ej7h1YdOKuKt/MLs706TVnALA65w==} @@ -2352,12 +2119,8 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} - dotenv@16.6.1: - resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} - engines: {node: '>=12'} - - dotenv@17.3.1: - resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} duck@0.1.12: @@ -2389,9 +2152,6 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} - end-of-stream@1.4.5: - resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} - entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2436,30 +2196,12 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} - escodegen@2.1.0: - resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} - engines: {node: '>=6.0'} - hasBin: true - - esprima@4.0.1: - resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} - engines: {node: '>=4'} - hasBin: true - - estraverse@5.3.0: - resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} - engines: {node: '>=4.0'} - estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} - esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -2468,10 +2210,6 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} - events@3.3.0: - resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} - engines: {node: '>=0.8.x'} - eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -2497,26 +2235,27 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - extract-zip@2.0.1: - resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} - engines: {node: '>= 10.17.0'} - hasBin: true - fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + fast-string-truncated-width@3.0.3: + resolution: {integrity: sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==} + + fast-string-width@3.0.2: + resolution: {integrity: sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} - fast-xml-builder@1.1.4: - resolution: {integrity: sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==} + fast-wrap-ansi@0.2.0: + resolution: {integrity: sha512-rLV8JHxTyhVmFYhBJuMujcrHqOT2cnO5Zxj37qROj23CP39GXubJRBUFF0z8KFK77Uc0SukZUf7JZhsVEQ6n8w==} - fast-xml-parser@5.5.8: - resolution: {integrity: sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==} - hasBin: true + fast-xml-builder@1.2.0: + resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==} - fd-slicer@1.1.0: - resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fast-xml-parser@5.7.3: + resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} + hasBin: true fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} @@ -2531,18 +2270,18 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} - file-type@21.3.4: - resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} - engines: {node: '>=20'} - - file-type@22.0.0: - resolution: {integrity: sha512-cmBmnYo8Zymabm2+qAP7jTFbKF10bQpYmxoGfuZbRFRcq00BRddJdGNH/P7GA1EMpJy5yQbqa9B7yROb3z8Ziw==} + file-type@22.0.1: + resolution: {integrity: sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==} engines: {node: '>=22'} finalhandler@2.1.1: resolution: {integrity: sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==} engines: {node: '>= 18.0.0'} + find-up@4.1.0: + resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==} + engines: {node: '>=8'} + focus-trap@7.8.0: resolution: {integrity: sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA==} @@ -2583,18 +2322,10 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} - gaxios@6.7.1: - resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} - engines: {node: '>=14'} - gaxios@7.1.4: resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} engines: {node: '>=18'} - gcp-metadata@6.1.1: - resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} - engines: {node: '>=14'} - gcp-metadata@8.1.2: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} @@ -2615,14 +2346,6 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} - get-stream@5.2.0: - resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} - engines: {node: '>=8'} - - get-uri@6.0.5: - resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} - engines: {node: '>= 14'} - glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -2636,14 +2359,6 @@ packages: resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} engines: {node: '>=18'} - google-auth-library@9.15.1: - resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} - engines: {node: '>=14'} - - google-logging-utils@0.0.2: - resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} - engines: {node: '>=14'} - google-logging-utils@1.1.3: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} @@ -2655,9 +2370,9 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - gtoken@7.1.0: - resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} - engines: {node: '>=14.0.0'} + grammy@1.42.0: + resolution: {integrity: sha512-1AdCge+AkjSdp2FwfICSFnVbl8Mq3KVHJDy+DgTI9+D6keJ0zWALPRKas5jv/8psiCzL4N2cEOcGW7O45Kn39g==} + engines: {node: ^12.20.0 || >=14.13.1} has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} @@ -2715,6 +2430,10 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http_ece@1.2.0: + resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} + engines: {node: '>=16'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} @@ -2744,25 +2463,17 @@ packages: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} - ipaddr.js@2.3.0: - resolution: {integrity: sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==} + ipaddr.js@2.4.0: + resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} engines: {node: '>= 10'} is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} - is-network-error@1.3.1: - resolution: {integrity: sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==} - engines: {node: '>=16'} - is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} - is-stream@2.0.1: - resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} - engines: {node: '>=8'} - is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} @@ -2792,8 +2503,8 @@ packages: jackspeak@3.4.3: resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} - jiti@2.6.1: - resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} hasBin: true jose@6.2.2: @@ -2839,6 +2550,10 @@ packages: koffi@2.15.2: resolution: {integrity: sha512-r9tjJLVRSOhCRWdVyQlF3/Ugzeg13jlzS4czS82MAgLff4W+BcYOW7g8Y62t9O5JYjYOLAjAovAZDNlDfZNu+g==} + kysely@0.29.1: + resolution: {integrity: sha512-mOW4e+UMfrV1u/+a4uXO72mkwEJCIL4Tb/OQ8wU8jY5spUHxLKFfC1AnfNhfSoHubnIRly3u/xgnMdD0Vzq2RQ==} + engines: {node: '>=22.0.0'} + lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -2854,9 +2569,9 @@ packages: linkify-it@5.0.0: resolution: {integrity: sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==} - loglevel@1.9.2: - resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} - engines: {node: '>= 0.6.0'} + locate-path@5.0.0: + resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} + engines: {node: '>=8'} long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -2874,10 +2589,6 @@ packages: resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} engines: {node: 20 || >=22} - lru-cache@7.18.3: - resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} - engines: {node: '>=12'} - magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2909,16 +2620,6 @@ packages: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} - matrix-events-sdk@0.0.1: - resolution: {integrity: sha512-1QEOsXO+bhyCroIe2/A5OwaxHvBm7EsSQ46DEDn8RBIfQwN5HWBpFvyWWR4QY0KHPPnnJdI99wgRiAl7Ad5qaA==} - - matrix-js-sdk@41.2.0: - resolution: {integrity: sha512-kVLDKm/bUlwlHoDKRemshs27LCnOnNpmsVKtbCOM5F5D/E1SkrKldou+vQ/lk4PyXTvu9/qglkd2m0pBwJ5opg==} - engines: {node: '>=22.0.0'} - - matrix-widget-api@1.17.0: - resolution: {integrity: sha512-5FHoo3iEP3Bdlv5jsYPWOqj+pGdFQNLWnJLiB0V7Ygne7bb+Gsj3ibyFyHWC6BVw+Z+tSW4ljHpO17I9TwStwQ==} - mdast-util-to-hast@13.2.1: resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} @@ -2964,6 +2665,9 @@ packages: resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==} engines: {node: '>=18'} + minimalistic-assert@1.0.1: + resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} + minimatch@10.2.4: resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} engines: {node: 18 || 20 || >=22} @@ -2972,6 +2676,9 @@ packages: resolution: {integrity: sha512-kQAVowdR33euIqeA0+VZTDqU+qo1IeVY+hrKYtZMio3Pg0P0vuh/kwRylLUddJhB6pf3q/botcOvRtx4IN1wqQ==} engines: {node: '>=16 || 14 >=14.17'} + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + minipass@7.1.3: resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} engines: {node: '>=16 || 14 >=14.17'} @@ -2986,16 +2693,9 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} - mri@1.2.0: - resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} - engines: {node: '>=4'} - ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} - mz@2.7.0: - resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} - nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -3005,20 +2705,15 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} - netmask@2.0.2: - resolution: {integrity: sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==} - engines: {node: '>= 0.4.0'} + node-addon-api@8.7.0: + resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} + engines: {node: ^18 || ^20 || >= 21} node-domexception@1.0.0: resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==} engines: {node: '>=10.5.0'} deprecated: Use your platform's native DOMException instead - node-downloader-helper@2.1.11: - resolution: {integrity: sha512-882fH2C9AWdiPCwz/2beq5t8FGMZK9Dx8TJUOIxzMCbvG7XUKM5BuJwN5f0NKo4SCQK6jR4p2TPm54mYGdGchQ==} - engines: {node: '>=14.18'} - hasBin: true - node-edge-tts@1.2.10: resolution: {integrity: sha512-bV2i4XU54D45+US0Zm1HcJRkifuB3W438dWyuJEHLQdKxnuqlI1kim2MOvR6Q3XUQZvfF9PoDyR1Rt7aeXhPdQ==} hasBin: true @@ -3036,8 +2731,9 @@ packages: resolution: {integrity: sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - node-readable-to-web-readable-stream@0.4.2: - resolution: {integrity: sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==} + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true nth-check@2.1.1: resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==} @@ -3050,10 +2746,6 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} - oidc-client-ts@3.5.0: - resolution: {integrity: sha512-l2q8l9CTCTOlbX+AnK4p3M+4CEpKpyQhle6blQkdFhm0IsBqsxm15bYaSa11G7pWdsYr6epdsRZxJpCyCRbT8A==} - engines: {node: '>=18'} - on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3076,29 +2768,26 @@ packages: zod: optional: true - openclaw@2026.3.28: - resolution: {integrity: sha512-n7ZS3zdimB2H/GfnylyG8xWXVrmlsSPHZdNEIEPe54Sl5XYuYD5yxilGYV0DWowgtsM5ysFEQMMMArdC/O77Jw==} - engines: {node: '>=22.14.0'} + openai@6.38.0: + resolution: {integrity: sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g==} hasBin: true peerDependencies: - '@napi-rs/canvas': ^0.1.89 - node-llama-cpp: 3.18.1 + ws: ^8.18.0 + zod: ^3.25 || ^4.0 peerDependenciesMeta: - node-llama-cpp: + ws: + optional: true + zod: optional: true - openshell@0.1.0: - resolution: {integrity: sha512-B7jLewH+d73hraWcrSFgNOjvd+frW5JPejkTpqgj2EJBjX/Yk1Y4blgP5pDl4FwrBxfmwsTKR08Uwgrdo+xpSg==} - engines: {node: '>=18'} + openclaw@2026.5.18: + resolution: {integrity: sha512-a9p2jdD0SEFUIxyCeOsf8gcO7fdo3vn1zGSYi04gA5mE+J1gHCSJTmk+R+hDPg6XOgHLXD+S2PrKi/74qTGPKw==} + engines: {node: '>=22.19.0'} hasBin: true option@0.2.4: resolution: {integrity: sha512-pkEqbDyl8ou5cpq+VsnQbe/WlEy5qS7xPzMS1U55OCG9KPvwFD46zDbxQIj3egJSFc3D+XhYOPUzz49zQAVy7A==} - osc-progress@0.3.0: - resolution: {integrity: sha512-4/8JfsetakdeEa4vAYV45FW20aY+B/+K8NEXp5Eiar3wR8726whgHrbSg5Ar/ZY1FLJ/AGtUqV7W2IVF+Gvp9A==} - engines: {node: '>=20'} - oxfmt@0.34.0: resolution: {integrity: sha512-t+zTE4XGpzPTK+Zk9gSwcJcFi4pqjl6PwO/ZxPBJiJQ2XCKMucwjPlHxvPHyVKJtkMSyrDGfQ7Ntg/hUr4OgHQ==} engines: {node: ^20.19.0 || >=22.12.0} @@ -3118,25 +2807,21 @@ packages: oxlint-tsgolint: optional: true + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@4.1.0: + resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} + engines: {node: '>=8'} + p-retry@4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} - p-retry@7.1.1: - resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==} - engines: {node: '>=20'} - - p-timeout@4.1.0: - resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==} - engines: {node: '>=10'} - - pac-proxy-agent@7.2.0: - resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} - engines: {node: '>= 14'} - - pac-resolver@7.0.1: - resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} - engines: {node: '>= 14'} + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} @@ -3144,15 +2829,6 @@ packages: pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} - parse5-htmlparser2-tree-adapter@6.0.1: - resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} - - parse5@5.1.1: - resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} - - parse5@6.0.1: - resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} - parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -3160,8 +2836,12 @@ packages: partial-json@0.1.7: resolution: {integrity: sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==} - path-expression-matcher@1.2.0: - resolution: {integrity: sha512-DwmPWeFn+tq7TiyJ2CxezCAirXjFxvaiD03npak3cRjlP9+OjTmSy1EpIrEbh+l6JgUundniloMLDQ/6VTdhLQ==} + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-expression-matcher@1.5.0: + resolution: {integrity: sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==} engines: {node: '>=14.0.0'} path-is-absolute@1.0.1: @@ -3199,12 +2879,9 @@ packages: resolution: {integrity: sha512-DlOzet0HO7OEnmUmB6wWGJrrdvbyJKftI1bhMitK7O2N8W2gc757yyYBbINy9IDafXAV9wmKr9t7xsTaNKRG5Q==} engines: {node: '>=20.16.0 || >=22.3.0'} - pdfjs-dist@5.6.205: - resolution: {integrity: sha512-tlUj+2IDa7G1SbvBNN74UHRLJybZDWYom+k6p5KIZl7huBvsA4APi6mKL+zCxd3tLjN5hOOEE9Tv7VdzO88pfg==} - engines: {node: '>=20.19.0 || >=22.13.0 || >=24'} - - pend@1.2.0: - resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + pdfjs-dist@5.7.284: + resolution: {integrity: sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==} + engines: {node: '>=22.13.0 || >=24'} perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -3220,11 +2897,15 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} - playwright-core@1.58.2: - resolution: {integrity: sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==} + playwright-core@1.60.0: + resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} engines: {node: '>=18'} hasBin: true + pngjs@5.0.0: + resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==} + engines: {node: '>=10.13.0'} + postcss@8.5.6: resolution: {integrity: sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==} engines: {node: ^10 || ^12 || >=14} @@ -3253,28 +2934,25 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} - proxy-agent@6.5.0: - resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} - engines: {node: '>= 14'} - proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} - pump@3.0.4: - resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} - punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} - qrcode-terminal@0.12.0: - resolution: {integrity: sha512-EXtzRZmC+YGmGlDFbXKxQiMZNwCLEO6BANKXG4iCtSIM0yqc/pappSx3RIKr4r0uh5JsBckOXeKrB3Iz7mdQpQ==} + qrcode@1.5.4: + resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==} + engines: {node: '>=10.13.0'} hasBin: true qs@6.15.0: resolution: {integrity: sha512-mAZTtNCeetKMH+pSjrb76NAM8V9a05I9aBZOHztWy/UqcJdQYNsf59vrRKWnojAT9Y+GbIvoTBC++CPHqpDBhQ==} engines: {node: '>=0.6'} + quickjs-wasi@2.2.0: + resolution: {integrity: sha512-zQxXmQMrEoD3S+jQdYsloq4qAuaxKFHZj6hHqOYGwB2iQZH+q9e/lf5zQPXCKOk0WJuAjzRFbO4KwHIp2D05Iw==} + range-parser@1.2.1: resolution: {integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==} engines: {node: '>= 0.6'} @@ -3307,6 +2985,9 @@ packages: resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} engines: {node: '>=0.10.0'} + require-main-filename@2.0.0: + resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==} + retry@0.12.0: resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} engines: {node: '>= 4'} @@ -3330,20 +3011,9 @@ packages: safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} - safe-compare@1.1.4: - resolution: {integrity: sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==} - safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - sandwich-stream@2.0.2: - resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==} - engines: {node: '>= 0.10'} - - sdp-transform@3.0.0: - resolution: {integrity: sha512-gfYVRGxjHkGF2NPeUWHw5u6T/KGFtS5/drPms73gaSuMaVHKCY3lpLnGDfswVQO0kddeePoti09AwhYP4zA8dQ==} - hasBin: true - search-insights@2.17.3: resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} @@ -3360,6 +3030,9 @@ packages: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} + set-blocking@2.0.0: + resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} + setimmediate@1.0.5: resolution: {integrity: sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==} @@ -3410,18 +3083,6 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} - smart-buffer@4.2.0: - resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} - engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} - - socks-proxy-agent@8.0.5: - resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} - engines: {node: '>= 14'} - - socks@2.8.7: - resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==} - engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} - source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3443,33 +3104,33 @@ packages: sprintf-js@1.0.3: resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} - sqlite-vec-darwin-arm64@0.1.7: - resolution: {integrity: sha512-dQ7u4GKPdOPi3IfZ44K7HHdYup2JssM6fuKR9zgqRzW137uFOQmRhbYChNu+ZfW+yhJutsPgfNRFsuWKmy627w==} + sqlite-vec-darwin-arm64@0.1.9: + resolution: {integrity: sha512-jSsZpE42OfBkGL/ItyJTVCUwl6o6Ka3U5rc4j+UBDIQzC1ulSSKMEhQLthsOnF/MdAf1MuAkYhkdKmmcjaIZQg==} cpu: [arm64] os: [darwin] - sqlite-vec-darwin-x64@0.1.7: - resolution: {integrity: sha512-MDoczft1BriQcGMEz+CqeSCkB0OsAf12ytZOapS6MaB7zgNzLLSLH6Sxe3yzcPWUyDuCWgK7WzyRIo8u1vAIVA==} + sqlite-vec-darwin-x64@0.1.9: + resolution: {integrity: sha512-KDlVyqQT7pnOhU1ymB9gs7dMbSoVmKHitT+k1/xkjarcX8bBqPxWrGlK/R+C5WmWkfvWwyq5FfXfiBYCBs6PlA==} cpu: [x64] os: [darwin] - sqlite-vec-linux-arm64@0.1.7: - resolution: {integrity: sha512-V429sYT/gwr9PgtT8rbjQd6ls7CFchFpiS45TKSf7rU7wxt9MBmCVorUcheD4kEZb4VeZ6PnFXXCqPMeaHkaUw==} + sqlite-vec-linux-arm64@0.1.9: + resolution: {integrity: sha512-5wXVJ9c9kR4CHm/wVqXb/R+XUHTdpZ4nWbPHlS+gc9qQFVHs92Km4bPnCKX4rtcPMzvNis+SIzMJR1SCEwpuUw==} cpu: [arm64] os: [linux] - sqlite-vec-linux-x64@0.1.7: - resolution: {integrity: sha512-wZL+lXeW7y63DLv6FYU6Q4nv2lP5F94cWt7bJCWNiHmZ6NdKIgz/p0QlyuJA/51b8TyoDvsTdusLVlZz9cIh5A==} + sqlite-vec-linux-x64@0.1.9: + resolution: {integrity: sha512-w3tCH8xK2finW8fQJ/m8uqKodXUZ9KAuAar2UIhz4BHILfpE0WM/MTGCRfa7RjYbrYim5Luk3guvMOGI7T7JQA==} cpu: [x64] os: [linux] - sqlite-vec-windows-x64@0.1.7: - resolution: {integrity: sha512-FEZMjMT03irJxwqMQg+A+4hHCiFslxISOAkQ0eYn2lP7GdpppkgYveaT5Xnw/2V+GLq2MXOJb0nDGFNethHSkg==} + sqlite-vec-windows-x64@0.1.9: + resolution: {integrity: sha512-y3gEIyy/17bq2QFPQOWLE68TYWcRZkBQVA2XLrTPHNTOp55xJi/BBBmOm40tVMDMjtP+Elpk6UBUXdaq+46b0Q==} cpu: [x64] os: [win32] - sqlite-vec@0.1.7: - resolution: {integrity: sha512-1Sge9uRc3B6wDKR4J6sGFi/E2ai9SAU5FenDki3OmhdP/a49PO2Juy1U5yQnx2bZP5t+C3BYJTkG+KkDi3q9Xg==} + sqlite-vec@0.1.9: + resolution: {integrity: sha512-L7XJWRIBNvR9O5+vh1FQ+IGkh/3D2AzVksW5gdtk28m78Hy8skFD0pqReKH1Yp0/BUKRGcffgKvyO/EON5JXpA==} stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -3506,8 +3167,8 @@ packages: strip-literal@3.1.0: resolution: {integrity: sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==} - strnum@2.2.1: - resolution: {integrity: sha512-BwRvNd5/QoAtyW1na1y1LsJGQNvRlkde6Q/ipqqEaivoMdV+B1OMOTVdwR+N/cwVUcIt9PYyHmV8HyexCZSupg==} + strnum@2.3.0: + resolution: {integrity: sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==} strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} @@ -3528,22 +3189,14 @@ packages: resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} - telegraf@4.16.3: - resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==} - engines: {node: ^12.20.0 || >=14.13.1} - hasBin: true + tar@7.5.15: + resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==} + engines: {node: '>=18'} test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} - thenify-all@1.6.0: - resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} - engines: {node: '>=0.8'} - - thenify@3.3.1: - resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} - tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3578,9 +3231,22 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} + tokenjuice@0.7.1: + resolution: {integrity: sha512-eO048hm9UcGHASjYkIWEij8QN68amGp+S1nJyo685qB1/ol+VGEYjPglcVPvCbJbZyFHvI+BBAMvOfnqYCtpsQ==} + engines: {node: '>=20'} + hasBin: true + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tree-sitter-bash@0.25.1: + resolution: {integrity: sha512-7hMytuYIMoXOq24yRulgIxthE9YmggZIOHCyPTTuJcu6EU54tYD+4G39cUb28kxC6jMf/AbPfWGLQtgPTdh3xw==} + peerDependencies: + tree-sitter: ^0.25.0 + peerDependenciesMeta: + tree-sitter: + optional: true + trim-lines@3.0.1: resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} @@ -3598,11 +3264,19 @@ packages: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} + typebox@1.1.38: + resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -3619,12 +3293,9 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@7.24.7: - resolution: {integrity: sha512-H/nlJ/h0ggGC+uRL3ovD+G0i4bqhvsDOpbDv7At5eFLlj2b41L8QliGbnl2H7SnDiYhENphh1tQFJZf+MyfLsQ==} - engines: {node: '>=20.18.1'} - - unhomoglyph@1.0.6: - resolution: {integrity: sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==} + undici@8.3.0: + resolution: {integrity: sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==} + engines: {node: '>=22.19.0'} unist-util-is@6.0.1: resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} @@ -3648,14 +3319,6 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - uuid@13.0.0: - resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==} - hasBin: true - - uuid@9.0.1: - resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} - hasBin: true - vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3790,16 +3453,27 @@ packages: typescript: optional: true + web-push@3.6.7: + resolution: {integrity: sha512-OpiIUe8cuGjrj3mMBFWY+e4MMIkW3SVT+7vEIjvD9kejGUypv8GPDf84JdPWskK8zMRIJ6xYGm+Kxr8YkPyA0A==} + engines: {node: '>= 16'} + hasBin: true + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} + web-tree-sitter@0.26.8: + resolution: {integrity: sha512-4sUwi7ZyOrIk5KLgYLkc2A/F0LFMQnBhfb+2Cdl7ik4ePJ6JD+fk4ofI2sA5eGawBKBaK4Vntt7Ww5KcEsay4A==} + webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} + which-module@2.0.1: + resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==} + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} @@ -3810,6 +3484,10 @@ packages: engines: {node: '>=8'} hasBin: true + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -3833,8 +3511,8 @@ packages: utf-8-validate: optional: true - ws@8.20.0: - resolution: {integrity: sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==} + ws@8.20.1: + resolution: {integrity: sha512-It4dO0K5v//JtTXuPkfEOaI3uUN87iYPnqo/ZzqCoG3g8uhA66QUMs/SrM0YK7/NAu+r4LMh/9dq2A7k+rHs+w==} engines: {node: '>=10.0.0'} peerDependencies: bufferutil: ^4.0.1 @@ -3845,10 +3523,17 @@ packages: utf-8-validate: optional: true + xml-naming@0.1.0: + resolution: {integrity: sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==} + engines: {node: '>=16.0.0'} + xmlbuilder@10.1.1: resolution: {integrity: sha512-OyzrcFLL/nb6fMGHbiRDuPup9ljBycsdCypwuyg5AAHvyWzGfChJpCXMG88AGTIMFhGZ9RccFN1e6lhg3hkwKg==} engines: {node: '>=4.0'} + y18n@4.0.3: + resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==} + y18n@5.0.8: resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} engines: {node: '>=10'} @@ -3857,50 +3542,43 @@ packages: resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} engines: {node: '>=18'} - yaml@2.8.3: - resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} hasBin: true - yargs-parser@20.2.9: - resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} - engines: {node: '>=10'} + yargs-parser@18.1.3: + resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} + engines: {node: '>=6'} yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} - yargs@16.2.0: - resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} - engines: {node: '>=10'} + yargs@15.4.1: + resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} + engines: {node: '>=8'} yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} - yauzl@2.10.0: - resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} - - yoctocolors@2.1.2: - resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} - engines: {node: '>=18'} - zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: zod: ^3.25 || ^4 - zod@4.3.6: - resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + zod@4.4.3: + resolution: {integrity: sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==} zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} snapshots: - '@agentclientprotocol/sdk@0.17.0(zod@4.3.6)': + '@agentclientprotocol/sdk@0.21.1(zod@4.4.3)': dependencies: - zod: 4.3.6 + zod: 4.4.3 '@algolia/abtesting@1.16.0': dependencies: @@ -4019,25 +3697,16 @@ snapshots: '@jridgewell/gen-mapping': 0.3.13 '@jridgewell/trace-mapping': 0.3.31 - '@anthropic-ai/sdk@0.73.0(zod@4.3.6)': + '@anthropic-ai/sdk@0.91.1(zod@4.4.3)': dependencies: json-schema-to-ts: 3.1.1 optionalDependencies: - zod: 4.3.6 - - '@anthropic-ai/vertex-sdk@0.14.4(zod@4.3.6)': - dependencies: - '@anthropic-ai/sdk': 0.73.0(zod@4.3.6) - google-auth-library: 9.15.1 - transitivePeerDependencies: - - encoding - - supports-color - - zod + zod: 4.4.3 '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/sha256-browser@5.2.0': @@ -4045,7 +3714,7 @@ snapshots: '@aws-crypto/sha256-js': 5.2.0 '@aws-crypto/supports-web-crypto': 5.2.0 '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 '@aws-sdk/util-locate-window': 3.965.5 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 @@ -4053,7 +3722,7 @@ snapshots: '@aws-crypto/sha256-js@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 tslib: 2.8.1 '@aws-crypto/supports-web-crypto@5.2.0': @@ -4062,419 +3731,190 @@ snapshots: '@aws-crypto/util@5.2.0': dependencies: - '@aws-sdk/types': 3.973.6 + '@aws-sdk/types': 3.973.8 '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 - '@aws-sdk/client-bedrock-runtime@3.1014.0': + '@aws-sdk/client-bedrock-runtime@3.1049.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.26 - '@aws-sdk/credential-provider-node': 3.972.29 - '@aws-sdk/eventstream-handler-node': 3.972.11 - '@aws-sdk/middleware-eventstream': 3.972.8 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-user-agent': 3.972.28 - '@aws-sdk/middleware-websocket': 3.972.13 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/token-providers': 3.1014.0 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.14 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.13 - '@smithy/eventstream-serde-browser': 4.2.12 - '@smithy/eventstream-serde-config-resolver': 4.3.12 - '@smithy/eventstream-serde-node': 4.2.12 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-retry': 4.4.46 - '@smithy/middleware-serde': 4.2.16 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.1 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.44 - '@smithy/util-defaults-mode-node': 4.2.48 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.13 - '@smithy/util-stream': 4.5.21 - '@smithy/util-utf8': 4.2.2 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/credential-provider-node': 3.972.43 + '@aws-sdk/eventstream-handler-node': 3.972.16 + '@aws-sdk/middleware-eventstream': 3.972.12 + '@aws-sdk/middleware-websocket': 3.972.20 + '@aws-sdk/token-providers': 3.1049.0 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/client-bedrock@3.1020.0': + '@aws-sdk/core@3.974.12': dependencies: - '@aws-crypto/sha256-browser': 5.2.0 - '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.26 - '@aws-sdk/credential-provider-node': 3.972.29 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-user-agent': 3.972.28 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/token-providers': 3.1020.0 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.14 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.13 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-retry': 4.4.46 - '@smithy/middleware-serde': 4.2.16 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.1 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.44 - '@smithy/util-defaults-mode-node': 4.2.48 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.13 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/core@3.973.26': - dependencies: - '@aws-sdk/types': 3.973.6 - '@aws-sdk/xml-builder': 3.972.16 - '@smithy/core': 3.23.13 - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-utf8': 4.2.2 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/xml-builder': 3.972.24 + '@aws/lambda-invoke-store': 0.2.4 + '@smithy/core': 3.24.3 + '@smithy/signature-v4': 5.4.3 + '@smithy/types': 4.14.2 + bowser: 2.14.1 tslib: 2.8.1 - '@aws-sdk/credential-provider-env@3.972.24': + '@aws-sdk/credential-provider-env@3.972.38': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-http@3.972.26': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.1 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.21 - tslib: 2.8.1 - - '@aws-sdk/credential-provider-ini@3.972.28': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/credential-provider-env': 3.972.24 - '@aws-sdk/credential-provider-http': 3.972.26 - '@aws-sdk/credential-provider-login': 3.972.28 - '@aws-sdk/credential-provider-process': 3.972.24 - '@aws-sdk/credential-provider-sso': 3.972.28 - '@aws-sdk/credential-provider-web-identity': 3.972.28 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-login@3.972.28': + '@aws-sdk/credential-provider-http@3.972.40': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/credential-provider-node@3.972.29': - dependencies: - '@aws-sdk/credential-provider-env': 3.972.24 - '@aws-sdk/credential-provider-http': 3.972.26 - '@aws-sdk/credential-provider-ini': 3.972.28 - '@aws-sdk/credential-provider-process': 3.972.24 - '@aws-sdk/credential-provider-sso': 3.972.28 - '@aws-sdk/credential-provider-web-identity': 3.972.28 - '@aws-sdk/types': 3.973.6 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/credential-provider-process@3.972.24': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/credential-provider-ini@3.972.42': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/credential-provider-env': 3.972.38 + '@aws-sdk/credential-provider-http': 3.972.40 + '@aws-sdk/credential-provider-login': 3.972.42 + '@aws-sdk/credential-provider-process': 3.972.38 + '@aws-sdk/credential-provider-sso': 3.972.42 + '@aws-sdk/credential-provider-web-identity': 3.972.42 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/credential-provider-imds': 4.3.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@aws-sdk/credential-provider-sso@3.972.28': + '@aws-sdk/credential-provider-login@3.972.42': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/token-providers': 3.1021.0 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/credential-provider-web-identity@3.972.28': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/credential-provider-node@3.972.43': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.38 + '@aws-sdk/credential-provider-http': 3.972.40 + '@aws-sdk/credential-provider-ini': 3.972.42 + '@aws-sdk/credential-provider-process': 3.972.38 + '@aws-sdk/credential-provider-sso': 3.972.42 + '@aws-sdk/credential-provider-web-identity': 3.972.42 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/credential-provider-imds': 4.3.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/eventstream-handler-node@3.972.11': + '@aws-sdk/credential-provider-process@3.972.38': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/eventstream-codec': 4.2.12 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@aws-sdk/middleware-eventstream@3.972.8': + '@aws-sdk/credential-provider-sso@3.972.42': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/token-providers': 3.1049.0 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@aws-sdk/middleware-host-header@3.972.8': + '@aws-sdk/credential-provider-web-identity@3.972.42': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@aws-sdk/middleware-logger@3.972.8': + '@aws-sdk/eventstream-handler-node@3.972.16': dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@aws-sdk/middleware-recursion-detection@3.972.9': + '@aws-sdk/middleware-eventstream@3.972.12': dependencies: - '@aws-sdk/types': 3.973.6 - '@aws/lambda-invoke-store': 0.2.4 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@aws-sdk/middleware-user-agent@3.972.28': + '@aws-sdk/middleware-websocket@3.972.20': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@smithy/core': 3.23.13 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-retry': 4.2.13 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/signature-v4': 5.4.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@aws-sdk/middleware-websocket@3.972.13': - dependencies: - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-format-url': 3.972.8 - '@smithy/eventstream-codec': 4.2.12 - '@smithy/eventstream-serde-browser': 4.2.12 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/protocol-http': 5.3.12 - '@smithy/signature-v4': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@aws-sdk/nested-clients@3.996.18': + '@aws-sdk/nested-clients@3.997.10': dependencies: '@aws-crypto/sha256-browser': 5.2.0 '@aws-crypto/sha256-js': 5.2.0 - '@aws-sdk/core': 3.973.26 - '@aws-sdk/middleware-host-header': 3.972.8 - '@aws-sdk/middleware-logger': 3.972.8 - '@aws-sdk/middleware-recursion-detection': 3.972.9 - '@aws-sdk/middleware-user-agent': 3.972.28 - '@aws-sdk/region-config-resolver': 3.972.10 - '@aws-sdk/types': 3.973.6 - '@aws-sdk/util-endpoints': 3.996.5 - '@aws-sdk/util-user-agent-browser': 3.972.8 - '@aws-sdk/util-user-agent-node': 3.973.14 - '@smithy/config-resolver': 4.4.13 - '@smithy/core': 3.23.13 - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/hash-node': 4.2.12 - '@smithy/invalid-dependency': 4.2.12 - '@smithy/middleware-content-length': 4.2.12 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-retry': 4.4.46 - '@smithy/middleware-serde': 4.2.16 - '@smithy/middleware-stack': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/node-http-handler': 4.5.1 - '@smithy/protocol-http': 5.3.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-body-length-node': 4.2.3 - '@smithy/util-defaults-mode-browser': 4.3.44 - '@smithy/util-defaults-mode-node': 4.2.48 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.13 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - - '@aws-sdk/region-config-resolver@3.972.10': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/config-resolver': 4.4.13 - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/token-providers@3.1014.0': - dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/signature-v4-multi-region': 3.996.27 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/token-providers@3.1020.0': + '@aws-sdk/signature-v4-multi-region@3.996.27': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/signature-v4': 5.4.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/token-providers@3.1021.0': + '@aws-sdk/token-providers@3.1049.0': dependencies: - '@aws-sdk/core': 3.973.26 - '@aws-sdk/nested-clients': 3.996.18 - '@aws-sdk/types': 3.973.6 - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - transitivePeerDependencies: - - aws-crt - '@aws-sdk/types@3.973.6': + '@aws-sdk/types@3.973.8': dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@aws-sdk/util-endpoints@3.996.5': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-endpoints': 3.3.3 - tslib: 2.8.1 - - '@aws-sdk/util-format-url@3.972.8': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 + '@smithy/types': 4.14.2 tslib: 2.8.1 '@aws-sdk/util-locate-window@3.965.5': dependencies: tslib: 2.8.1 - '@aws-sdk/util-user-agent-browser@3.972.8': - dependencies: - '@aws-sdk/types': 3.973.6 - '@smithy/types': 4.13.1 - bowser: 2.14.1 - tslib: 2.8.1 - - '@aws-sdk/util-user-agent-node@3.973.14': - dependencies: - '@aws-sdk/middleware-user-agent': 3.972.28 - '@aws-sdk/types': 3.973.6 - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-config-provider': 4.2.2 - tslib: 2.8.1 - - '@aws-sdk/xml-builder@3.972.16': + '@aws-sdk/xml-builder@3.972.24': dependencies: - '@smithy/types': 4.13.1 - fast-xml-parser: 5.5.8 + '@nodable/entities': 2.1.0 + '@smithy/types': 4.14.2 + fast-xml-parser: 5.7.3 tslib: 2.8.1 '@aws/lambda-invoke-store@0.2.4': {} @@ -4502,13 +3942,16 @@ snapshots: '@borewit/text-codec@0.2.2': {} - '@clack/core@1.1.0': + '@clack/core@1.3.1': dependencies: + fast-wrap-ansi: 0.2.0 sisteransi: 1.0.5 - '@clack/prompts@1.1.0': + '@clack/prompts@1.4.0': dependencies: - '@clack/core': 1.1.0 + '@clack/core': 1.3.1 + fast-string-width: 3.0.2 + fast-wrap-ansi: 0.2.0 sisteransi: 1.0.5 '@docsearch/css@3.8.2': {} @@ -4535,6 +3978,74 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' + '@earendil-works/pi-agent-core@0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-ai': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + ignore: 7.0.5 + typebox: 1.1.38 + yaml: 2.9.0 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-ai@0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) + '@aws-sdk/client-bedrock-runtime': 3.1049.0 + '@google/genai': 1.46.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) + '@mistralai/mistralai': 2.2.1 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + openai: 6.26.0(ws@8.20.1)(zod@4.4.3) + partial-json: 0.1.7 + typebox: 1.1.38 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-coding-agent@0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + dependencies: + '@earendil-works/pi-agent-core': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-ai': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-tui': 0.75.1 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + diff: 8.0.3 + glob: 13.0.6 + highlight.js: 10.7.3 + hosted-git-info: 9.0.2 + ignore: 7.0.5 + jiti: 2.7.0 + minimatch: 10.2.4 + proper-lockfile: 4.1.2 + typebox: 1.1.38 + undici: 8.3.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.6 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@earendil-works/pi-tui@0.75.1': + dependencies: + get-east-asian-width: 1.5.0 + marked: 15.0.12 + optionalDependencies: + koffi: 2.15.2 + '@emnapi/runtime@1.9.1': dependencies: tslib: 2.8.1 @@ -4687,20 +4198,45 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@google/genai@1.46.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))': + '@google/genai@1.46.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': dependencies: google-auth-library: 10.6.2 p-retry: 4.6.2 protobufjs: 7.5.4 - ws: 8.20.0 + ws: 8.20.1 optionalDependencies: - '@modelcontextprotocol/sdk': 1.28.0(zod@4.3.6) + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) transitivePeerDependencies: - bufferutil - supports-color - utf-8-validate - '@homebridge/ciao@1.3.6': + '@google/genai@2.3.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': + dependencies: + google-auth-library: 10.6.2 + p-retry: 4.6.2 + protobufjs: 7.5.4 + ws: 8.20.1 + optionalDependencies: + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@grammyjs/runner@2.0.3(grammy@1.42.0)': + dependencies: + abort-controller: 3.0.0 + grammy: 1.42.0 + + '@grammyjs/transformer-throttler@1.2.1(grammy@1.42.0)': + dependencies: + bottleneck: 2.19.5 + grammy: 1.42.0 + + '@grammyjs/types@3.26.0': {} + + '@homebridge/ciao@1.3.8': dependencies: debug: 4.4.3 fast-deep-equal: 3.1.3 @@ -4719,7 +4255,8 @@ snapshots: '@iconify/types@2.0.0': {} - '@img/colour@1.1.0': {} + '@img/colour@1.1.0': + optional: true '@img/sharp-darwin-arm64@0.34.5': optionalDependencies: @@ -4844,193 +4381,91 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@line/bot-sdk@10.6.0': - dependencies: - '@types/node': 24.12.0 - optionalDependencies: - axios: 1.13.6(debug@4.4.3) - transitivePeerDependencies: - - debug - - '@lydell/node-pty-darwin-arm64@1.2.0-beta.3': + '@lydell/node-pty-darwin-arm64@1.2.0-beta.12': optional: true - '@lydell/node-pty-darwin-x64@1.2.0-beta.3': + '@lydell/node-pty-darwin-x64@1.2.0-beta.12': optional: true - '@lydell/node-pty-linux-arm64@1.2.0-beta.3': + '@lydell/node-pty-linux-arm64@1.2.0-beta.12': optional: true - '@lydell/node-pty-linux-x64@1.2.0-beta.3': + '@lydell/node-pty-linux-x64@1.2.0-beta.12': optional: true - '@lydell/node-pty-win32-arm64@1.2.0-beta.3': + '@lydell/node-pty-win32-arm64@1.2.0-beta.12': optional: true - '@lydell/node-pty-win32-x64@1.2.0-beta.3': + '@lydell/node-pty-win32-x64@1.2.0-beta.12': optional: true - '@lydell/node-pty@1.2.0-beta.3': + '@lydell/node-pty@1.2.0-beta.12': optionalDependencies: - '@lydell/node-pty-darwin-arm64': 1.2.0-beta.3 - '@lydell/node-pty-darwin-x64': 1.2.0-beta.3 - '@lydell/node-pty-linux-arm64': 1.2.0-beta.3 - '@lydell/node-pty-linux-x64': 1.2.0-beta.3 - '@lydell/node-pty-win32-arm64': 1.2.0-beta.3 - '@lydell/node-pty-win32-x64': 1.2.0-beta.3 + '@lydell/node-pty-darwin-arm64': 1.2.0-beta.12 + '@lydell/node-pty-darwin-x64': 1.2.0-beta.12 + '@lydell/node-pty-linux-arm64': 1.2.0-beta.12 + '@lydell/node-pty-linux-x64': 1.2.0-beta.12 + '@lydell/node-pty-win32-arm64': 1.2.0-beta.12 + '@lydell/node-pty-win32-x64': 1.2.0-beta.12 - '@mariozechner/clipboard-darwin-arm64@0.3.2': + '@mariozechner/clipboard-darwin-arm64@0.3.6': optional: true - '@mariozechner/clipboard-darwin-universal@0.3.2': + '@mariozechner/clipboard-darwin-universal@0.3.6': optional: true - '@mariozechner/clipboard-darwin-x64@0.3.2': + '@mariozechner/clipboard-darwin-x64@0.3.6': optional: true - '@mariozechner/clipboard-linux-arm64-gnu@0.3.2': + '@mariozechner/clipboard-linux-arm64-gnu@0.3.6': optional: true - '@mariozechner/clipboard-linux-arm64-musl@0.3.2': + '@mariozechner/clipboard-linux-arm64-musl@0.3.6': optional: true - '@mariozechner/clipboard-linux-riscv64-gnu@0.3.2': + '@mariozechner/clipboard-linux-riscv64-gnu@0.3.6': optional: true - '@mariozechner/clipboard-linux-x64-gnu@0.3.2': + '@mariozechner/clipboard-linux-x64-gnu@0.3.6': optional: true - '@mariozechner/clipboard-linux-x64-musl@0.3.2': + '@mariozechner/clipboard-linux-x64-musl@0.3.6': optional: true - '@mariozechner/clipboard-win32-arm64-msvc@0.3.2': + '@mariozechner/clipboard-win32-arm64-msvc@0.3.6': optional: true - '@mariozechner/clipboard-win32-x64-msvc@0.3.2': - optional: true - - '@mariozechner/clipboard@0.3.2': - optionalDependencies: - '@mariozechner/clipboard-darwin-arm64': 0.3.2 - '@mariozechner/clipboard-darwin-universal': 0.3.2 - '@mariozechner/clipboard-darwin-x64': 0.3.2 - '@mariozechner/clipboard-linux-arm64-gnu': 0.3.2 - '@mariozechner/clipboard-linux-arm64-musl': 0.3.2 - '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.2 - '@mariozechner/clipboard-linux-x64-gnu': 0.3.2 - '@mariozechner/clipboard-linux-x64-musl': 0.3.2 - '@mariozechner/clipboard-win32-arm64-msvc': 0.3.2 - '@mariozechner/clipboard-win32-x64-msvc': 0.3.2 + '@mariozechner/clipboard-win32-x64-msvc@0.3.6': optional: true - '@mariozechner/jiti@2.6.5': - dependencies: - std-env: 3.10.0 - yoctocolors: 2.1.2 - - '@mariozechner/pi-agent-core@0.63.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': - dependencies: - '@mariozechner/pi-ai': 0.63.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@mariozechner/pi-ai@0.63.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': - dependencies: - '@anthropic-ai/sdk': 0.73.0(zod@4.3.6) - '@aws-sdk/client-bedrock-runtime': 3.1014.0 - '@google/genai': 1.46.0(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6)) - '@mistralai/mistralai': 1.14.1 - '@sinclair/typebox': 0.34.49 - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) - chalk: 5.6.2 - openai: 6.26.0(ws@8.20.0)(zod@4.3.6) - partial-json: 0.1.7 - proxy-agent: 6.5.0 - undici: 7.24.7 - zod-to-json-schema: 3.25.1(zod@4.3.6) - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@mariozechner/pi-coding-agent@0.63.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)': - dependencies: - '@mariozechner/jiti': 2.6.5 - '@mariozechner/pi-agent-core': 0.63.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@mariozechner/pi-ai': 0.63.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@mariozechner/pi-tui': 0.63.1 - '@silvia-odwyer/photon-node': 0.3.4 - ajv: 8.18.0 - chalk: 5.6.2 - cli-highlight: 2.1.11 - diff: 8.0.3 - extract-zip: 2.0.1 - file-type: 21.3.4 - glob: 13.0.6 - hosted-git-info: 9.0.2 - ignore: 7.0.5 - marked: 15.0.12 - minimatch: 10.2.4 - proper-lockfile: 4.1.2 - strip-ansi: 7.1.2 - undici: 7.24.7 - yaml: 2.8.3 + '@mariozechner/clipboard@0.3.6': optionalDependencies: - '@mariozechner/clipboard': 0.3.2 + '@mariozechner/clipboard-darwin-arm64': 0.3.6 + '@mariozechner/clipboard-darwin-universal': 0.3.6 + '@mariozechner/clipboard-darwin-x64': 0.3.6 + '@mariozechner/clipboard-linux-arm64-gnu': 0.3.6 + '@mariozechner/clipboard-linux-arm64-musl': 0.3.6 + '@mariozechner/clipboard-linux-riscv64-gnu': 0.3.6 + '@mariozechner/clipboard-linux-x64-gnu': 0.3.6 + '@mariozechner/clipboard-linux-x64-musl': 0.3.6 + '@mariozechner/clipboard-win32-arm64-msvc': 0.3.6 + '@mariozechner/clipboard-win32-x64-msvc': 0.3.6 + optional: true + + '@mistralai/mistralai@2.2.1': + dependencies: + ws: 8.20.1 + zod: 4.4.3 + zod-to-json-schema: 3.25.1(zod@4.4.3) transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - aws-crt - bufferutil - - supports-color - utf-8-validate - - ws - - zod - '@mariozechner/pi-tui@0.63.1': - dependencies: - '@types/mime-types': 2.1.4 - chalk: 5.6.2 - get-east-asian-width: 1.5.0 - marked: 15.0.12 - mime-types: 3.0.2 - optionalDependencies: - koffi: 2.15.2 - - '@matrix-org/matrix-sdk-crypto-nodejs@0.4.0': - dependencies: - https-proxy-agent: 7.0.6 - node-downloader-helper: 2.1.11 - transitivePeerDependencies: - - supports-color - optional: true - - '@matrix-org/matrix-sdk-crypto-wasm@18.0.0': {} - - '@mistralai/mistralai@1.14.1': - dependencies: - ws: 8.20.0 - zod: 4.3.6 - zod-to-json-schema: 3.25.1(zod@4.3.6) - transitivePeerDependencies: - - bufferutil - - utf-8-validate - - '@modelcontextprotocol/sdk@1.28.0(zod@4.3.6)': + '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: '@hono/node-server': 1.19.11(hono@4.12.9) - ajv: 8.18.0 - ajv-formats: 3.0.1(ajv@8.18.0) + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 cors: 2.8.6 cross-spawn: 7.0.6 @@ -5043,74 +4478,89 @@ snapshots: json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 raw-body: 3.0.2 - zod: 4.3.6 - zod-to-json-schema: 3.25.1(zod@4.3.6) + zod: 4.4.3 + zod-to-json-schema: 3.25.1(zod@4.4.3) transitivePeerDependencies: - supports-color '@mozilla/readability@0.6.0': {} + '@napi-rs/canvas-android-arm64@0.1.100': + optional: true + '@napi-rs/canvas-android-arm64@0.1.80': optional: true - '@napi-rs/canvas-android-arm64@0.1.97': + '@napi-rs/canvas-darwin-arm64@0.1.100': optional: true '@napi-rs/canvas-darwin-arm64@0.1.80': optional: true - '@napi-rs/canvas-darwin-arm64@0.1.97': + '@napi-rs/canvas-darwin-x64@0.1.100': optional: true '@napi-rs/canvas-darwin-x64@0.1.80': optional: true - '@napi-rs/canvas-darwin-x64@0.1.97': + '@napi-rs/canvas-linux-arm-gnueabihf@0.1.100': optional: true '@napi-rs/canvas-linux-arm-gnueabihf@0.1.80': optional: true - '@napi-rs/canvas-linux-arm-gnueabihf@0.1.97': + '@napi-rs/canvas-linux-arm64-gnu@0.1.100': optional: true '@napi-rs/canvas-linux-arm64-gnu@0.1.80': optional: true - '@napi-rs/canvas-linux-arm64-gnu@0.1.97': + '@napi-rs/canvas-linux-arm64-musl@0.1.100': optional: true '@napi-rs/canvas-linux-arm64-musl@0.1.80': optional: true - '@napi-rs/canvas-linux-arm64-musl@0.1.97': + '@napi-rs/canvas-linux-riscv64-gnu@0.1.100': optional: true '@napi-rs/canvas-linux-riscv64-gnu@0.1.80': optional: true - '@napi-rs/canvas-linux-riscv64-gnu@0.1.97': + '@napi-rs/canvas-linux-x64-gnu@0.1.100': optional: true '@napi-rs/canvas-linux-x64-gnu@0.1.80': optional: true - '@napi-rs/canvas-linux-x64-gnu@0.1.97': + '@napi-rs/canvas-linux-x64-musl@0.1.100': optional: true '@napi-rs/canvas-linux-x64-musl@0.1.80': optional: true - '@napi-rs/canvas-linux-x64-musl@0.1.97': + '@napi-rs/canvas-win32-arm64-msvc@0.1.100': optional: true - '@napi-rs/canvas-win32-arm64-msvc@0.1.97': + '@napi-rs/canvas-win32-x64-msvc@0.1.100': optional: true '@napi-rs/canvas-win32-x64-msvc@0.1.80': optional: true - '@napi-rs/canvas-win32-x64-msvc@0.1.97': + '@napi-rs/canvas@0.1.100': + optionalDependencies: + '@napi-rs/canvas-android-arm64': 0.1.100 + '@napi-rs/canvas-darwin-arm64': 0.1.100 + '@napi-rs/canvas-darwin-x64': 0.1.100 + '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.100 + '@napi-rs/canvas-linux-arm64-gnu': 0.1.100 + '@napi-rs/canvas-linux-arm64-musl': 0.1.100 + '@napi-rs/canvas-linux-riscv64-gnu': 0.1.100 + '@napi-rs/canvas-linux-x64-gnu': 0.1.100 + '@napi-rs/canvas-linux-x64-musl': 0.1.100 + '@napi-rs/canvas-win32-arm64-msvc': 0.1.100 + '@napi-rs/canvas-win32-x64-msvc': 0.1.100 optional: true '@napi-rs/canvas@0.1.80': @@ -5126,19 +4576,16 @@ snapshots: '@napi-rs/canvas-linux-x64-musl': 0.1.80 '@napi-rs/canvas-win32-x64-msvc': 0.1.80 - '@napi-rs/canvas@0.1.97': + '@nodable/entities@2.1.0': {} + + '@openclaw/fs-safe@0.2.4': optionalDependencies: - '@napi-rs/canvas-android-arm64': 0.1.97 - '@napi-rs/canvas-darwin-arm64': 0.1.97 - '@napi-rs/canvas-darwin-x64': 0.1.97 - '@napi-rs/canvas-linux-arm-gnueabihf': 0.1.97 - '@napi-rs/canvas-linux-arm64-gnu': 0.1.97 - '@napi-rs/canvas-linux-arm64-musl': 0.1.97 - '@napi-rs/canvas-linux-riscv64-gnu': 0.1.97 - '@napi-rs/canvas-linux-x64-gnu': 0.1.97 - '@napi-rs/canvas-linux-x64-musl': 0.1.97 - '@napi-rs/canvas-win32-arm64-msvc': 0.1.97 - '@napi-rs/canvas-win32-x64-msvc': 0.1.97 + jszip: 3.10.1 + tar: 7.5.13 + + '@openclaw/proxyline@0.3.3(undici@8.3.0)': + dependencies: + undici: 8.3.0 '@oxfmt/binding-android-arm-eabi@0.34.0': optional: true @@ -5415,225 +4862,41 @@ snapshots: '@silvia-odwyer/photon-node@0.3.4': {} - '@sinclair/typebox@0.34.48': {} - - '@sinclair/typebox@0.34.49': {} - - '@smithy/config-resolver@4.4.13': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-config-provider': 4.2.2 - '@smithy/util-endpoints': 3.3.3 - '@smithy/util-middleware': 4.2.12 - tslib: 2.8.1 - - '@smithy/core@3.23.13': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-base64': 4.3.2 - '@smithy/util-body-length-browser': 4.2.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-stream': 4.5.21 - '@smithy/util-utf8': 4.2.2 - '@smithy/uuid': 1.1.2 - tslib: 2.8.1 - - '@smithy/credential-provider-imds@4.2.12': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - tslib: 2.8.1 - - '@smithy/eventstream-codec@4.2.12': + '@smithy/core@3.24.3': dependencies: '@aws-crypto/crc32': 5.2.0 - '@smithy/types': 4.13.1 - '@smithy/util-hex-encoding': 4.2.2 - tslib: 2.8.1 - - '@smithy/eventstream-serde-browser@4.2.12': - dependencies: - '@smithy/eventstream-serde-universal': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-config-resolver@4.3.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-node@4.2.12': - dependencies: - '@smithy/eventstream-serde-universal': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/eventstream-serde-universal@4.2.12': - dependencies: - '@smithy/eventstream-codec': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/fetch-http-handler@5.3.15': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@smithy/hash-node@4.2.12': + '@smithy/credential-provider-imds@4.3.3': dependencies: - '@smithy/types': 4.13.1 - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-utf8': 4.2.2 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@smithy/invalid-dependency@4.2.12': + '@smithy/fetch-http-handler@5.4.3': dependencies: - '@smithy/types': 4.13.1 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 '@smithy/is-array-buffer@2.2.0': dependencies: tslib: 2.8.1 - '@smithy/is-array-buffer@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/middleware-content-length@4.2.12': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/middleware-endpoint@4.4.28': - dependencies: - '@smithy/core': 3.23.13 - '@smithy/middleware-serde': 4.2.16 - '@smithy/node-config-provider': 4.3.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - '@smithy/url-parser': 4.2.12 - '@smithy/util-middleware': 4.2.12 - tslib: 2.8.1 - - '@smithy/middleware-retry@4.4.46': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/service-error-classification': 4.2.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-retry': 4.2.13 - '@smithy/uuid': 1.1.2 - tslib: 2.8.1 - - '@smithy/middleware-serde@4.2.16': - dependencies: - '@smithy/core': 3.23.13 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/middleware-stack@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/node-config-provider@4.3.12': - dependencies: - '@smithy/property-provider': 4.2.12 - '@smithy/shared-ini-file-loader': 4.4.7 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/node-http-handler@4.5.1': - dependencies: - '@smithy/protocol-http': 5.3.12 - '@smithy/querystring-builder': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/property-provider@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/protocol-http@5.3.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/querystring-builder@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - '@smithy/util-uri-escape': 4.2.2 - tslib: 2.8.1 - - '@smithy/querystring-parser@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/service-error-classification@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - - '@smithy/shared-ini-file-loader@4.4.7': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/signature-v4@5.3.12': + '@smithy/node-http-handler@4.7.3': dependencies: - '@smithy/is-array-buffer': 4.2.2 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-middleware': 4.2.12 - '@smithy/util-uri-escape': 4.2.2 - '@smithy/util-utf8': 4.2.2 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@smithy/smithy-client@4.12.8': + '@smithy/signature-v4@5.4.3': dependencies: - '@smithy/core': 3.23.13 - '@smithy/middleware-endpoint': 4.4.28 - '@smithy/middleware-stack': 4.2.12 - '@smithy/protocol-http': 5.3.12 - '@smithy/types': 4.13.1 - '@smithy/util-stream': 4.5.21 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 tslib: 2.8.1 - '@smithy/types@4.13.1': - dependencies: - tslib: 2.8.1 - - '@smithy/url-parser@4.2.12': - dependencies: - '@smithy/querystring-parser': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-base64@4.3.2': - dependencies: - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-body-length-browser@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-body-length-node@4.2.3': + '@smithy/types@4.14.2': dependencies: tslib: 2.8.1 @@ -5642,85 +4905,11 @@ snapshots: '@smithy/is-array-buffer': 2.2.0 tslib: 2.8.1 - '@smithy/util-buffer-from@4.2.2': - dependencies: - '@smithy/is-array-buffer': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-config-provider@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-defaults-mode-browser@4.3.44': - dependencies: - '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-defaults-mode-node@4.2.48': - dependencies: - '@smithy/config-resolver': 4.4.13 - '@smithy/credential-provider-imds': 4.2.12 - '@smithy/node-config-provider': 4.3.12 - '@smithy/property-provider': 4.2.12 - '@smithy/smithy-client': 4.12.8 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-endpoints@3.3.3': - dependencies: - '@smithy/node-config-provider': 4.3.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-hex-encoding@4.2.2': - dependencies: - tslib: 2.8.1 - - '@smithy/util-middleware@4.2.12': - dependencies: - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-retry@4.2.13': - dependencies: - '@smithy/service-error-classification': 4.2.12 - '@smithy/types': 4.13.1 - tslib: 2.8.1 - - '@smithy/util-stream@4.5.21': - dependencies: - '@smithy/fetch-http-handler': 5.3.15 - '@smithy/node-http-handler': 4.5.1 - '@smithy/types': 4.13.1 - '@smithy/util-base64': 4.3.2 - '@smithy/util-buffer-from': 4.2.2 - '@smithy/util-hex-encoding': 4.2.2 - '@smithy/util-utf8': 4.2.2 - tslib: 2.8.1 - - '@smithy/util-uri-escape@4.2.2': - dependencies: - tslib: 2.8.1 - '@smithy/util-utf8@2.3.0': dependencies: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 - '@smithy/util-utf8@4.2.2': - dependencies: - '@smithy/util-buffer-from': 4.2.2 - tslib: 2.8.1 - - '@smithy/uuid@1.1.2': - dependencies: - tslib: 2.8.1 - - '@telegraf/types@7.1.0': - optional: true - '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -5730,8 +4919,6 @@ snapshots: '@tokenizer/token@0.3.0': {} - '@tootallnate/quickjs-emscripten@0.23.0': {} - '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 @@ -5741,8 +4928,6 @@ snapshots: '@types/estree@1.0.8': {} - '@types/events@3.0.3': {} - '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -5760,12 +4945,6 @@ snapshots: '@types/mdurl@2.0.0': {} - '@types/mime-types@2.1.4': {} - - '@types/node@24.12.0': - dependencies: - undici-types: 7.16.0 - '@types/node@25.2.0': dependencies: undici-types: 7.16.0 @@ -5774,12 +4953,7 @@ snapshots: '@types/unist@3.0.3': {} - '@types/web-bluetooth@0.0.21': {} - - '@types/yauzl@2.10.3': - dependencies: - '@types/node': 25.2.0 - optional: true + '@types/web-bluetooth@0.0.21': {} '@ungap/structured-clone@1.3.0': {} @@ -5788,7 +4962,7 @@ snapshots: vite: 5.4.21(@types/node@25.2.0) vue: 3.5.31(typescript@5.9.3) - '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3))': + '@vitest/coverage-v8@3.2.4(vitest@3.2.4(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@ampproject/remapping': 2.3.0 '@bcoe/v8-coverage': 1.0.2 @@ -5803,7 +4977,7 @@ snapshots: std-env: 3.10.0 test-exclude: 7.0.1 tinyrainbow: 2.0.0 - vitest: 3.2.4(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3) + vitest: 3.2.4(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - supports-color @@ -5815,13 +4989,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3))': + '@vitest/mocker@3.2.4(vite@7.3.1(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0) '@vitest/pretty-format@3.2.4': dependencies: @@ -5930,7 +5104,7 @@ snapshots: transitivePeerDependencies: - typescript - '@vueuse/integrations@12.8.2(axios@1.13.6)(focus-trap@7.8.0)(jwt-decode@4.0.0)(typescript@5.9.3)': + '@vueuse/integrations@12.8.2(axios@1.13.6)(focus-trap@7.8.0)(jwt-decode@4.0.0)(qrcode@1.5.4)(typescript@5.9.3)': dependencies: '@vueuse/core': 12.8.2(typescript@5.9.3) '@vueuse/shared': 12.8.2(typescript@5.9.3) @@ -5939,6 +5113,7 @@ snapshots: axios: 1.13.6(debug@4.4.3) focus-trap: 7.8.0 jwt-decode: 4.0.0 + qrcode: 1.5.4 transitivePeerDependencies: - typescript @@ -5955,7 +5130,6 @@ snapshots: abort-controller@3.0.0: dependencies: event-target-shim: 5.0.1 - optional: true accepts@2.0.0: dependencies: @@ -5964,11 +5138,11 @@ snapshots: agent-base@7.1.4: {} - ajv-formats@3.0.1(ajv@8.18.0): + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: - ajv: 8.18.0 + ajv: 8.20.0 - ajv@8.18.0: + ajv@8.20.0: dependencies: fast-deep-equal: 3.1.3 fast-uri: 3.1.0 @@ -5992,8 +5166,6 @@ snapshots: '@algolia/requester-fetch': 5.50.0 '@algolia/requester-node-http': 5.50.0 - another-json@0.2.0: {} - ansi-regex@5.0.1: {} ansi-regex@6.2.2: {} @@ -6004,19 +5176,20 @@ snapshots: ansi-styles@6.2.3: {} - any-promise@1.3.0: {} - argparse@1.0.10: dependencies: sprintf-js: 1.0.3 argparse@2.0.1: {} - assertion-error@2.0.1: {} - - ast-types@0.13.4: + asn1.js@5.4.1: dependencies: - tslib: 2.8.1 + bn.js: 4.12.3 + inherits: 2.0.4 + minimalistic-assert: 1.0.1 + safer-buffer: 2.1.2 + + assertion-error@2.0.1: {} ast-v8-to-istanbul@0.3.11: dependencies: @@ -6036,18 +5209,16 @@ snapshots: balanced-match@4.0.3: {} - base-x@5.0.1: {} - base64-js@1.5.1: {} - basic-ftp@5.2.0: {} - bignumber.js@9.3.1: {} birpc@2.9.0: {} bluebird@3.4.7: {} + bn.js@4.12.3: {} + body-parser@2.2.2: dependencies: bytes: 3.1.2 @@ -6064,32 +5235,16 @@ snapshots: boolbase@1.0.0: {} + bottleneck@2.19.5: {} + bowser@2.14.1: {} brace-expansion@5.0.2: dependencies: balanced-match: 4.0.3 - bs58@6.0.0: - dependencies: - base-x: 5.0.1 - - buffer-alloc-unsafe@1.1.0: - optional: true - - buffer-alloc@1.2.0: - dependencies: - buffer-alloc-unsafe: 1.1.0 - buffer-fill: 1.0.0 - optional: true - - buffer-crc32@0.2.13: {} - buffer-equal-constant-time@1.0.1: {} - buffer-fill@1.0.0: - optional: true - buffer-from@1.1.2: {} bytes@3.1.2: {} @@ -6106,6 +5261,8 @@ snapshots: call-bind-apply-helpers: 1.0.2 get-intrinsic: 1.3.0 + camelcase@5.3.1: {} + ccount@2.0.1: {} chai@5.3.3: @@ -6116,11 +5273,6 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 - chalk@4.1.2: - dependencies: - ansi-styles: 4.3.0 - supports-color: 7.2.0 - chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -6135,20 +5287,11 @@ snapshots: chownr@3.0.0: {} - cli-highlight@2.1.11: - dependencies: - chalk: 4.1.2 - highlight.js: 10.7.3 - mz: 2.7.0 - parse5: 5.1.1 - parse5-htmlparser2-tree-adapter: 6.0.1 - yargs: 16.2.0 - - cliui@7.0.4: + cliui@6.0.0: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi: 7.0.0 + wrap-ansi: 6.2.0 cliui@8.0.1: dependencies: @@ -6213,19 +5356,13 @@ snapshots: data-uri-to-buffer@4.0.1: {} - data-uri-to-buffer@6.0.2: {} - debug@4.4.3: dependencies: ms: 2.1.3 - deep-eql@5.0.2: {} + decamelize@1.2.0: {} - degenerator@5.0.1: - dependencies: - ast-types: 0.13.4 - escodegen: 2.1.0 - esprima: 4.0.1 + deep-eql@5.0.2: {} delayed-stream@1.0.0: {} @@ -6233,7 +5370,8 @@ snapshots: dequal@2.0.3: {} - detect-libc@2.1.2: {} + detect-libc@2.1.2: + optional: true devlop@1.1.0: dependencies: @@ -6241,6 +5379,8 @@ snapshots: diff@8.0.3: {} + dijkstrajs@1.0.3: {} + dingbat-to-unicode@1.0.1: {} dingtalk-stream@2.1.4: @@ -6271,10 +5411,7 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 - dotenv@16.6.1: - optional: true - - dotenv@17.3.1: {} + dotenv@17.4.2: {} duck@0.1.12: dependencies: @@ -6302,10 +5439,6 @@ snapshots: encodeurl@2.0.0: {} - end-of-stream@1.4.5: - dependencies: - once: 1.4.0 - entities@4.5.0: {} entities@7.0.1: {} @@ -6386,32 +5519,15 @@ snapshots: escape-html@1.0.3: {} - escodegen@2.1.0: - dependencies: - esprima: 4.0.1 - estraverse: 5.3.0 - esutils: 2.0.3 - optionalDependencies: - source-map: 0.6.1 - - esprima@4.0.1: {} - - estraverse@5.3.0: {} - estree-walker@2.0.2: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 - esutils@2.0.3: {} - etag@1.8.1: {} - event-target-shim@5.0.1: - optional: true - - events@3.3.0: {} + event-target-shim@5.0.1: {} eventsource-parser@3.0.6: {} @@ -6461,33 +5577,31 @@ snapshots: extend@3.0.2: {} - extract-zip@2.0.1: - dependencies: - debug: 4.4.3 - get-stream: 5.2.0 - yauzl: 2.10.0 - optionalDependencies: - '@types/yauzl': 2.10.3 - transitivePeerDependencies: - - supports-color - fast-deep-equal@3.1.3: {} + fast-string-truncated-width@3.0.3: {} + + fast-string-width@3.0.2: + dependencies: + fast-string-truncated-width: 3.0.3 + fast-uri@3.1.0: {} - fast-xml-builder@1.1.4: + fast-wrap-ansi@0.2.0: dependencies: - path-expression-matcher: 1.2.0 + fast-string-width: 3.0.2 - fast-xml-parser@5.5.8: + fast-xml-builder@1.2.0: dependencies: - fast-xml-builder: 1.1.4 - path-expression-matcher: 1.2.0 - strnum: 2.2.1 + path-expression-matcher: 1.5.0 + xml-naming: 0.1.0 - fd-slicer@1.1.0: + fast-xml-parser@5.7.3: dependencies: - pend: 1.2.0 + '@nodable/entities': 2.1.0 + fast-xml-builder: 1.2.0 + path-expression-matcher: 1.5.0 + strnum: 2.3.0 fdir@6.5.0(picomatch@4.0.3): optionalDependencies: @@ -6498,16 +5612,7 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 - file-type@21.3.4: - dependencies: - '@tokenizer/inflate': 0.4.1 - strtok3: 10.3.5 - token-types: 6.1.2 - uint8array-extras: 1.5.0 - transitivePeerDependencies: - - supports-color - - file-type@22.0.0: + file-type@22.0.1: dependencies: '@tokenizer/inflate': 0.4.1 strtok3: 10.3.5 @@ -6527,6 +5632,11 @@ snapshots: transitivePeerDependencies: - supports-color + find-up@4.1.0: + dependencies: + locate-path: 5.0.0 + path-exists: 4.0.0 + focus-trap@7.8.0: dependencies: tabbable: 6.4.0 @@ -6561,17 +5671,6 @@ snapshots: function-bind@1.1.2: {} - gaxios@6.7.1: - dependencies: - extend: 3.0.2 - https-proxy-agent: 7.0.6 - is-stream: 2.0.1 - node-fetch: 2.7.0 - uuid: 9.0.1 - transitivePeerDependencies: - - encoding - - supports-color - gaxios@7.1.4: dependencies: extend: 3.0.2 @@ -6580,15 +5679,6 @@ snapshots: transitivePeerDependencies: - supports-color - gcp-metadata@6.1.1: - dependencies: - gaxios: 6.7.1 - google-logging-utils: 0.0.2 - json-bigint: 1.0.0 - transitivePeerDependencies: - - encoding - - supports-color - gcp-metadata@8.1.2: dependencies: gaxios: 7.1.4 @@ -6619,18 +5709,6 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 - get-stream@5.2.0: - dependencies: - pump: 3.0.4 - - get-uri@6.0.5: - dependencies: - basic-ftp: 5.2.0 - data-uri-to-buffer: 6.0.2 - debug: 4.4.3 - transitivePeerDependencies: - - supports-color - glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -6657,30 +5735,18 @@ snapshots: transitivePeerDependencies: - supports-color - google-auth-library@9.15.1: - dependencies: - base64-js: 1.5.1 - ecdsa-sig-formatter: 1.0.11 - gaxios: 6.7.1 - gcp-metadata: 6.1.1 - gtoken: 7.1.0 - jws: 4.0.1 - transitivePeerDependencies: - - encoding - - supports-color - - google-logging-utils@0.0.2: {} - google-logging-utils@1.1.3: {} gopd@1.2.0: {} graceful-fs@4.2.11: {} - gtoken@7.1.0: + grammy@1.42.0: dependencies: - gaxios: 6.7.1 - jws: 4.0.1 + '@grammyjs/types': 3.26.0 + abort-controller: 3.0.0 + debug: 4.4.3 + node-fetch: 2.7.0 transitivePeerDependencies: - encoding - supports-color @@ -6753,6 +5819,8 @@ snapshots: transitivePeerDependencies: - supports-color + http_ece@1.2.0: {} + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -6776,16 +5844,12 @@ snapshots: ipaddr.js@1.9.1: {} - ipaddr.js@2.3.0: {} + ipaddr.js@2.4.0: {} is-fullwidth-code-point@3.0.0: {} - is-network-error@1.3.1: {} - is-promise@4.0.0: {} - is-stream@2.0.1: {} - is-what@5.5.0: {} isarray@1.0.0: {} @@ -6819,7 +5883,7 @@ snapshots: optionalDependencies: '@pkgjs/parseargs': 0.11.0 - jiti@2.6.1: {} + jiti@2.7.0: {} jose@6.2.2: {} @@ -6860,11 +5924,14 @@ snapshots: jwa: 2.0.1 safe-buffer: 5.1.2 - jwt-decode@4.0.0: {} + jwt-decode@4.0.0: + optional: true koffi@2.15.2: optional: true + kysely@0.29.1: {} + lie@3.3.0: dependencies: immediate: 3.0.6 @@ -6881,7 +5948,9 @@ snapshots: dependencies: uc.micro: 2.1.0 - loglevel@1.9.2: {} + locate-path@5.0.0: + dependencies: + p-locate: 4.1.0 long@5.3.2: {} @@ -6897,8 +5966,6 @@ snapshots: lru-cache@11.2.7: {} - lru-cache@7.18.3: {} - magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6941,30 +6008,6 @@ snapshots: math-intrinsics@1.1.0: {} - matrix-events-sdk@0.0.1: {} - - matrix-js-sdk@41.2.0: - dependencies: - '@babel/runtime': 7.29.2 - '@matrix-org/matrix-sdk-crypto-wasm': 18.0.0 - another-json: 0.2.0 - bs58: 6.0.0 - content-type: 1.0.5 - jwt-decode: 4.0.0 - loglevel: 1.9.2 - matrix-events-sdk: 0.0.1 - matrix-widget-api: 1.17.0 - oidc-client-ts: 3.5.0 - p-retry: 7.1.1 - sdp-transform: 3.0.0 - unhomoglyph: 1.0.6 - uuid: 13.0.0 - - matrix-widget-api@1.17.0: - dependencies: - '@types/events': 3.0.3 - events: 3.3.0 - mdast-util-to-hast@13.2.1: dependencies: '@types/hast': 3.0.4 @@ -7012,6 +6055,8 @@ snapshots: dependencies: mime-db: 1.54.0 + minimalistic-assert@1.0.1: {} + minimatch@10.2.4: dependencies: brace-expansion: 5.0.2 @@ -7020,6 +6065,8 @@ snapshots: dependencies: brace-expansion: 5.0.2 + minimist@1.2.8: {} + minipass@7.1.3: {} minisearch@7.2.0: {} @@ -7030,32 +6077,20 @@ snapshots: mitt@3.0.1: {} - mri@1.2.0: - optional: true - ms@2.1.3: {} - mz@2.7.0: - dependencies: - any-promise: 1.3.0 - object-assign: 4.1.1 - thenify-all: 1.6.0 - nanoid@3.3.11: {} negotiator@1.0.0: {} - netmask@2.0.2: {} + node-addon-api@8.7.0: {} node-domexception@1.0.0: {} - node-downloader-helper@2.1.11: - optional: true - node-edge-tts@1.2.10: dependencies: https-proxy-agent: 7.0.6 - ws: 8.20.0 + ws: 8.20.1 yargs: 17.7.2 transitivePeerDependencies: - bufferutil @@ -7072,8 +6107,7 @@ snapshots: fetch-blob: 3.2.0 formdata-polyfill: 4.0.10 - node-readable-to-web-readable-stream@0.4.2: - optional: true + node-gyp-build@4.8.4: {} nth-check@2.1.1: dependencies: @@ -7083,10 +6117,6 @@ snapshots: object-inspect@1.13.4: {} - oidc-client-ts@3.5.0: - dependencies: - jwt-decode: 4.0.0 - on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -7101,87 +6131,82 @@ snapshots: regex: 6.1.0 regex-recursion: 6.0.2 - openai@6.26.0(ws@8.20.0)(zod@4.3.6): + openai@6.26.0(ws@8.20.1)(zod@4.4.3): + optionalDependencies: + ws: 8.20.1 + zod: 4.4.3 + + openai@6.38.0(ws@8.20.1)(zod@4.4.3): optionalDependencies: - ws: 8.20.0 - zod: 4.3.6 - - openclaw@2026.3.28(@napi-rs/canvas@0.1.97): - dependencies: - '@agentclientprotocol/sdk': 0.17.0(zod@4.3.6) - '@anthropic-ai/vertex-sdk': 0.14.4(zod@4.3.6) - '@aws-sdk/client-bedrock': 3.1020.0 - '@clack/prompts': 1.1.0 - '@homebridge/ciao': 1.3.6 - '@line/bot-sdk': 10.6.0 - '@lydell/node-pty': 1.2.0-beta.3 - '@mariozechner/pi-agent-core': 0.63.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@mariozechner/pi-ai': 0.63.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@mariozechner/pi-coding-agent': 0.63.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6) - '@mariozechner/pi-tui': 0.63.1 - '@modelcontextprotocol/sdk': 1.28.0(zod@4.3.6) + ws: 8.20.1 + zod: 4.4.3 + + openclaw@2026.5.18: + dependencies: + '@agentclientprotocol/sdk': 0.21.1(zod@4.4.3) + '@clack/core': 1.3.1 + '@clack/prompts': 1.4.0 + '@earendil-works/pi-agent-core': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-ai': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-coding-agent': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@earendil-works/pi-tui': 0.75.1 + '@google/genai': 2.3.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) + '@grammyjs/runner': 2.0.3(grammy@1.42.0) + '@grammyjs/transformer-throttler': 1.2.1(grammy@1.42.0) + '@homebridge/ciao': 1.3.8 + '@lydell/node-pty': 1.2.0-beta.12 + '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) '@mozilla/readability': 0.6.0 - '@napi-rs/canvas': 0.1.97 - '@sinclair/typebox': 0.34.48 - ajv: 8.18.0 + '@openclaw/fs-safe': 0.2.4 + '@openclaw/proxyline': 0.3.3(undici@8.3.0) + ajv: 8.20.0 chalk: 5.6.2 chokidar: 5.0.0 - cli-highlight: 2.1.11 commander: 14.0.3 croner: 10.0.1 - dotenv: 17.3.1 + dotenv: 17.4.2 express: 5.2.1 - file-type: 22.0.0 - gaxios: 7.1.4 - hono: 4.12.9 - ipaddr.js: 2.3.0 - jiti: 2.6.1 + file-type: 22.0.1 + grammy: 1.42.0 + ipaddr.js: 2.4.0 + jiti: 2.7.0 json5: 2.2.3 jszip: 3.10.1 + kysely: 0.29.1 linkedom: 0.18.12 - long: 5.3.2 markdown-it: 14.1.1 - matrix-js-sdk: 41.2.0 node-edge-tts: 1.2.10 - osc-progress: 0.3.0 - pdfjs-dist: 5.6.205 - playwright-core: 1.58.2 - qrcode-terminal: 0.12.0 - sharp: 0.34.5 - sqlite-vec: 0.1.7 - tar: 7.5.13 + openai: 6.38.0(ws@8.20.1)(zod@4.4.3) + pdfjs-dist: 5.7.284 + playwright-core: 1.60.0 + qrcode: 1.5.4 + quickjs-wasi: 2.2.0 + tar: 7.5.15 + tokenjuice: 0.7.1 + tree-sitter-bash: 0.25.1 tslog: 4.10.2 - undici: 7.24.7 - uuid: 13.0.0 - ws: 8.20.0 - yaml: 2.8.3 - zod: 4.3.6 + typebox: 1.1.38 + typescript: 6.0.3 + undici: 8.3.0 + web-push: 3.6.7 + web-tree-sitter: 0.26.8 + ws: 8.20.1 + yaml: 2.9.0 + zod: 4.4.3 optionalDependencies: - '@matrix-org/matrix-sdk-crypto-nodejs': 0.4.0 - openshell: 0.1.0 + sharp: 0.34.5 + sqlite-vec: 0.1.9 transitivePeerDependencies: - '@cfworker/json-schema' - - aws-crt - bufferutil - canvas - - debug - encoding - supports-color + - tree-sitter - utf-8-validate - openshell@0.1.0: - dependencies: - dotenv: 16.6.1 - telegraf: 4.16.3 - transitivePeerDependencies: - - encoding - - supports-color - optional: true - option@0.2.4: {} - osc-progress@0.3.0: {} - oxfmt@0.34.0: dependencies: tinypool: 2.1.0 @@ -7238,53 +6263,32 @@ snapshots: '@oxlint/binding-win32-x64-msvc': 1.49.0 oxlint-tsgolint: 0.18.1 - p-retry@4.6.2: + p-limit@2.3.0: dependencies: - '@types/retry': 0.12.0 - retry: 0.13.1 + p-try: 2.2.0 - p-retry@7.1.1: + p-locate@4.1.0: dependencies: - is-network-error: 1.3.1 - - p-timeout@4.1.0: - optional: true + p-limit: 2.3.0 - pac-proxy-agent@7.2.0: + p-retry@4.6.2: dependencies: - '@tootallnate/quickjs-emscripten': 0.23.0 - agent-base: 7.1.4 - debug: 4.4.3 - get-uri: 6.0.5 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - pac-resolver: 7.0.1 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color + '@types/retry': 0.12.0 + retry: 0.13.1 - pac-resolver@7.0.1: - dependencies: - degenerator: 5.0.1 - netmask: 2.0.2 + p-try@2.2.0: {} package-json-from-dist@1.0.1: {} pako@1.0.11: {} - parse5-htmlparser2-tree-adapter@6.0.1: - dependencies: - parse5: 6.0.1 - - parse5@5.1.1: {} - - parse5@6.0.1: {} - parseurl@1.3.3: {} partial-json@0.1.7: {} - path-expression-matcher@1.2.0: {} + path-exists@4.0.0: {} + + path-expression-matcher@1.5.0: {} path-is-absolute@1.0.1: {} @@ -7315,12 +6319,9 @@ snapshots: optionalDependencies: '@napi-rs/canvas': 0.1.80 - pdfjs-dist@5.6.205: + pdfjs-dist@5.7.284: optionalDependencies: - '@napi-rs/canvas': 0.1.97 - node-readable-to-web-readable-stream: 0.4.2 - - pend@1.2.0: {} + '@napi-rs/canvas': 0.1.100 perfect-debounce@1.0.0: {} @@ -7330,7 +6331,9 @@ snapshots: pkce-challenge@5.0.1: {} - playwright-core@1.58.2: {} + playwright-core@1.60.0: {} + + pngjs@5.0.0: {} postcss@8.5.6: dependencies: @@ -7376,34 +6379,22 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 - proxy-agent@6.5.0: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - lru-cache: 7.18.3 - pac-proxy-agent: 7.2.0 - proxy-from-env: 1.1.0 - socks-proxy-agent: 8.0.5 - transitivePeerDependencies: - - supports-color - proxy-from-env@1.1.0: {} - pump@3.0.4: - dependencies: - end-of-stream: 1.4.5 - once: 1.4.0 - punycode.js@2.3.1: {} - qrcode-terminal@0.12.0: {} + qrcode@1.5.4: + dependencies: + dijkstrajs: 1.0.3 + pngjs: 5.0.0 + yargs: 15.4.1 qs@6.15.0: dependencies: side-channel: 1.1.0 + quickjs-wasi@2.2.0: {} + range-parser@1.2.1: {} raw-body@3.0.2: @@ -7439,6 +6430,8 @@ snapshots: require-from-string@2.0.2: {} + require-main-filename@2.0.0: {} + retry@0.12.0: {} retry@0.13.1: {} @@ -7488,18 +6481,8 @@ snapshots: safe-buffer@5.1.2: {} - safe-compare@1.1.4: - dependencies: - buffer-alloc: 1.2.0 - optional: true - safer-buffer@2.1.2: {} - sandwich-stream@2.0.2: - optional: true - - sdp-transform@3.0.0: {} - search-insights@2.17.3: {} semver@7.7.3: {} @@ -7529,6 +6512,8 @@ snapshots: transitivePeerDependencies: - supports-color + set-blocking@2.0.0: {} + setimmediate@1.0.5: {} setprototypeof@1.2.0: {} @@ -7563,6 +6548,7 @@ snapshots: '@img/sharp-win32-arm64': 0.34.5 '@img/sharp-win32-ia32': 0.34.5 '@img/sharp-win32-x64': 0.34.5 + optional: true shebang-command@2.0.0: dependencies: @@ -7617,21 +6603,6 @@ snapshots: sisteransi@1.0.5: {} - smart-buffer@4.2.0: {} - - socks-proxy-agent@8.0.5: - dependencies: - agent-base: 7.1.4 - debug: 4.4.3 - socks: 2.8.7 - transitivePeerDependencies: - - supports-color - - socks@2.8.7: - dependencies: - ip-address: 10.1.0 - smart-buffer: 4.2.0 - source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -7647,28 +6618,29 @@ snapshots: sprintf-js@1.0.3: {} - sqlite-vec-darwin-arm64@0.1.7: + sqlite-vec-darwin-arm64@0.1.9: optional: true - sqlite-vec-darwin-x64@0.1.7: + sqlite-vec-darwin-x64@0.1.9: optional: true - sqlite-vec-linux-arm64@0.1.7: + sqlite-vec-linux-arm64@0.1.9: optional: true - sqlite-vec-linux-x64@0.1.7: + sqlite-vec-linux-x64@0.1.9: optional: true - sqlite-vec-windows-x64@0.1.7: + sqlite-vec-windows-x64@0.1.9: optional: true - sqlite-vec@0.1.7: + sqlite-vec@0.1.9: optionalDependencies: - sqlite-vec-darwin-arm64: 0.1.7 - sqlite-vec-darwin-x64: 0.1.7 - sqlite-vec-linux-arm64: 0.1.7 - sqlite-vec-linux-x64: 0.1.7 - sqlite-vec-windows-x64: 0.1.7 + sqlite-vec-darwin-arm64: 0.1.9 + sqlite-vec-darwin-x64: 0.1.9 + sqlite-vec-linux-arm64: 0.1.9 + sqlite-vec-linux-x64: 0.1.9 + sqlite-vec-windows-x64: 0.1.9 + optional: true stackback@0.0.2: {} @@ -7709,7 +6681,7 @@ snapshots: dependencies: js-tokens: 9.0.1 - strnum@2.2.1: {} + strnum@2.3.0: {} strtok3@10.3.5: dependencies: @@ -7732,21 +6704,15 @@ snapshots: minipass: 7.1.3 minizlib: 3.1.0 yallist: 5.0.0 + optional: true - telegraf@4.16.3: + tar@7.5.15: dependencies: - '@telegraf/types': 7.1.0 - abort-controller: 3.0.0 - debug: 4.4.3 - mri: 1.2.0 - node-fetch: 2.7.0 - p-timeout: 4.1.0 - safe-compare: 1.1.4 - sandwich-stream: 2.0.2 - transitivePeerDependencies: - - encoding - - supports-color - optional: true + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 test-exclude@7.0.1: dependencies: @@ -7754,14 +6720,6 @@ snapshots: glob: 10.5.0 minimatch: 9.0.6 - thenify-all@1.6.0: - dependencies: - thenify: 3.3.1 - - thenify@3.3.1: - dependencies: - any-promise: 1.3.0 - tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -7787,8 +6745,15 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 + tokenjuice@0.7.1: {} + tr46@0.0.3: {} + tree-sitter-bash@0.25.1: + dependencies: + node-addon-api: 8.7.0 + node-gyp-build: 4.8.4 + trim-lines@3.0.1: {} ts-algebra@2.0.0: {} @@ -7803,8 +6768,12 @@ snapshots: media-typer: 1.1.0 mime-types: 3.0.2 + typebox@1.1.38: {} + typescript@5.9.3: {} + typescript@6.0.3: {} + uc.micro@2.1.0: {} uhyphen@0.2.0: {} @@ -7815,9 +6784,7 @@ snapshots: undici-types@7.16.0: {} - undici@7.24.7: {} - - unhomoglyph@1.0.6: {} + undici@8.3.0: {} unist-util-is@6.0.1: dependencies: @@ -7846,10 +6813,6 @@ snapshots: util-deprecate@1.0.2: {} - uuid@13.0.0: {} - - uuid@9.0.1: {} - vary@1.1.2: {} vfile-message@4.0.3: @@ -7862,13 +6825,13 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.3 - vite-node@3.2.4(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3): + vite-node@3.2.4(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0): dependencies: cac: 6.7.14 debug: 4.4.3 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0) transitivePeerDependencies: - '@types/node' - jiti @@ -7892,7 +6855,7 @@ snapshots: '@types/node': 25.2.0 fsevents: 2.3.3 - vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3): + vite@7.3.1(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0): dependencies: esbuild: 0.27.3 fdir: 6.5.0(picomatch@4.0.3) @@ -7903,10 +6866,10 @@ snapshots: optionalDependencies: '@types/node': 25.2.0 fsevents: 2.3.3 - jiti: 2.6.1 - yaml: 2.8.3 + jiti: 2.7.0 + yaml: 2.9.0 - vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@25.2.0)(axios@1.13.6)(jwt-decode@4.0.0)(postcss@8.5.8)(search-insights@2.17.3)(typescript@5.9.3): + vitepress@1.6.4(@algolia/client-search@5.50.0)(@types/node@25.2.0)(axios@1.13.6)(jwt-decode@4.0.0)(postcss@8.5.8)(qrcode@1.5.4)(search-insights@2.17.3)(typescript@5.9.3): dependencies: '@docsearch/css': 3.8.2 '@docsearch/js': 3.8.2(@algolia/client-search@5.50.0)(search-insights@2.17.3) @@ -7919,7 +6882,7 @@ snapshots: '@vue/devtools-api': 7.7.9 '@vue/shared': 3.5.31 '@vueuse/core': 12.8.2(typescript@5.9.3) - '@vueuse/integrations': 12.8.2(axios@1.13.6)(focus-trap@7.8.0)(jwt-decode@4.0.0)(typescript@5.9.3) + '@vueuse/integrations': 12.8.2(axios@1.13.6)(focus-trap@7.8.0)(jwt-decode@4.0.0)(qrcode@1.5.4)(typescript@5.9.3) focus-trap: 7.8.0 mark.js: 8.11.1 minisearch: 7.2.0 @@ -7955,11 +6918,11 @@ snapshots: - typescript - universal-cookie - vitest@3.2.4(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3): + vitest@3.2.4(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0): dependencies: '@types/chai': 5.2.3 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3)) + '@vitest/mocker': 3.2.4(vite@7.3.1(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -7977,8 +6940,8 @@ snapshots: tinyglobby: 0.2.15 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 7.3.1(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3) - vite-node: 3.2.4(@types/node@25.2.0)(jiti@2.6.1)(yaml@2.8.3) + vite: 7.3.1(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0) + vite-node: 3.2.4(@types/node@25.2.0)(jiti@2.7.0)(yaml@2.9.0) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 25.2.0 @@ -8006,8 +6969,20 @@ snapshots: optionalDependencies: typescript: 5.9.3 + web-push@3.6.7: + dependencies: + asn1.js: 5.4.1 + http_ece: 1.2.0 + https-proxy-agent: 7.0.6 + jws: 4.0.1 + minimist: 1.2.8 + transitivePeerDependencies: + - supports-color + web-streams-polyfill@3.3.3: {} + web-tree-sitter@0.26.8: {} + webidl-conversions@3.0.1: {} whatwg-url@5.0.0: @@ -8015,6 +6990,8 @@ snapshots: tr46: 0.0.3 webidl-conversions: 3.0.1 + which-module@2.0.1: {} + which@2.0.2: dependencies: isexe: 2.0.0 @@ -8024,6 +7001,12 @@ snapshots: siginfo: 2.0.0 stackback: 0.0.2 + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -8040,29 +7023,40 @@ snapshots: ws@8.19.0: {} - ws@8.20.0: {} + ws@8.20.1: {} + + xml-naming@0.1.0: {} xmlbuilder@10.1.1: {} + y18n@4.0.3: {} + y18n@5.0.8: {} yallist@5.0.0: {} - yaml@2.8.3: {} + yaml@2.9.0: {} - yargs-parser@20.2.9: {} + yargs-parser@18.1.3: + dependencies: + camelcase: 5.3.1 + decamelize: 1.2.0 yargs-parser@21.1.1: {} - yargs@16.2.0: + yargs@15.4.1: dependencies: - cliui: 7.0.4 - escalade: 3.2.0 + cliui: 6.0.0 + decamelize: 1.2.0 + find-up: 4.1.0 get-caller-file: 2.0.5 require-directory: 2.1.1 + require-main-filename: 2.0.0 + set-blocking: 2.0.0 string-width: 4.2.3 - y18n: 5.0.8 - yargs-parser: 20.2.9 + which-module: 2.0.1 + y18n: 4.0.3 + yargs-parser: 18.1.3 yargs@17.7.2: dependencies: @@ -8074,17 +7068,10 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 - yauzl@2.10.0: - dependencies: - buffer-crc32: 0.2.13 - fd-slicer: 1.1.0 - - yoctocolors@2.1.2: {} - - zod-to-json-schema@3.25.1(zod@4.3.6): + zod-to-json-schema@3.25.1(zod@4.4.3): dependencies: - zod: 4.3.6 + zod: 4.4.3 - zod@4.3.6: {} + zod@4.4.3: {} zwitch@2.0.4: {} diff --git a/src/messaging/channel-actions.ts b/src/messaging/channel-actions.ts index 4db41aee..89ce5db1 100644 --- a/src/messaging/channel-actions.ts +++ b/src/messaging/channel-actions.ts @@ -60,7 +60,7 @@ function describeDingTalkMessageTool(cfg: OpenClawConfig) { (config.accounts && Object.values(config.accounts).some((account) => account?.messageType === "card")); return { actions: ["send"] as const, - capabilities: hasCardMode ? (["cards"] as const) : [], + capabilities: hasCardMode ? (["presentation"] as const) : [], schema: null, }; } diff --git a/tests/unit/channel-actions-module.test.ts b/tests/unit/channel-actions-module.test.ts index 178612ff..90090738 100644 --- a/tests/unit/channel-actions-module.test.ts +++ b/tests/unit/channel-actions-module.test.ts @@ -61,7 +61,7 @@ describe("createDingTalkMessageActions", () => { } as any), ).toEqual({ actions: ["send"], - capabilities: ["cards"], + capabilities: ["presentation"], schema: null, }); }); diff --git a/tests/unit/message-actions.test.ts b/tests/unit/message-actions.test.ts index 84fd0639..c8e05335 100644 --- a/tests/unit/message-actions.test.ts +++ b/tests/unit/message-actions.test.ts @@ -143,7 +143,7 @@ describe('dingtalkPlugin.actions.send', () => { } as any), ).toEqual({ actions: ['send'], - capabilities: ['cards'], + capabilities: ['presentation'], schema: null, }); }); diff --git a/tests/unit/plugin-manifest.test.ts b/tests/unit/plugin-manifest.test.ts index 18122a10..67ab72ac 100644 --- a/tests/unit/plugin-manifest.test.ts +++ b/tests/unit/plugin-manifest.test.ts @@ -140,9 +140,9 @@ describe("plugin manifest channel metadata", () => { }; }>("package.json"); - expect(packageJson.peerDependencies?.openclaw).toBe(">=2026.3.28"); - expect(packageJson.openclaw?.compat?.pluginApi).toBe(">=2026.3.28"); - expect(packageJson.openclaw?.build?.openclawVersion).toBe("2026.3.28"); - expect(packageJson.openclaw?.install?.minHostVersion).toBe(">=2026.3.28"); + expect(packageJson.peerDependencies?.openclaw).toBe(">=2026.4.7"); + expect(packageJson.openclaw?.compat?.pluginApi).toBe(">=2026.4.7"); + expect(packageJson.openclaw?.build?.openclawVersion).toBe("2026.4.7"); + expect(packageJson.openclaw?.install?.minHostVersion).toBe(">=2026.4.7"); }); }); diff --git a/tests/unit/sdk-import-structure.test.ts b/tests/unit/sdk-import-structure.test.ts index 9e74be1e..91b2a6dd 100644 --- a/tests/unit/sdk-import-structure.test.ts +++ b/tests/unit/sdk-import-structure.test.ts @@ -89,7 +89,7 @@ describe("plugin-sdk import structure", () => { }; expect(packageJson.devDependencies?.openclaw).toBeUndefined(); expect(packageJson.peerDependencies?.openclaw).toBeDefined(); - expect(packageJson.peerDependencies?.openclaw).toBe(">=2026.3.28"); - expect(packageJson.openclaw?.install?.minHostVersion).toBe(">=2026.3.28"); + expect(packageJson.peerDependencies?.openclaw).toBe(">=2026.4.7"); + expect(packageJson.openclaw?.install?.minHostVersion).toBe(">=2026.4.7"); }); }); From 1ce36f01c6dcdea747c6dea5d6c7031e079b4e4b Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:02:17 +0800 Subject: [PATCH 17/44] feat(approval): add execApprovals config surface Add DingTalk execApprovals types and strict schema, and preserve the field through the default-account resolveDingTalkAccount path. --- src/config-schema.ts | 9 +++ src/config.ts | 1 + src/types.ts | 11 ++++ tests/unit/approval-config-schema.test.ts | 63 +++++++++++++++++++ tests/unit/config.test.ts | 74 ++++++++++++++++++++++- 5 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 tests/unit/approval-config-schema.test.ts diff --git a/src/config-schema.ts b/src/config-schema.ts index efcd8414..a4c04a98 100644 --- a/src/config-schema.ts +++ b/src/config-schema.ts @@ -10,6 +10,12 @@ const AckReactionSchema = z.union([ const CardStreamingModeSchema = z.enum(["off", "answer", "all"]); const ContextVisibilitySchema = z.enum(["all", "allowlist", "allowlist_quote"]); +export const ExecApprovalsConfigSchema = z + .object({ + enabled: z.union([z.boolean(), z.literal("auto")]).optional(), + approvers: z.array(z.string()).optional(), + }) + .strict(); /** * Runtime-parsed DingTalk account config. @@ -144,6 +150,9 @@ const DingTalkAccountConfigShape = { /** Enable the local feedback-learning loop for notes, reflections, and command-assisted learning. */ learningEnabled: z.boolean().optional(), + /** Native OpenClaw exec/plugin approval configuration. v1 intentionally rejects v2 future fields. */ + execApprovals: ExecApprovalsConfigSchema.optional(), + /** Automatically apply generated learning output into session notes or global rules when available. */ learningAutoApply: z.boolean().optional(), diff --git a/src/config.ts b/src/config.ts index 7daeaf73..7009d31d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -311,6 +311,7 @@ export function resolveDingTalkAccount( cardStreamInterval: dingtalk?.cardStreamInterval, aicardDegradeMs: dingtalk?.aicardDegradeMs, learningEnabled: dingtalk?.learningEnabled, + execApprovals: dingtalk?.execApprovals, learningAutoApply: dingtalk?.learningAutoApply, learningNoteTtlMs: dingtalk?.learningNoteTtlMs, convertMarkdownTables: dingtalk?.convertMarkdownTables, diff --git a/src/types.ts b/src/types.ts index f70779bb..7b6c6456 100644 --- a/src/types.ts +++ b/src/types.ts @@ -24,6 +24,13 @@ export type AckReactionMode = "off" | "emoji" | "kaomoji"; export type AckReactionConfigValue = string; export type CardStreamingMode = "off" | "answer" | "all"; export type ContextVisibilityMode = "all" | "allowlist" | "allowlist_quote"; +export type ApprovalDecision = "allow-once" | "allow-always" | "deny"; +export type ApprovalPhase = "pending" | "resolved" | "expired"; + +export interface ExecApprovalsConfig { + enabled?: boolean | "auto"; + approvers?: string[]; +} /** * DingTalk channel configuration (extends base OpenClaw config) @@ -87,6 +94,8 @@ export interface DingTalkConfig extends OpenClawConfig { aicardDegradeMs?: number; /** Enable local learning loop (events/reflections/session notes/global rules) */ learningEnabled?: boolean; + /** Native OpenClaw exec/plugin approval configuration for this DingTalk account. */ + execApprovals?: ExecApprovalsConfig; /** Auto-apply generated reflections into session notes/global rules (default false) */ learningAutoApply?: boolean; /** Session learning note TTL in milliseconds (default 6h) */ @@ -167,6 +176,8 @@ export interface DingTalkChannelConfig { aicardDegradeMs?: number; /** Enable local learning loop (events/reflections/session notes/global rules) */ learningEnabled?: boolean; + /** Native OpenClaw exec/plugin approval configuration for this DingTalk account. */ + execApprovals?: ExecApprovalsConfig; /** Auto-apply generated reflections into session notes/global rules (default false) */ learningAutoApply?: boolean; /** Session learning note TTL in milliseconds (default 6h) */ diff --git a/tests/unit/approval-config-schema.test.ts b/tests/unit/approval-config-schema.test.ts new file mode 100644 index 00000000..3b1b5644 --- /dev/null +++ b/tests/unit/approval-config-schema.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; +import { DingTalkConfigSchema } from "../../src/config-schema"; + +describe("DingTalkConfigSchema · execApprovals", () => { + it("accepts enabled=auto with approvers", () => { + const parsed = DingTalkConfigSchema.parse({ + clientId: "x", + clientSecret: "y", + execApprovals: { enabled: "auto", approvers: ["staff001"] }, + }); + + expect(parsed.execApprovals?.enabled).toBe("auto"); + expect(parsed.execApprovals?.approvers).toEqual(["staff001"]); + }); + + it("accepts enabled=true and enabled=false", () => { + expect(() => + DingTalkConfigSchema.parse({ + clientId: "x", + clientSecret: "y", + execApprovals: { enabled: true, approvers: [] }, + }), + ).not.toThrow(); + expect(() => + DingTalkConfigSchema.parse({ + clientId: "x", + clientSecret: "y", + execApprovals: { enabled: false }, + }), + ).not.toThrow(); + }); + + it("allows omitting execApprovals for backward compatibility", () => { + expect(() => DingTalkConfigSchema.parse({ clientId: "x", clientSecret: "y" })).not.toThrow(); + }); + + it("requires approver entries to be strings", () => { + expect(() => + DingTalkConfigSchema.parse({ + clientId: "x", + clientSecret: "y", + execApprovals: { approvers: [123 as unknown as string] }, + }), + ).toThrow(); + }); + + it("rejects v2 future fields target and ttlMs", () => { + expect(() => + DingTalkConfigSchema.parse({ + clientId: "x", + clientSecret: "y", + execApprovals: { approvers: ["staff001"], target: "dm" } as never, + }), + ).toThrow(); + expect(() => + DingTalkConfigSchema.parse({ + clientId: "x", + clientSecret: "y", + execApprovals: { approvers: ["staff001"], ttlMs: 600000 } as never, + }), + ).toThrow(); + }); +}); diff --git a/tests/unit/config.test.ts b/tests/unit/config.test.ts index 41c30c11..67661a9e 100644 --- a/tests/unit/config.test.ts +++ b/tests/unit/config.test.ts @@ -1,7 +1,12 @@ import { describe, expect, it, beforeEach, afterEach } from 'vitest'; import * as path from 'node:path'; import * as os from 'node:os'; -import { resolveGroupConfig, resolveRelativePath, stripTargetPrefix } from '../../src/config'; +import { + resolveDingTalkAccount, + resolveGroupConfig, + resolveRelativePath, + stripTargetPrefix, +} from '../../src/config'; describe('config helpers', () => { describe('stripTargetPrefix', () => { @@ -315,3 +320,70 @@ describe('config helpers', () => { }); }); }); + +describe('resolveDingTalkAccount · execApprovals field forwarding', () => { + it('exposes channel-level execApprovals on the default account', () => { + const cfg = { + channels: { + dingtalk: { + clientId: 'x', + clientSecret: 'y', + execApprovals: { enabled: 'auto', approvers: ['staff001'] }, + }, + }, + }; + + const account = resolveDingTalkAccount(cfg, undefined); + + expect(account.execApprovals?.approvers).toEqual(['staff001']); + expect(account.execApprovals?.enabled).toBe('auto'); + }); + + it('account override replaces channel-level approvers', () => { + const cfg = { + channels: { + dingtalk: { + clientId: 'x', + clientSecret: 'y', + execApprovals: { approvers: ['staffA'] }, + accounts: { + acme: { + clientId: 'x', + clientSecret: 'y', + execApprovals: { approvers: ['staffB'] }, + }, + }, + }, + }, + }; + + const acme = resolveDingTalkAccount(cfg, 'acme'); + + expect(acme.execApprovals?.approvers).toEqual(['staffB']); + }); + + it('account without execApprovals inherits the channel-level config', () => { + const cfg = { + channels: { + dingtalk: { + clientId: 'x', + clientSecret: 'y', + execApprovals: { approvers: ['staffA'] }, + accounts: { acme: { clientId: 'x', clientSecret: 'y' } }, + }, + }, + }; + + const acme = resolveDingTalkAccount(cfg, 'acme'); + + expect(acme.execApprovals?.approvers).toEqual(['staffA']); + }); + + it('leaves execApprovals undefined when the channel config omits it', () => { + const cfg = { channels: { dingtalk: { clientId: 'x', clientSecret: 'y' } } }; + + const account = resolveDingTalkAccount(cfg, undefined); + + expect(account.execApprovals).toBeUndefined(); + }); +}); From dac8af9b72220b40bc47e081f6744d2a41a9203a Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:04:03 +0800 Subject: [PATCH 18/44] feat(approval): add approval config and command parser helpers Add normalized approver config helpers and the DingTalk /approve parser aligned with the upstream approve command aliases. --- src/approval/approval-command-parser.ts | 45 ++++++++++ src/approval/approval-config.ts | 74 +++++++++++++++++ tests/unit/approval-command-parser.test.ts | 96 ++++++++++++++++++++++ tests/unit/approval-config.test.ts | 88 ++++++++++++++++++++ 4 files changed, 303 insertions(+) create mode 100644 src/approval/approval-command-parser.ts create mode 100644 src/approval/approval-config.ts create mode 100644 tests/unit/approval-command-parser.test.ts create mode 100644 tests/unit/approval-config.test.ts diff --git a/src/approval/approval-command-parser.ts b/src/approval/approval-command-parser.ts new file mode 100644 index 00000000..fec8b5e1 --- /dev/null +++ b/src/approval/approval-command-parser.ts @@ -0,0 +1,45 @@ +import type { ApprovalDecision } from "../types"; + +const APPROVE_COMMAND_RE = /^\/?approve(?:\s|$)/i; + +const DECISION_ALIASES: Record = { + allow: "allow-once", + once: "allow-once", + "allow-once": "allow-once", + allowonce: "allow-once", + always: "allow-always", + "allow-always": "allow-always", + allowalways: "allow-always", + deny: "deny", + reject: "deny", + block: "deny", +}; + +export interface ParsedApproveCommand { + approvalId: string; + decision: ApprovalDecision; +} + +export function parseApproveCommand(text: string): ParsedApproveCommand | null { + const trimmed = text.trim(); + if (!APPROVE_COMMAND_RE.test(trimmed)) { + return null; + } + + const tokens = trimmed.split(/\s+/); + if (tokens.length !== 3) { + return null; + } + + const [, first, second] = tokens; + const firstDecision = DECISION_ALIASES[first.toLowerCase()]; + const secondDecision = DECISION_ALIASES[second.toLowerCase()]; + + if (firstDecision && !secondDecision) { + return { approvalId: second, decision: firstDecision }; + } + if (secondDecision && !firstDecision) { + return { approvalId: first, decision: secondDecision }; + } + return null; +} diff --git a/src/approval/approval-config.ts b/src/approval/approval-config.ts new file mode 100644 index 00000000..c0a9947b --- /dev/null +++ b/src/approval/approval-config.ts @@ -0,0 +1,74 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import { getConfig } from "../config"; + +const DINGTALK_PREFIX_RE = /^(dingtalk|dd|ding):/i; + +export interface ApprovalConfigQuery { + cfg: OpenClawConfig; + accountId: string; +} + +export interface ResolvedExecApprovalsConfig { + enabled: boolean | "auto" | undefined; + approvers: string[]; + isNativeDeliveryEnabled: boolean; +} + +function normalizeStaffId(raw: string): string { + return raw.replace(DINGTALK_PREFIX_RE, "").trim(); +} + +export function listExecApprovers({ cfg, accountId }: ApprovalConfigQuery): string[] { + const account = getConfig(cfg, accountId); + const configuredApprovers = account.execApprovals?.approvers ?? []; + const source = + configuredApprovers.length > 0 ? configuredApprovers : (cfg.commands?.ownerAllowFrom ?? []); + const seen = new Set(); + const approvers: string[] = []; + + for (const item of source) { + if (typeof item !== "string") { + continue; + } + const normalized = normalizeStaffId(item); + if (!normalized || seen.has(normalized)) { + continue; + } + seen.add(normalized); + approvers.push(normalized); + } + + return approvers; +} + +export function getExecApprovalsConfig( + query: ApprovalConfigQuery, +): ResolvedExecApprovalsConfig { + const account = getConfig(query.cfg, query.accountId); + const enabled = account.execApprovals?.enabled; + const approvers = listExecApprovers(query); + return { + enabled, + approvers, + isNativeDeliveryEnabled: enabled === false ? false : approvers.length > 0, + }; +} + +export function isExecAuthorizedSender({ + cfg, + accountId, + senderId, +}: ApprovalConfigQuery & { senderId: string }): boolean { + const normalizedSender = normalizeStaffId(senderId); + return listExecApprovers({ cfg, accountId }).includes(normalizedSender); +} + +export function isPluginAuthorizedSender( + query: ApprovalConfigQuery & { senderId: string }, +): boolean { + return isExecAuthorizedSender(query); +} + +export function resolveNativeDeliveryMode(_query: ApprovalConfigQuery): "channel" { + return "channel"; +} diff --git a/tests/unit/approval-command-parser.test.ts b/tests/unit/approval-command-parser.test.ts new file mode 100644 index 00000000..2bbb66e9 --- /dev/null +++ b/tests/unit/approval-command-parser.test.ts @@ -0,0 +1,96 @@ +import { describe, expect, it } from "vitest"; +import { parseApproveCommand } from "../../src/approval/approval-command-parser"; + +const ALIAS_ALLOW_ONCE = ["allow", "once", "allow-once", "allowonce"] as const; +const ALIAS_ALLOW_ALWAYS = ["always", "allow-always", "allowalways"] as const; +const ALIAS_DENY = ["deny", "reject", "block"] as const; + +describe("parseApproveCommand", () => { + describe("order A: /approve ", () => { + for (const alias of ALIAS_ALLOW_ONCE) { + it(`/approve abc ${alias} -> allow-once`, () => { + expect(parseApproveCommand(`/approve abc ${alias}`)).toEqual({ + approvalId: "abc", + decision: "allow-once", + }); + }); + } + + for (const alias of ALIAS_ALLOW_ALWAYS) { + it(`/approve abc ${alias} -> allow-always`, () => { + expect(parseApproveCommand(`/approve abc ${alias}`)).toEqual({ + approvalId: "abc", + decision: "allow-always", + }); + }); + } + + for (const alias of ALIAS_DENY) { + it(`/approve abc ${alias} -> deny`, () => { + expect(parseApproveCommand(`/approve abc ${alias}`)).toEqual({ + approvalId: "abc", + decision: "deny", + }); + }); + } + }); + + describe("order B: /approve ", () => { + for (const alias of ALIAS_ALLOW_ONCE) { + it(`/approve ${alias} abc -> allow-once`, () => { + expect(parseApproveCommand(`/approve ${alias} abc`)).toEqual({ + approvalId: "abc", + decision: "allow-once", + }); + }); + } + + for (const alias of ALIAS_ALLOW_ALWAYS) { + it(`/approve ${alias} abc -> allow-always`, () => { + expect(parseApproveCommand(`/approve ${alias} abc`)).toEqual({ + approvalId: "abc", + decision: "allow-always", + }); + }); + } + + for (const alias of ALIAS_DENY) { + it(`/approve ${alias} abc -> deny`, () => { + expect(parseApproveCommand(`/approve ${alias} abc`)).toEqual({ + approvalId: "abc", + decision: "deny", + }); + }); + } + }); + + it("accepts bare approve without a leading slash", () => { + expect(parseApproveCommand("approve abc once")).toEqual({ + approvalId: "abc", + decision: "allow-once", + }); + }); + + it("matches decision aliases case-insensitively", () => { + expect(parseApproveCommand("/approve abc ALLOW")).toEqual({ + approvalId: "abc", + decision: "allow-once", + }); + }); + + it("preserves approvalId casing", () => { + expect(parseApproveCommand("/approve ABC-123 deny")?.approvalId).toBe("ABC-123"); + }); + + it("returns null for malformed commands", () => { + expect(parseApproveCommand("/approve")).toBeNull(); + expect(parseApproveCommand("/approve abc")).toBeNull(); + expect(parseApproveCommand("/approve abc xyz")).toBeNull(); + expect(parseApproveCommand("approve foo bar baz qux")).toBeNull(); + expect(parseApproveCommand("")).toBeNull(); + }); + + it("keeps the alias count aligned with upstream commands-approve", () => { + expect(ALIAS_ALLOW_ONCE.length + ALIAS_ALLOW_ALWAYS.length + ALIAS_DENY.length).toBe(10); + }); +}); diff --git a/tests/unit/approval-config.test.ts b/tests/unit/approval-config.test.ts new file mode 100644 index 00000000..d0485f8c --- /dev/null +++ b/tests/unit/approval-config.test.ts @@ -0,0 +1,88 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import { describe, expect, it } from "vitest"; +import { + getExecApprovalsConfig, + isExecAuthorizedSender, + isPluginAuthorizedSender, + listExecApprovers, + resolveNativeDeliveryMode, +} from "../../src/approval/approval-config"; + +const cfg = ( + approvers: string[], + opts: { ownerAllowFrom?: string[]; enabled?: boolean | "auto" } = {}, +): OpenClawConfig => + ({ + channels: { + dingtalk: { + clientId: "x", + clientSecret: "y", + execApprovals: { enabled: opts.enabled ?? "auto", approvers }, + }, + }, + commands: { ownerAllowFrom: opts.ownerAllowFrom }, + }) as unknown as OpenClawConfig; + +describe("approval-config", () => { + it("returns deduplicated normalized staff IDs", () => { + const c = cfg(["staff001", "dingtalk:staff002", "DD:staff003", "ding:staff001"]); + expect(listExecApprovers({ cfg: c, accountId: "default" })).toEqual([ + "staff001", + "staff002", + "staff003", + ]); + }); + + it("falls back to commands.ownerAllowFrom when approvers are empty", () => { + const c = cfg([], { ownerAllowFrom: ["staff999"] }); + expect(listExecApprovers({ cfg: c, accountId: "default" })).toEqual(["staff999"]); + }); + + it("authorizes listed staff IDs for exec approval", () => { + const c = cfg(["staff001"]); + expect(isExecAuthorizedSender({ cfg: c, accountId: "default", senderId: "staff001" })).toBe( + true, + ); + expect(isExecAuthorizedSender({ cfg: c, accountId: "default", senderId: "staff999" })).toBe( + false, + ); + }); + + it("accepts dingtalk/dd/ding prefixes in senderId", () => { + const c = cfg(["staff001"]); + expect( + isExecAuthorizedSender({ cfg: c, accountId: "default", senderId: "dingtalk:staff001" }), + ).toBe(true); + }); + + it("uses exec authorization for plugin approvals in v1", () => { + const c = cfg(["staff001"]); + expect(isPluginAuthorizedSender({ cfg: c, accountId: "default", senderId: "staff001" })).toBe( + true, + ); + }); + + it("preserves explicit enabled=false even when approvers are configured", () => { + const c = cfg(["staff001"], { enabled: false }); + expect(getExecApprovalsConfig({ cfg: c, accountId: "default" }).enabled).toBe(false); + }); + + it("enables native delivery for enabled=auto when approvers exist", () => { + const c = cfg(["staff001"]); + expect(getExecApprovalsConfig({ cfg: c, accountId: "default" }).isNativeDeliveryEnabled).toBe( + true, + ); + }); + + it("disables native delivery for enabled=auto when approvers are empty", () => { + const c = cfg([]); + expect(getExecApprovalsConfig({ cfg: c, accountId: "default" }).isNativeDeliveryEnabled).toBe( + false, + ); + }); + + it('uses "channel" as the v1 native delivery mode', () => { + const c = cfg(["staff001"]); + expect(resolveNativeDeliveryMode({ cfg: c, accountId: "default" })).toBe("channel"); + }); +}); From 1723940bfd49419b42c4dabfa04ba2f24b46ea22 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:05:02 +0800 Subject: [PATCH 19/44] feat(approval): add DingTalk origin target resolver Reuse the OpenClaw native origin-target helper and keep DingTalk-specific user/group target normalization local. --- src/approval/approval-target-resolver.ts | 63 ++++++++++ tests/unit/approval-target-resolver.test.ts | 126 ++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 src/approval/approval-target-resolver.ts create mode 100644 tests/unit/approval-target-resolver.test.ts diff --git a/src/approval/approval-target-resolver.ts b/src/approval/approval-target-resolver.ts new file mode 100644 index 00000000..fc087fb0 --- /dev/null +++ b/src/approval/approval-target-resolver.ts @@ -0,0 +1,63 @@ +import { createChannelNativeOriginTargetResolver } from "openclaw/plugin-sdk/approval-native-runtime"; +import type { + ExecApprovalRequest, + PluginApprovalRequest, +} from "openclaw/plugin-sdk/approval-runtime"; + +type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest; + +export type DingTalkApprovalTarget = { + to: string; + accountId?: string | null; + threadId?: string | number | null; +}; + +export function normalizeApprovalTargetTo(raw: string): string { + const trimmed = raw.trim(); + if (/^(user|group):/i.test(trimmed)) { + return trimmed; + } + if (/^cid/i.test(trimmed)) { + return `group:${trimmed}`; + } + return `user:${trimmed}`; +} + +function resolveTurnSourceTarget(request: ApprovalRequest): DingTalkApprovalTarget | null { + const payload = request.request; + if (String(payload.turnSourceChannel || "").toLowerCase() !== "dingtalk") { + return null; + } + if (!payload.turnSourceTo) { + return null; + } + return { + to: normalizeApprovalTargetTo(payload.turnSourceTo), + accountId: payload.turnSourceAccountId ?? null, + threadId: payload.turnSourceThreadId ?? null, + }; +} + +function resolveSessionTarget( + sessionTarget: { to: string; accountId?: string | null; threadId?: string | number | null }, +): DingTalkApprovalTarget | null { + if (!sessionTarget.to) { + return null; + } + return { + to: normalizeApprovalTargetTo(sessionTarget.to), + accountId: sessionTarget.accountId ?? null, + threadId: sessionTarget.threadId ?? null, + }; +} + +export const resolveDingTalkOriginTarget = + createChannelNativeOriginTargetResolver({ + channel: "dingtalk", + resolveTurnSourceTarget, + resolveSessionTarget, + normalizeTarget: (target) => ({ + ...target, + to: normalizeApprovalTargetTo(target.to), + }), + }); diff --git a/tests/unit/approval-target-resolver.test.ts b/tests/unit/approval-target-resolver.test.ts new file mode 100644 index 00000000..8889996b --- /dev/null +++ b/tests/unit/approval-target-resolver.test.ts @@ -0,0 +1,126 @@ +import { describe, expect, it } from "vitest"; +import { + normalizeApprovalTargetTo, + resolveDingTalkOriginTarget, +} from "../../src/approval/approval-target-resolver"; + +const request = ( + payload: Partial<{ + turnSourceChannel: string | null; + turnSourceTo: string | null; + turnSourceAccountId: string | null; + turnSourceThreadId: string | number | null; + sessionKey: string | null; + }>, +) => + ({ + id: "approval-1", + createdAtMs: 0, + expiresAtMs: 0, + request: payload, + }) as never; + +describe("normalizeApprovalTargetTo", () => { + it("keeps group-prefixed targets unchanged", () => { + expect(normalizeApprovalTargetTo("group:cidxxxxx")).toBe("group:cidxxxxx"); + }); + + it("keeps user-prefixed targets unchanged", () => { + expect(normalizeApprovalTargetTo("user:staff001")).toBe("user:staff001"); + }); + + it("adds group prefix for bare cid targets", () => { + expect(normalizeApprovalTargetTo("cidxxxxx")).toBe("group:cidxxxxx"); + }); + + it("adds user prefix for bare staff IDs", () => { + expect(normalizeApprovalTargetTo("staff001")).toBe("user:staff001"); + }); +}); + +describe("resolveDingTalkOriginTarget", () => { + it("returns null for non-DingTalk turn sources", () => { + const resolved = resolveDingTalkOriginTarget({ + cfg: {} as never, + accountId: "default", + request: request({ turnSourceChannel: "discord", turnSourceTo: "group:cidxxx" }), + }); + + expect(resolved).toBeNull(); + }); + + it("returns null when turnSourceTo is empty", () => { + const resolved = resolveDingTalkOriginTarget({ + cfg: {} as never, + accountId: "default", + request: request({ turnSourceChannel: "dingtalk", turnSourceTo: null }), + }); + + expect(resolved).toBeNull(); + }); + + it("resolves prefixed DingTalk group targets", () => { + const resolved = resolveDingTalkOriginTarget({ + cfg: {} as never, + accountId: "default", + request: request({ turnSourceChannel: "dingtalk", turnSourceTo: "group:cidxxx" }), + }); + + expect(resolved).toEqual(expect.objectContaining({ to: "group:cidxxx" })); + }); + + it("normalizes bare cid targets to group targets", () => { + const resolved = resolveDingTalkOriginTarget({ + cfg: {} as never, + accountId: "default", + request: request({ turnSourceChannel: "dingtalk", turnSourceTo: "cidxxx" }), + }); + + expect(resolved?.to).toBe("group:cidxxx"); + }); + + it("normalizes bare staff IDs to user targets", () => { + const resolved = resolveDingTalkOriginTarget({ + cfg: {} as never, + accountId: "default", + request: request({ turnSourceChannel: "dingtalk", turnSourceTo: "staff001" }), + }); + + expect(resolved?.to).toBe("user:staff001"); + }); + + it("returns null when turnSourceAccountId does not match the input accountId", () => { + const resolved = resolveDingTalkOriginTarget({ + cfg: {} as never, + accountId: "acme", + request: request({ + turnSourceChannel: "dingtalk", + turnSourceTo: "group:cidxxx", + turnSourceAccountId: "other", + }), + }); + + expect(resolved).toBeNull(); + }); + + it("preserves turnSourceAccountId and turnSourceThreadId", () => { + const resolved = resolveDingTalkOriginTarget({ + cfg: {} as never, + accountId: "acme", + request: request({ + turnSourceChannel: "dingtalk", + turnSourceTo: "group:cidxxx", + turnSourceAccountId: "acme", + turnSourceThreadId: "thread-xyz", + }), + }); + + expect(resolved).toEqual( + expect.objectContaining({ + to: "group:cidxxx", + accountId: "acme", + threadId: "thread-xyz", + }), + ); + }); +}); From 8172a1aaa14242c4f2c5f0185f449b742cd86063 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:06:35 +0800 Subject: [PATCH 20/44] feat(approval): resolve approvals through OpenClaw gateway Add the DingTalk approval resolver, authorization method derivation, and gateway error classification. Exec resolution follows the current OpenClaw SDK contract by omitting resolveMethod. --- src/approval/approval-resolver.ts | 137 +++++++++++++++++ tests/unit/approval-resolver.test.ts | 214 +++++++++++++++++++++++++++ 2 files changed, 351 insertions(+) create mode 100644 src/approval/approval-resolver.ts create mode 100644 tests/unit/approval-resolver.test.ts diff --git a/src/approval/approval-resolver.ts b/src/approval/approval-resolver.ts new file mode 100644 index 00000000..c4839724 --- /dev/null +++ b/src/approval/approval-resolver.ts @@ -0,0 +1,137 @@ +import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import type { ApprovalDecision, Logger } from "../types"; +import { isExecAuthorizedSender, isPluginAuthorizedSender } from "./approval-config"; + +export type ResolverReason = + | "unauthorized" + | "already-resolved" + | "not-found" + | "invalid-decision" + | "gateway-error"; + +export type ResolverResult = + | { ok: true } + | { + ok: false; + reason: ResolverReason; + error?: unknown; + allowedDecisions?: string[]; + }; + +export interface ResolveApprovalInput { + cfg: OpenClawConfig; + accountId: string; + approvalId: string; + decision: ApprovalDecision; + senderId: string; + log?: Logger; +} + +type GatewayErrorLike = { + gatewayCode?: unknown; + details?: { + reason?: unknown; + allowedDecisions?: unknown; + }; +}; + +export function isInvalidApprovalDecisionError(error: unknown): boolean { + if (!error || typeof error !== "object") { + return false; + } + const candidate = error as GatewayErrorLike; + if (candidate.gatewayCode !== "INVALID_REQUEST") { + return false; + } + const details = candidate.details; + if (!details || typeof details !== "object") { + return false; + } + return ( + details.reason === "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" || + Array.isArray(details.allowedDecisions) + ); +} + +function extractAllowedDecisions(error: unknown): string[] | undefined { + const details = (error as GatewayErrorLike | null)?.details; + if (!Array.isArray(details?.allowedDecisions)) { + return undefined; + } + return details.allowedDecisions.filter((entry): entry is string => typeof entry === "string"); +} + +function deriveGatewayParams(params: { + approvalId: string; + execAuthorized: boolean; + pluginAuthorized: boolean; +}): + | { resolveMethod?: "plugin"; allowPluginFallback?: boolean } + | null { + if (params.approvalId.startsWith("plugin:")) { + return { resolveMethod: "plugin" }; + } + if (params.execAuthorized && params.pluginAuthorized) { + return { allowPluginFallback: true }; + } + if (params.pluginAuthorized) { + return { resolveMethod: "plugin" }; + } + if (params.execAuthorized) { + return { allowPluginFallback: false }; + } + return null; +} + +export async function resolveApproval(input: ResolveApprovalInput): Promise { + const execAuthorized = isExecAuthorizedSender(input); + const pluginAuthorized = isPluginAuthorizedSender(input); + const gatewayParams = deriveGatewayParams({ + approvalId: input.approvalId, + execAuthorized, + pluginAuthorized, + }); + + if (!gatewayParams) { + input.log?.info?.( + `[DingTalk][Approval] unauthorized sender=${input.senderId} approvalId=${input.approvalId}`, + ); + return { ok: false, reason: "unauthorized" }; + } + + try { + await resolveApprovalOverGateway({ + cfg: input.cfg, + approvalId: input.approvalId, + decision: input.decision, + senderId: input.senderId, + clientDisplayName: "DingTalk", + ...gatewayParams, + }); + return { ok: true }; + } catch (error) { + const gatewayCode = (error as GatewayErrorLike | null)?.gatewayCode; + if (gatewayCode === "APPROVAL_NOT_FOUND") { + return { ok: false, reason: "not-found", error }; + } + if (gatewayCode === "APPROVAL_ALREADY_RESOLVED") { + return { ok: false, reason: "already-resolved", error }; + } + if (isInvalidApprovalDecisionError(error)) { + return { + ok: false, + reason: "invalid-decision", + error, + allowedDecisions: extractAllowedDecisions(error), + }; + } + + input.log?.warn?.( + `[DingTalk][Approval] gateway-error approvalId=${input.approvalId} err=${String( + (error as Error | null)?.message ?? error, + )}`, + ); + return { ok: false, reason: "gateway-error", error }; + } +} diff --git a/tests/unit/approval-resolver.test.ts b/tests/unit/approval-resolver.test.ts new file mode 100644 index 00000000..701923ff --- /dev/null +++ b/tests/unit/approval-resolver.test.ts @@ -0,0 +1,214 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { isInvalidApprovalDecisionError, resolveApproval } from "../../src/approval/approval-resolver"; + +vi.mock("openclaw/plugin-sdk/approval-gateway-runtime", () => ({ + resolveApprovalOverGateway: vi.fn(), +})); + +vi.mock("../../src/approval/approval-config", () => ({ + isExecAuthorizedSender: vi.fn(() => true), + isPluginAuthorizedSender: vi.fn(() => true), +})); + +const { resolveApprovalOverGateway } = await import( + "openclaw/plugin-sdk/approval-gateway-runtime" +); +const { isExecAuthorizedSender, isPluginAuthorizedSender } = await import( + "../../src/approval/approval-config" +); + +const mockGateway = vi.mocked(resolveApprovalOverGateway); +const mockExecAuth = vi.mocked(isExecAuthorizedSender); +const mockPluginAuth = vi.mocked(isPluginAuthorizedSender); + +const base = { + cfg: {} as never, + accountId: "default", + senderId: "staffA", + log: undefined, +}; + +describe("approval-resolver · method derivation", () => { + beforeEach(() => { + mockGateway.mockReset(); + mockExecAuth.mockReset().mockReturnValue(true); + mockPluginAuth.mockReset().mockReturnValue(true); + }); + + it("uses plugin resolveMethod for plugin-prefixed approval IDs", async () => { + mockGateway.mockResolvedValue(undefined); + + await resolveApproval({ ...base, approvalId: "plugin:xyz", decision: "allow-once" }); + + expect(mockGateway).toHaveBeenCalledWith(expect.objectContaining({ resolveMethod: "plugin" })); + }); + + it("omits resolveMethod for exec and enables plugin fallback when both auth checks pass", async () => { + mockGateway.mockResolvedValue(undefined); + + await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + + expect(mockGateway).toHaveBeenCalledWith( + expect.objectContaining({ allowPluginFallback: true }), + ); + expect(mockGateway.mock.calls[0][0]).not.toHaveProperty("resolveMethod"); + }); + + it("uses plugin when only plugin authorization passes", async () => { + mockGateway.mockResolvedValue(undefined); + mockExecAuth.mockReturnValue(false); + mockPluginAuth.mockReturnValue(true); + + await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + + expect(mockGateway).toHaveBeenCalledWith(expect.objectContaining({ resolveMethod: "plugin" })); + }); + + it("uses exec without plugin fallback when only exec authorization passes", async () => { + mockGateway.mockResolvedValue(undefined); + mockExecAuth.mockReturnValue(true); + mockPluginAuth.mockReturnValue(false); + + await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + + expect(mockGateway).toHaveBeenCalledWith( + expect.objectContaining({ allowPluginFallback: false }), + ); + expect(mockGateway.mock.calls[0][0]).not.toHaveProperty("resolveMethod"); + }); + + it("returns unauthorized and skips the gateway when neither auth check passes", async () => { + mockExecAuth.mockReturnValue(false); + mockPluginAuth.mockReturnValue(false); + + const result = await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + + expect(result).toEqual({ ok: false, reason: "unauthorized" }); + expect(mockGateway).not.toHaveBeenCalled(); + }); +}); + +describe("approval-resolver · error classification", () => { + beforeEach(() => { + mockGateway.mockReset(); + mockExecAuth.mockReset().mockReturnValue(true); + mockPluginAuth.mockReset().mockReturnValue(true); + }); + + it("maps APPROVAL_NOT_FOUND to not-found", async () => { + mockGateway.mockRejectedValue( + Object.assign(new Error("not found"), { gatewayCode: "APPROVAL_NOT_FOUND" }), + ); + + const result = await resolveApproval({ ...base, approvalId: "abc", decision: "deny" }); + + expect(result).toEqual(expect.objectContaining({ ok: false, reason: "not-found" })); + }); + + it("maps APPROVAL_ALREADY_RESOLVED to already-resolved", async () => { + mockGateway.mockRejectedValue( + Object.assign(new Error("already"), { gatewayCode: "APPROVAL_ALREADY_RESOLVED" }), + ); + + const result = await resolveApproval({ ...base, approvalId: "abc", decision: "deny" }); + + expect(result).toEqual(expect.objectContaining({ ok: false, reason: "already-resolved" })); + }); + + it("maps exec allow-always unavailability to invalid-decision", async () => { + mockGateway.mockRejectedValue( + Object.assign(new Error("invalid"), { + gatewayCode: "INVALID_REQUEST", + details: { reason: "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" }, + }), + ); + + const result = await resolveApproval({ + ...base, + approvalId: "abc", + decision: "allow-always", + }); + + expect(result).toEqual(expect.objectContaining({ ok: false, reason: "invalid-decision" })); + }); + + it("maps plugin allowedDecisions errors to invalid-decision and preserves decisions", async () => { + mockGateway.mockRejectedValue( + Object.assign(new Error("invalid"), { + gatewayCode: "INVALID_REQUEST", + details: { allowedDecisions: ["allow-once", "deny"] }, + }), + ); + + const result = await resolveApproval({ + ...base, + approvalId: "plugin:p", + decision: "allow-always", + }); + + expect(result).toEqual( + expect.objectContaining({ + ok: false, + reason: "invalid-decision", + allowedDecisions: ["allow-once", "deny"], + }), + ); + }); + + it("maps other INVALID_REQUEST errors to gateway-error", async () => { + mockGateway.mockRejectedValue( + Object.assign(new Error("misc"), { + gatewayCode: "INVALID_REQUEST", + details: { other: true }, + }), + ); + + const result = await resolveApproval({ ...base, approvalId: "abc", decision: "deny" }); + + expect(result).toEqual(expect.objectContaining({ ok: false, reason: "gateway-error" })); + }); + + it("maps arbitrary errors to gateway-error", async () => { + mockGateway.mockRejectedValue(new Error("network down")); + + const result = await resolveApproval({ ...base, approvalId: "abc", decision: "deny" }); + + expect(result).toEqual(expect.objectContaining({ ok: false, reason: "gateway-error" })); + }); + + it("returns ok=true when the gateway resolves", async () => { + mockGateway.mockResolvedValue(undefined); + + const result = await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + + expect(result).toEqual({ ok: true }); + }); +}); + +describe("isInvalidApprovalDecisionError", () => { + it("recognizes exec invalid-decision shape", () => { + expect( + isInvalidApprovalDecisionError({ + gatewayCode: "INVALID_REQUEST", + details: { reason: "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" }, + }), + ).toBe(true); + }); + + it("recognizes plugin invalid-decision shape", () => { + expect( + isInvalidApprovalDecisionError({ + gatewayCode: "INVALID_REQUEST", + details: { allowedDecisions: ["allow-once"] }, + }), + ).toBe(true); + }); + + it("does not recognize INVALID_REQUEST without relevant details", () => { + expect(isInvalidApprovalDecisionError({ gatewayCode: "INVALID_REQUEST" })).toBe(false); + }); + + it("does not recognize non-INVALID_REQUEST errors", () => { + expect(isInvalidApprovalDecisionError({ gatewayCode: "NETWORK_ERROR" })).toBe(false); + }); +}); From de478868d11823a053a0ea55c73a5b74ab4d87ce Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:08:03 +0800 Subject: [PATCH 21/44] feat(card): locate active card runs by session Add approval-oriented active card helpers using the real DingTalk AI card status constants and the existing in-process card-run registry. --- src/card/card-run-registry.ts | 23 +++- tests/unit/card-run-registry-approval.test.ts | 102 ++++++++++++++++++ 2 files changed, 124 insertions(+), 1 deletion(-) create mode 100644 tests/unit/card-run-registry-approval.test.ts diff --git a/src/card/card-run-registry.ts b/src/card/card-run-registry.ts index 0af7acf2..89306d37 100644 --- a/src/card/card-run-registry.ts +++ b/src/card/card-run-registry.ts @@ -8,7 +8,7 @@ * routing per card callback when using the stop button feature. */ import type { CardDraftController } from "../card-draft-controller"; -import type { AICardInstance } from "../types"; +import { AICardStatus, type AICardInstance } from "../types"; export interface CardRunRecord { outTrackId: string; @@ -92,6 +92,27 @@ export function resolveCardRun(outTrackId: string): CardRunRecord | null { return records.get(outTrackId.trim()) ?? null; } +export function isActiveCardRun(record: CardRunRecord): boolean { + const state = record.card?.state; + return state === AICardStatus.PROCESSING || state === AICardStatus.INPUTING; +} + +export function resolveActiveCardRunBySession( + accountId: string, + sessionKey: string, +): CardRunRecord | null { + let latest: CardRunRecord | null = null; + for (const record of records.values()) { + if (record.accountId !== accountId) { continue; } + if (record.sessionKey !== sessionKey) { continue; } + if (!isActiveCardRun(record)) { continue; } + if (!latest || record.registeredAt > latest.registeredAt) { + latest = record; + } + } + return latest; +} + /** * Find the most recently registered card run for a given account + conversation. * Uses case-insensitive match of the conversationId within sessionKey. diff --git a/tests/unit/card-run-registry-approval.test.ts b/tests/unit/card-run-registry-approval.test.ts new file mode 100644 index 00000000..6a6a9387 --- /dev/null +++ b/tests/unit/card-run-registry-approval.test.ts @@ -0,0 +1,102 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { + clearCardRunRegistryForTest, + isActiveCardRun, + registerCardRun, + resolveActiveCardRunBySession, + type CardRunRecord, +} from "../../src/card/card-run-registry"; +import { AICardStatus, type AICardState } from "../../src/types"; + +const cardWithState = (state: AICardState) => + ({ state }) as unknown as NonNullable; + +function makeRecord(state?: AICardState): CardRunRecord { + return { + outTrackId: "out-1", + accountId: "default", + sessionKey: "session-1", + agentId: "agent-default", + card: state ? cardWithState(state) : undefined, + registeredAt: Date.now(), + }; +} + +function register( + outTrackId: string, + options: { + accountId?: string; + sessionKey: string; + agentId?: string; + state?: AICardState; + registeredAt?: number; + }, +): void { + registerCardRun(outTrackId, { + accountId: options.accountId ?? "default", + sessionKey: options.sessionKey, + agentId: options.agentId ?? "agent-default", + card: options.state ? cardWithState(options.state) : undefined, + registeredAt: options.registeredAt, + }); +} + +describe("card-run-registry · approval helpers", () => { + beforeEach(() => clearCardRunRegistryForTest()); + + it("treats PROCESSING and INPUTING card runs as active", () => { + expect(isActiveCardRun(makeRecord(AICardStatus.PROCESSING))).toBe(true); + expect(isActiveCardRun(makeRecord(AICardStatus.INPUTING))).toBe(true); + }); + + it("treats terminal card states as inactive", () => { + for (const state of [AICardStatus.FINISHED, AICardStatus.STOPPED, AICardStatus.FAILED]) { + expect(isActiveCardRun(makeRecord(state))).toBe(false); + } + }); + + it("treats records without a card as inactive", () => { + expect(isActiveCardRun(makeRecord())).toBe(false); + }); + + it("finds an active record by accountId and sessionKey", () => { + register("out-active", { sessionKey: "session-A", state: AICardStatus.INPUTING }); + + expect(resolveActiveCardRunBySession("default", "session-A")?.outTrackId).toBe("out-active"); + }); + + it("returns null when accountId does not match", () => { + register("out-active", { + accountId: "other", + sessionKey: "session-A", + state: AICardStatus.INPUTING, + }); + + expect(resolveActiveCardRunBySession("default", "session-A")).toBeNull(); + }); + + it("returns null for terminal card states", () => { + register("out-done", { sessionKey: "session-A", state: AICardStatus.FINISHED }); + + expect(resolveActiveCardRunBySession("default", "session-A")).toBeNull(); + }); + + it("returns null for missing session keys", () => { + expect(resolveActiveCardRunBySession("default", "missing")).toBeNull(); + }); + + it("returns the latest active record for duplicate account/session pairs", () => { + register("out-old", { + sessionKey: "session-A", + state: AICardStatus.INPUTING, + registeredAt: 1000, + }); + register("out-new", { + sessionKey: "session-A", + state: AICardStatus.INPUTING, + registeredAt: 2000, + }); + + expect(resolveActiveCardRunBySession("default", "session-A")?.outTrackId).toBe("out-new"); + }); +}); From 12e929ecc0fa507faaaeffb060c010c30c493730 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:08:43 +0800 Subject: [PATCH 22/44] feat(approval): locate active AI card for approvals Add a narrow session-based card locator over the card-run registry for native approval presentation routing. --- src/approval/approval-card-locator.ts | 24 ++++++++++++ tests/unit/approval-card-locator.test.ts | 47 ++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 src/approval/approval-card-locator.ts create mode 100644 tests/unit/approval-card-locator.test.ts diff --git a/src/approval/approval-card-locator.ts b/src/approval/approval-card-locator.ts new file mode 100644 index 00000000..ce1b4698 --- /dev/null +++ b/src/approval/approval-card-locator.ts @@ -0,0 +1,24 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import { resolveActiveCardRunBySession } from "../card/card-run-registry"; + +export interface FindActiveAgentCardInput { + cfg: OpenClawConfig; + accountId: string; + sessionKey: string; +} + +export interface ActiveAgentCardLocation { + outTrackId: string; + sessionKey: string; +} + +export function findActiveAgentCard(input: FindActiveAgentCardInput): ActiveAgentCardLocation | null { + if (!input.sessionKey) { + return null; + } + const record = resolveActiveCardRunBySession(input.accountId, input.sessionKey); + if (!record) { + return null; + } + return { outTrackId: record.outTrackId, sessionKey: record.sessionKey }; +} diff --git a/tests/unit/approval-card-locator.test.ts b/tests/unit/approval-card-locator.test.ts new file mode 100644 index 00000000..68df94d2 --- /dev/null +++ b/tests/unit/approval-card-locator.test.ts @@ -0,0 +1,47 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { findActiveAgentCard } from "../../src/approval/approval-card-locator"; + +vi.mock("../../src/card/card-run-registry", () => ({ + resolveActiveCardRunBySession: vi.fn(), +})); + +const { resolveActiveCardRunBySession } = await import("../../src/card/card-run-registry"); +const mockResolveActiveCardRunBySession = vi.mocked(resolveActiveCardRunBySession); + +describe("approval-card-locator", () => { + beforeEach(() => mockResolveActiveCardRunBySession.mockReset()); + + it("returns outTrackId and sessionKey when the registry finds an active record", () => { + mockResolveActiveCardRunBySession.mockReturnValue({ + outTrackId: "ai_card_xxx", + sessionKey: "session-A", + } as never); + + expect( + findActiveAgentCard({ cfg: {} as never, accountId: "default", sessionKey: "session-A" }), + ).toEqual({ outTrackId: "ai_card_xxx", sessionKey: "session-A" }); + }); + + it("returns null when the registry misses", () => { + mockResolveActiveCardRunBySession.mockReturnValue(null); + + expect( + findActiveAgentCard({ cfg: {} as never, accountId: "default", sessionKey: "session-A" }), + ).toBeNull(); + }); + + it("returns null without querying the registry when sessionKey is empty", () => { + expect(findActiveAgentCard({ cfg: {} as never, accountId: "default", sessionKey: "" })).toBe( + null, + ); + expect(mockResolveActiveCardRunBySession).not.toHaveBeenCalled(); + }); + + it("passes accountId through to the registry", () => { + mockResolveActiveCardRunBySession.mockReturnValue(null); + + findActiveAgentCard({ cfg: {} as never, accountId: "acme", sessionKey: "session-A" }); + + expect(mockResolveActiveCardRunBySession).toHaveBeenCalledWith("acme", "session-A"); + }); +}); From b89d9c8b429fa62728b66129dd77bb706ecd1809 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:09:56 +0800 Subject: [PATCH 23/44] feat(approval): intercept DingTalk approve commands Add the early /approve command handler that parses decisions, resolves approvals through the gateway resolver, and sends Markdown-only direct hints for actionable failures. --- src/approval/approval-command-intercept.ts | 88 ++++++++++ tests/unit/approval-command-intercept.test.ts | 151 ++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 src/approval/approval-command-intercept.ts create mode 100644 tests/unit/approval-command-intercept.test.ts diff --git a/src/approval/approval-command-intercept.ts b/src/approval/approval-command-intercept.ts new file mode 100644 index 00000000..eca46a93 --- /dev/null +++ b/src/approval/approval-command-intercept.ts @@ -0,0 +1,88 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import { getConfig } from "../config"; +import { sendProactiveTextOrMarkdown } from "../send-service"; +import type { Logger } from "../types"; +import { parseApproveCommand } from "./approval-command-parser"; +import { resolveApproval } from "./approval-resolver"; + +export interface ApproveCommandInterceptInput { + cfg: OpenClawConfig; + accountId: string; + text: string; + senderId: string; + log?: Logger; +} + +const APPROVE_COMMAND_RE = /^\/?approve(?:\s|$)/i; + +async function sendDirectHint( + input: Pick, + text: string, +): Promise { + try { + await sendProactiveTextOrMarkdown( + getConfig(input.cfg, input.accountId), + `user:${input.senderId}`, + text, + { accountId: input.accountId, forceMarkdown: true, log: input.log }, + ); + } catch (error) { + input.log?.warn?.( + `[DingTalk][Approval] failed to send /approve hint sender=${input.senderId} err=${String( + (error as Error | null)?.message ?? error, + )}`, + ); + } +} + +export async function tryInterceptApproveCommand( + input: ApproveCommandInterceptInput, +): Promise { + const trimmed = input.text.trim(); + if (!APPROVE_COMMAND_RE.test(trimmed)) { + return false; + } + + const parsed = parseApproveCommand(trimmed); + if (!parsed) { + await sendDirectHint( + input, + "⚠️ /approve 命令格式错误。用法:`/approve `", + ); + input.log?.warn?.("[DingTalk][Approval] malformed /approve command"); + return true; + } + + const result = await resolveApproval({ + cfg: input.cfg, + accountId: input.accountId, + approvalId: parsed.approvalId, + decision: parsed.decision, + senderId: input.senderId, + log: input.log, + }); + + if (result.ok) { + input.log?.info?.( + `[DingTalk][Approval] /approve resolved approvalId=${parsed.approvalId} decision=${parsed.decision}`, + ); + return true; + } + + if (result.reason === "unauthorized") { + await sendDirectHint(input, `⛔ 你不在 approver 名单,无权批准此请求(${parsed.approvalId})。`); + } else if (result.reason === "invalid-decision") { + const hint = result.allowedDecisions?.length + ? `请选择:${result.allowedDecisions.join(" / ")}` + : "请选择允许一次或拒绝"; + await sendDirectHint( + input, + `ℹ️ 该审批不支持 ${parsed.decision}。${hint}(${parsed.approvalId})。`, + ); + } else if (result.reason === "not-found" || result.reason === "already-resolved") { + await sendDirectHint(input, `ℹ️ 审批 ${parsed.approvalId} 已处理或已过期,无需再次操作。`); + } + + input.log?.info?.(`[DingTalk][Approval] /approve resolver returned ${result.reason}`); + return true; +} diff --git a/tests/unit/approval-command-intercept.test.ts b/tests/unit/approval-command-intercept.test.ts new file mode 100644 index 00000000..28243586 --- /dev/null +++ b/tests/unit/approval-command-intercept.test.ts @@ -0,0 +1,151 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { tryInterceptApproveCommand } from "../../src/approval/approval-command-intercept"; + +vi.mock("../../src/approval/approval-resolver", () => ({ + resolveApproval: vi.fn(), +})); + +vi.mock("../../src/send-service", () => ({ + sendProactiveTextOrMarkdown: vi.fn().mockResolvedValue({ ok: true }), +})); + +vi.mock("../../src/config", () => ({ + getConfig: vi.fn(() => ({ clientId: "x", clientSecret: "y" })), +})); + +const { resolveApproval } = await import("../../src/approval/approval-resolver"); +const { sendProactiveTextOrMarkdown } = await import("../../src/send-service"); +const mockResolveApproval = vi.mocked(resolveApproval); +const mockSend = vi.mocked(sendProactiveTextOrMarkdown); + +const base = { + cfg: {} as never, + accountId: "default", + senderId: "staffA", + log: undefined, +}; + +describe("tryInterceptApproveCommand", () => { + beforeEach(() => { + mockResolveApproval.mockReset(); + mockSend.mockReset().mockResolvedValue({ ok: true }); + }); + + it("returns false for non-approve commands", async () => { + await expect(tryInterceptApproveCommand({ ...base, text: "hello" })).resolves.toBe(false); + expect(mockResolveApproval).not.toHaveBeenCalled(); + }); + + it("returns true and sends a DM for malformed approve commands", async () => { + await expect(tryInterceptApproveCommand({ ...base, text: "/approve abc xyz" })).resolves.toBe( + true, + ); + + expect(mockResolveApproval).not.toHaveBeenCalled(); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:staffA", + expect.stringContaining("格式错误"), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("calls the resolver and does not DM on success", async () => { + mockResolveApproval.mockResolvedValue({ ok: true }); + + await expect( + tryInterceptApproveCommand({ ...base, text: "/approve abc allow-once" }), + ).resolves.toBe(true); + + expect(mockResolveApproval).toHaveBeenCalledWith( + expect.objectContaining({ + approvalId: "abc", + decision: "allow-once", + senderId: "staffA", + }), + ); + expect(mockSend).not.toHaveBeenCalled(); + }); + + it("sends an unauthorized DM with the approval id", async () => { + mockResolveApproval.mockResolvedValue({ ok: false, reason: "unauthorized" }); + + await tryInterceptApproveCommand({ ...base, text: "/approve abc deny" }); + + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:staffA", + expect.stringMatching(/无权.*abc/), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("sends invalid-decision DM with allowed decisions when available", async () => { + mockResolveApproval.mockResolvedValue({ + ok: false, + reason: "invalid-decision", + allowedDecisions: ["allow-once", "deny"], + }); + + await tryInterceptApproveCommand({ ...base, text: "/approve abc allow-always" }); + + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:staffA", + expect.stringMatching(/不支持.*allow-once.*deny/), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("sends default invalid-decision DM without allowed decisions", async () => { + mockResolveApproval.mockResolvedValue({ ok: false, reason: "invalid-decision" }); + + await tryInterceptApproveCommand({ ...base, text: "/approve abc allow-always" }); + + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:staffA", + expect.stringContaining("允许一次或拒绝"), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("sends a light DM for not-found and already-resolved", async () => { + mockResolveApproval.mockResolvedValueOnce({ ok: false, reason: "not-found" }); + await tryInterceptApproveCommand({ ...base, text: "/approve abc deny" }); + mockResolveApproval.mockResolvedValueOnce({ ok: false, reason: "already-resolved" }); + await tryInterceptApproveCommand({ ...base, text: "/approve def deny" }); + + expect(mockSend).toHaveBeenNthCalledWith( + 1, + expect.anything(), + "user:staffA", + expect.stringContaining("已处理或已过期"), + expect.objectContaining({ forceMarkdown: true }), + ); + expect(mockSend).toHaveBeenNthCalledWith( + 2, + expect.anything(), + "user:staffA", + expect.stringContaining("已处理或已过期"), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("does not DM for gateway errors", async () => { + mockResolveApproval.mockResolvedValue({ ok: false, reason: "gateway-error" }); + + await tryInterceptApproveCommand({ ...base, text: "/approve abc deny" }); + + expect(mockSend).not.toHaveBeenCalled(); + }); + + it("does not throw when sending a DM fails", async () => { + mockResolveApproval.mockResolvedValue({ ok: false, reason: "unauthorized" }); + mockSend.mockRejectedValueOnce(new Error("network")); + + await expect(tryInterceptApproveCommand({ ...base, text: "/approve abc deny" })).resolves.toBe( + true, + ); + }); +}); From 23c68fd410baeaf27f91eb3c38dce2f0ccc64843 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:10:51 +0800 Subject: [PATCH 24/44] feat(approval): wire DingTalk approval capability Configure the OpenClaw approver-restricted native approval capability for DingTalk origin-only delivery without nativeRuntime yet. --- src/approval/approval-capability.ts | 46 +++++++++++++++++++ tests/unit/approval-capability.test.ts | 61 ++++++++++++++++++++++++++ 2 files changed, 107 insertions(+) create mode 100644 src/approval/approval-capability.ts create mode 100644 tests/unit/approval-capability.test.ts diff --git a/src/approval/approval-capability.ts b/src/approval/approval-capability.ts new file mode 100644 index 00000000..8ec153c6 --- /dev/null +++ b/src/approval/approval-capability.ts @@ -0,0 +1,46 @@ +import { createApproverRestrictedNativeApprovalCapability } from "openclaw/plugin-sdk/approval-delivery-runtime"; +import type { ChannelApprovalCapability } from "openclaw/plugin-sdk/channel-contract"; +import { listDingTalkAccountIds } from "../config"; +import { + getExecApprovalsConfig, + isExecAuthorizedSender, + isPluginAuthorizedSender, + listExecApprovers, + resolveNativeDeliveryMode, +} from "./approval-config"; +import { resolveDingTalkOriginTarget } from "./approval-target-resolver"; + +const EXEC_APPROVAL_SETUP_TEXT = + "Configure channels.dingtalk.execApprovals.approvers or commands.ownerAllowFrom; " + + "leave channels.dingtalk.execApprovals.enabled unset/auto or set it to true."; + +export function createDingTalkApprovalCapability(): ChannelApprovalCapability { + return createApproverRestrictedNativeApprovalCapability({ + channel: "dingtalk", + channelLabel: "DingTalk", + listAccountIds: (cfg) => { + const accountIds = listDingTalkAccountIds(cfg); + return accountIds.length > 0 ? accountIds : ["default"]; + }, + hasApprovers: ({ cfg, accountId }) => + listExecApprovers({ cfg, accountId: accountId || "default" }).length > 0, + isExecAuthorizedSender: ({ cfg, accountId, senderId }) => + Boolean( + senderId && + isExecAuthorizedSender({ cfg, accountId: accountId || "default", senderId }), + ), + isPluginAuthorizedSender: ({ cfg, accountId, senderId }) => + Boolean( + senderId && + isPluginAuthorizedSender({ cfg, accountId: accountId || "default", senderId }), + ), + isNativeDeliveryEnabled: ({ cfg, accountId }) => + getExecApprovalsConfig({ cfg, accountId: accountId || "default" }).isNativeDeliveryEnabled, + resolveNativeDeliveryMode: ({ cfg, accountId }) => + resolveNativeDeliveryMode({ cfg, accountId: accountId || "default" }), + requireMatchingTurnSourceChannel: true, + resolveOriginTarget: resolveDingTalkOriginTarget, + notifyOriginWhenDmOnly: false, + describeExecApprovalSetup: () => EXEC_APPROVAL_SETUP_TEXT, + }); +} diff --git a/tests/unit/approval-capability.test.ts b/tests/unit/approval-capability.test.ts new file mode 100644 index 00000000..4c8929d2 --- /dev/null +++ b/tests/unit/approval-capability.test.ts @@ -0,0 +1,61 @@ +import { describe, expect, it, vi } from "vitest"; + +vi.mock("openclaw/plugin-sdk/approval-delivery-runtime", () => ({ + createApproverRestrictedNativeApprovalCapability: vi.fn(() => ({ mock: "capability" })), +})); + +const { createDingTalkApprovalCapability } = await import("../../src/approval/approval-capability"); +const { createApproverRestrictedNativeApprovalCapability } = await import( + "openclaw/plugin-sdk/approval-delivery-runtime" +); + +const mockFactory = vi.mocked(createApproverRestrictedNativeApprovalCapability); + +describe("createDingTalkApprovalCapability", () => { + it("configures the SDK factory for DingTalk", () => { + createDingTalkApprovalCapability(); + + expect(mockFactory).toHaveBeenCalledWith( + expect.objectContaining({ + channel: "dingtalk", + channelLabel: "DingTalk", + }), + ); + }); + + it("uses origin-only native delivery boundaries in v1", () => { + createDingTalkApprovalCapability(); + const args = mockFactory.mock.calls.at(-1)?.[0]; + + expect(args).toEqual( + expect.objectContaining({ + notifyOriginWhenDmOnly: false, + requireMatchingTurnSourceChannel: true, + }), + ); + expect(args?.resolveApproverDmTargets).toBeUndefined(); + }); + + it("does not attach nativeRuntime in PR-1", () => { + createDingTalkApprovalCapability(); + + expect(mockFactory.mock.calls.at(-1)?.[0].nativeRuntime).toBeUndefined(); + }); + + it("does not implement resolveApproveCommandBehavior because DingTalk intercepts early", () => { + createDingTalkApprovalCapability(); + + expect(mockFactory.mock.calls.at(-1)?.[0]).not.toHaveProperty("resolveApproveCommandBehavior"); + }); + + it("describes approvers, ownerAllowFrom fallback, and enabled mode", () => { + createDingTalkApprovalCapability(); + const describe = mockFactory.mock.calls.at(-1)?.[0].describeExecApprovalSetup; + + const text = describe?.({ cfg: {} as never, accountId: "default" }); + + expect(text).toMatch(/channels\.dingtalk\.execApprovals\.approvers/); + expect(text).toMatch(/commands\.ownerAllowFrom/); + expect(text).toMatch(/enabled/); + }); +}); From e95c6166d3a8074e7cbf713f472032a014013100 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:13:59 +0800 Subject: [PATCH 25/44] feat(approval): expose capability and bypass approve commands Attach DingTalk approvalCapability and intercept /approve messages before routing/session-lock dispatch so approval resolution cannot deadlock behind the waiting agent turn. --- src/channel.ts | 2 + src/inbound-handler.ts | 15 ++ .../unit/channel-approval-capability.test.ts | 9 + .../inbound-handler-approve-intercept.test.ts | 213 ++++++++++++++++++ 4 files changed, 239 insertions(+) create mode 100644 tests/unit/channel-approval-capability.test.ts create mode 100644 tests/unit/inbound-handler-approve-intercept.test.ts diff --git a/src/channel.ts b/src/channel.ts index 00782008..17ffb5fb 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -1,4 +1,5 @@ import { buildChannelConfigSchema, type OpenClawConfig } from "openclaw/plugin-sdk/core"; +import { createDingTalkApprovalCapability } from "./approval/approval-capability"; import { getConfig, isConfigured, mergeAccountWithDefaults, resolveGroupConfig } from "./config"; import { DingTalkConfigSchema } from "./config-schema.js"; import { @@ -121,6 +122,7 @@ export const dingtalkPlugin: DingTalkChannelPlugin = { listPeersLive: async (params) => listDingTalkDirectoryUsers(params), }, actions: createDingTalkMessageActions(), + approvalCapability: createDingTalkApprovalCapability(), outbound: createDingTalkOutbound(), gateway: createDingTalkGateway(), status: createDingTalkStatus(), diff --git a/src/inbound-handler.ts b/src/inbound-handler.ts index 36492985..88e8432b 100644 --- a/src/inbound-handler.ts +++ b/src/inbound-handler.ts @@ -6,6 +6,7 @@ import { normalizeAllowFrom, isSenderAllowed, resolveGroupAccess } from "./acces import { classifyAckReactionEmoji } from "./ack-reaction-classifier"; import { attachNativeAckReaction } from "./ack-reaction-service"; import { createDynamicAckReactionController } from "./ack-reaction/dynamic-ack-reaction-controller"; +import { tryInterceptApproveCommand } from "./approval/approval-command-intercept"; import { getAccessToken } from "./auth"; import { createAICard, commitAICardBlocks, isCardInTerminalState } from "./card-service"; import { isCardRunStopRequested, registerCardRun, removeCardRun } from "./card/card-run-registry"; @@ -775,6 +776,20 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams) config: dingtalkConfig, }); + const approveCommandText = stripLeadingMentions(extractedContent.text).trim(); + if (/^\/?approve(?:\s|$)/i.test(approveCommandText)) { + const intercepted = await tryInterceptApproveCommand({ + cfg, + accountId, + text: approveCommandText, + senderId, + log, + }); + if (intercepted) { + return; + } + } + // Single routing decision for this message. Skipped for recursive sub-agent // calls, where routing is already fixed by subAgentOptions. const messageTarget = subAgentOptions diff --git a/tests/unit/channel-approval-capability.test.ts b/tests/unit/channel-approval-capability.test.ts new file mode 100644 index 00000000..d6e80ed8 --- /dev/null +++ b/tests/unit/channel-approval-capability.test.ts @@ -0,0 +1,9 @@ +import { describe, expect, it } from "vitest"; +import { dingtalkPlugin } from "../../src/channel"; + +describe("dingtalkPlugin approval capability", () => { + it("exposes approvalCapability for OpenClaw native approvals", () => { + expect(dingtalkPlugin.approvalCapability).toBeDefined(); + expect(dingtalkPlugin.approvalCapability?.authorizeActorAction).toBeTypeOf("function"); + }); +}); diff --git a/tests/unit/inbound-handler-approve-intercept.test.ts b/tests/unit/inbound-handler-approve-intercept.test.ts new file mode 100644 index 00000000..8ca78920 --- /dev/null +++ b/tests/unit/inbound-handler-approve-intercept.test.ts @@ -0,0 +1,213 @@ +import fs from "node:fs"; +import path from "node:path"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import type { DingTalkConfig, DingTalkInboundMessage } from "../../src/types"; + +const shared = vi.hoisted(() => ({ + tryInterceptApproveCommandMock: vi.fn(), + sendBySessionMock: vi.fn(), + sendMessageMock: vi.fn(), + extractMessageContentMock: vi.fn(), + getRuntimeMock: vi.fn(), + acquireSessionLockMock: vi.fn(), + isAbortRequestTextMock: vi.fn(), +})); + +vi.mock("../../src/approval/approval-command-intercept", () => ({ + tryInterceptApproveCommand: shared.tryInterceptApproveCommandMock, +})); + +vi.mock("../../src/auth", () => ({ + getAccessToken: vi.fn().mockResolvedValue("token_abc"), +})); + +vi.mock("../../src/runtime", () => ({ + getDingTalkRuntime: shared.getRuntimeMock, +})); + +vi.mock("../../src/message-utils", () => ({ + extractMessageContent: shared.extractMessageContentMock, +})); + +vi.mock("../../src/send-service", () => ({ + sendBySession: shared.sendBySessionMock, + sendMessage: shared.sendMessageMock, + sendProactiveMedia: vi.fn(), + uploadMedia: vi.fn(), +})); + +vi.mock("../../src/card-service", () => ({ + createAICard: vi.fn(), + finishAICard: vi.fn(), + commitAICardBlocks: vi.fn(), + formatContentForCard: vi.fn((s: string) => s), + isCardInTerminalState: vi.fn(), + streamAICard: vi.fn(), + updateAICardBlockList: vi.fn(), + streamAICardContent: vi.fn(), + clearAICardStreamingContent: vi.fn(), +})); + +vi.mock("../../src/session-lock", () => ({ + acquireSessionLock: shared.acquireSessionLockMock, +})); + +vi.mock("openclaw/plugin-sdk/reply-runtime", () => ({ + isAbortRequestText: shared.isAbortRequestTextMock, + isBtwRequestText: vi.fn().mockReturnValue(false), +})); + +vi.mock("../../src/message-context-store", async () => { + const actual = await vi.importActual( + "../../src/message-context-store", + ); + return { + ...actual, + upsertInboundMessageContext: vi.fn(actual.upsertInboundMessageContext), + resolveByMsgId: vi.fn(actual.resolveByMsgId), + resolveByAlias: vi.fn(actual.resolveByAlias), + resolveByCreatedAtWindow: vi.fn(actual.resolveByCreatedAtWindow), + clearMessageContextCacheForTest: vi.fn(actual.clearMessageContextCacheForTest), + }; +}); + +vi.mock("../../src/messaging/quoted-file-service", () => ({ + downloadGroupFile: vi.fn().mockResolvedValue(null), + getUnionIdByStaffId: vi.fn().mockResolvedValue("union_1"), + resolveQuotedFile: vi.fn().mockResolvedValue(null), +})); + +vi.mock("../../src/messaging/attachment-text-extractor", () => ({ + extractAttachmentText: vi.fn().mockResolvedValue(null), +})); + +vi.mock("../../src/media-utils", async () => { + const actual = await vi.importActual("../../src/media-utils"); + return { + ...actual, + prepareMediaInput: vi.fn(), + resolveOutboundMediaType: vi.fn(), + }; +}); + +import { handleDingTalkMessage, resetProactivePermissionHintStateForTest } from "../../src/inbound-handler"; +import { clearCardRunRegistryForTest } from "../../src/card/card-run-registry"; +import { clearTargetDirectoryStateCache } from "../../src/targeting/target-directory-store"; +import * as messageContextStore from "../../src/message-context-store"; + +function buildRuntime() { + return { + channel: { + routing: { + resolveAgentRoute: vi + .fn() + .mockReturnValue({ agentId: "main", sessionKey: "session-main", mainSessionKey: "session-main" }), + buildAgentSessionKey: vi.fn().mockReturnValue("agent-session-key"), + }, + media: { + saveMediaBuffer: vi.fn(), + }, + session: { + resolveStorePath: vi.fn().mockReturnValue("/tmp/inbound-approval-test/store.json"), + readSessionUpdatedAt: vi.fn().mockReturnValue(null), + recordInboundSession: vi.fn().mockResolvedValue(undefined), + }, + reply: { + resolveEnvelopeFormatOptions: vi.fn().mockReturnValue({}), + formatInboundEnvelope: vi.fn().mockReturnValue("body"), + finalizeInboundContext: vi.fn().mockReturnValue({ SessionKey: "session-main" }), + dispatchReplyWithBufferedBlockDispatcher: vi.fn().mockResolvedValue({ queuedFinal: "ok" }), + }, + }, + }; +} + +function message(overrides: Partial = {}): DingTalkInboundMessage { + return { + msgId: `msg_${Math.random()}`, + msgtype: "text", + text: { content: "hello" }, + conversationType: "1", + conversationId: "staffA", + senderId: "staffA", + senderStaffId: "staffA", + chatbotUserId: "bot", + senderNick: "Staff A", + createAt: Date.now(), + sessionWebhook: "https://session.webhook", + ...overrides, + } as DingTalkInboundMessage; +} + +async function invoke(text: string, overrides: Partial = {}) { + shared.extractMessageContentMock.mockReturnValue({ text, messageType: "text" }); + await handleDingTalkMessage({ + cfg: {}, + accountId: "main", + sessionWebhook: "https://session.webhook", + log: undefined, + dingtalkConfig: { dmPolicy: "open", groupPolicy: "open" } as unknown as DingTalkConfig, + data: message(overrides), + }); +} + +describe("inbound-handler · /approve early intercept", () => { + beforeEach(() => { + clearTargetDirectoryStateCache(); + resetProactivePermissionHintStateForTest(); + clearCardRunRegistryForTest(); + fs.rmSync(path.join(path.dirname("/tmp/inbound-approval-test/store.json"), "dingtalk-state"), { + recursive: true, + force: true, + }); + shared.tryInterceptApproveCommandMock.mockReset().mockResolvedValue(false); + shared.sendBySessionMock.mockReset(); + shared.sendMessageMock.mockReset().mockResolvedValue({ ok: true }); + shared.extractMessageContentMock.mockReset(); + shared.acquireSessionLockMock.mockReset().mockResolvedValue(vi.fn()); + shared.isAbortRequestTextMock.mockReset().mockReturnValue(false); + shared.getRuntimeMock.mockReturnValue(buildRuntime()); + messageContextStore.clearMessageContextCacheForTest(); + }); + + it("does not call the approval intercept for ordinary messages", async () => { + await invoke("hello"); + + expect(shared.tryInterceptApproveCommandMock).not.toHaveBeenCalled(); + }); + + it("calls approval intercept for direct /approve commands and bypasses reply dispatch", async () => { + shared.tryInterceptApproveCommandMock.mockResolvedValue(true); + + await invoke("/approve abc deny"); + + expect(shared.tryInterceptApproveCommandMock).toHaveBeenCalledWith( + expect.objectContaining({ text: "/approve abc deny", senderId: "staffA" }), + ); + expect(shared.acquireSessionLockMock).not.toHaveBeenCalled(); + expect(shared.sendMessageMock).not.toHaveBeenCalled(); + }); + + it("strips leading @mentions before checking group approve commands", async () => { + shared.tryInterceptApproveCommandMock.mockResolvedValue(true); + + await invoke("@OpenClaw /approve abc once", { + conversationType: "2", + conversationId: "cid-group", + conversationTitle: "Group", + }); + + expect(shared.tryInterceptApproveCommandMock).toHaveBeenCalledWith( + expect.objectContaining({ text: "/approve abc once" }), + ); + expect(shared.acquireSessionLockMock).not.toHaveBeenCalled(); + }); + + it("continues the normal pipeline when the intercept returns false", async () => { + shared.tryInterceptApproveCommandMock.mockResolvedValue(false); + + await invoke("/approve abc once"); + + expect(shared.acquireSessionLockMock).toHaveBeenCalledWith("session-main"); + }); +}); From cb25eb9814030e18da849962a8b0561047da3f5f Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:30:54 +0800 Subject: [PATCH 26/44] feat(approval): implement DingTalk native approval runtime --- src/approval/approval-callback-handler.ts | 154 ++++++++++++ src/approval/approval-capability.ts | 3 + src/approval/approval-card-patcher.ts | 41 ++++ src/approval/approval-card-state.ts | 35 +++ src/approval/approval-markdown-render.ts | 73 ++++++ src/approval/approval-native-runtime.ts | 228 +++++++++++++++++ src/card-callback-service.ts | 22 ++ src/card-service.ts | 4 + src/card/card-run-registry.ts | 16 ++ src/card/card-template.ts | 3 +- src/gateway/channel-gateway.ts | 10 + .../integration/gateway-inbound-flow.test.ts | 40 +++ tests/unit/approval-callback-handler.test.ts | 187 ++++++++++++++ tests/unit/approval-capability.test.ts | 8 +- tests/unit/approval-card-patcher.test.ts | 77 ++++++ tests/unit/approval-card-state.test.ts | 55 +++++ tests/unit/approval-markdown-render.test.ts | 93 +++++++ tests/unit/approval-native-runtime.test.ts | 229 ++++++++++++++++++ tests/unit/card-callback-service.test.ts | 20 ++ tests/unit/card-run-registry-approval.test.ts | 13 + tests/unit/card-service.test.ts | 2 + 21 files changed, 1309 insertions(+), 4 deletions(-) create mode 100644 src/approval/approval-callback-handler.ts create mode 100644 src/approval/approval-card-patcher.ts create mode 100644 src/approval/approval-card-state.ts create mode 100644 src/approval/approval-markdown-render.ts create mode 100644 src/approval/approval-native-runtime.ts create mode 100644 tests/unit/approval-callback-handler.test.ts create mode 100644 tests/unit/approval-card-patcher.test.ts create mode 100644 tests/unit/approval-card-state.test.ts create mode 100644 tests/unit/approval-markdown-render.test.ts create mode 100644 tests/unit/approval-native-runtime.test.ts diff --git a/src/approval/approval-callback-handler.ts b/src/approval/approval-callback-handler.ts new file mode 100644 index 00000000..4d525397 --- /dev/null +++ b/src/approval/approval-callback-handler.ts @@ -0,0 +1,154 @@ +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import { getAccessToken } from "../auth"; +import type { CardCallbackAnalysis } from "../card-callback-service"; +import { + isActiveCardRun, + resolveCardRun, +} from "../card/card-run-registry"; +import { getConfig } from "../config"; +import { sendProactiveTextOrMarkdown } from "../send-service"; +import type { ApprovalDecision, Logger } from "../types"; +import { applyExpiredPatch, applyResolvedPatch } from "./approval-card-patcher"; +import { resolveApproval } from "./approval-resolver"; + +const APPROVAL_DECISIONS: readonly ApprovalDecision[] = ["allow-once", "allow-always", "deny"]; + +export interface HandleApprovalCallbackInput { + cfg: OpenClawConfig; + accountId: string; + analysis: CardCallbackAnalysis; + log?: Logger; +} + +export interface HandleApprovalCallbackResult { + handled: boolean; + reason?: string; +} + +function isApprovalDecision(value: unknown): value is ApprovalDecision { + return typeof value === "string" && (APPROVAL_DECISIONS as readonly string[]).includes(value); +} + +function parseDecision(analysis: CardCallbackAnalysis): ApprovalDecision | null { + const fromParams = analysis.cardPrivateData?.params?.action; + if (isApprovalDecision(fromParams)) { + return fromParams; + } + const [firstActionId] = analysis.cardPrivateData?.actionIds ?? []; + if (isApprovalDecision(firstActionId)) { + return firstActionId; + } + if (isApprovalDecision(analysis.actionId)) { + return analysis.actionId; + } + return null; +} + +function resolveApprovalId(analysis: CardCallbackAnalysis): string | null { + const fromParams = analysis.cardPrivateData?.params?.approveId; + if (typeof fromParams === "string" && fromParams.trim()) { + return fromParams.trim(); + } + if (!analysis.outTrackId) { + return null; + } + return resolveCardRun(analysis.outTrackId)?.pendingApprovalId ?? null; +} + +async function sendPrivateHint(params: { + cfg: OpenClawConfig; + accountId: string; + userId?: string; + text: string; + log?: Logger; +}): Promise { + const userId = params.userId?.trim(); + if (!userId) { + params.log?.warn?.("[DingTalk][Approval] Skip private hint because callback userId is missing"); + return; + } + await sendProactiveTextOrMarkdown( + getConfig(params.cfg, params.accountId), + `user:${userId}`, + params.text, + { forceMarkdown: true, accountId: params.accountId, log: params.log }, + ).catch((error) => { + params.log?.warn?.( + `[DingTalk][Approval] Failed to send private hint user=${userId}: ${String(error)}`, + ); + }); +} + +export async function tryHandleApprovalCallback( + input: HandleApprovalCallbackInput, +): Promise { + const decision = parseDecision(input.analysis); + if (!decision || !input.analysis.outTrackId) { + return { handled: false }; + } + + const dtConfig = getConfig(input.cfg, input.accountId); + const token = await getAccessToken(dtConfig, input.log); + const run = resolveCardRun(input.analysis.outTrackId); + const cardStillActive = run ? isActiveCardRun(run) : false; + const approvalId = resolveApprovalId(input.analysis); + + if (!approvalId) { + await applyExpiredPatch(input.analysis.outTrackId, token, cardStillActive, dtConfig).catch((error) => { + input.log?.warn?.(`[DingTalk][Approval] Failed to expire callback without approvalId: ${String(error)}`); + }); + return { handled: true, reason: "missing-approval-id" }; + } + + const result = await resolveApproval({ + cfg: input.cfg, + accountId: input.accountId, + approvalId, + decision, + senderId: input.analysis.userId ?? "", + log: input.log, + }); + + if (result.ok) { + await applyResolvedPatch( + input.analysis.outTrackId, + decision, + token, + cardStillActive, + dtConfig, + ).catch((error) => { + input.log?.warn?.(`[DingTalk][Approval] Failed to patch resolved card: ${String(error)}`); + }); + return { handled: true, reason: "resolved" }; + } + + if (result.reason === "unauthorized") { + await sendPrivateHint({ + cfg: input.cfg, + accountId: input.accountId, + userId: input.analysis.userId, + text: `⛔ 你不在 approver 名单,无权批准此请求(${approvalId})。`, + log: input.log, + }); + return { handled: true, reason: result.reason }; + } + + if (result.reason === "invalid-decision") { + const hint = result.allowedDecisions?.length + ? `请选择:${result.allowedDecisions.join(" / ")}` + : "请选择允许一次或拒绝"; + await sendPrivateHint({ + cfg: input.cfg, + accountId: input.accountId, + userId: input.analysis.userId, + text: `ℹ️ 该审批不支持 ${decision}。${hint}(${approvalId})。`, + log: input.log, + }); + return { handled: true, reason: result.reason }; + } + + await applyExpiredPatch(input.analysis.outTrackId, token, cardStillActive, dtConfig).catch((error) => { + input.log?.warn?.(`[DingTalk][Approval] Failed to patch expired card: ${String(error)}`); + }); + return { handled: true, reason: result.reason }; +} diff --git a/src/approval/approval-capability.ts b/src/approval/approval-capability.ts index 8ec153c6..79570725 100644 --- a/src/approval/approval-capability.ts +++ b/src/approval/approval-capability.ts @@ -8,6 +8,7 @@ import { listExecApprovers, resolveNativeDeliveryMode, } from "./approval-config"; +import { createDingTalkApprovalNativeRuntime } from "./approval-native-runtime"; import { resolveDingTalkOriginTarget } from "./approval-target-resolver"; const EXEC_APPROVAL_SETUP_TEXT = @@ -41,6 +42,8 @@ export function createDingTalkApprovalCapability(): ChannelApprovalCapability { requireMatchingTurnSourceChannel: true, resolveOriginTarget: resolveDingTalkOriginTarget, notifyOriginWhenDmOnly: false, + nativeRuntime: + createDingTalkApprovalNativeRuntime() as unknown as ChannelApprovalCapability["nativeRuntime"], describeExecApprovalSetup: () => EXEC_APPROVAL_SETUP_TEXT, }); } diff --git a/src/approval/approval-card-patcher.ts b/src/approval/approval-card-patcher.ts new file mode 100644 index 00000000..d33bb5b6 --- /dev/null +++ b/src/approval/approval-card-patcher.ts @@ -0,0 +1,41 @@ +import type { DingTalkConfig } from "../types"; +import { updateCardVariables } from "../card-callback-service"; +import { + buildApprovalClearedCardParams, + buildApprovalPendingCardParams, +} from "./approval-card-state"; +import { + clearCardRunPendingApproval, + markCardRunPendingApproval, +} from "../card/card-run-registry"; + +export async function applyPendingPatch( + outTrackId: string, + approvalId: string, + token: string, + config?: Pick, +): Promise { + await updateCardVariables(outTrackId, buildApprovalPendingCardParams(approvalId), token, config); + markCardRunPendingApproval(outTrackId, approvalId); +} + +export async function applyResolvedPatch( + outTrackId: string, + _decision: string, + token: string, + cardStillActive: boolean, + config?: Pick, +): Promise { + await updateCardVariables(outTrackId, buildApprovalClearedCardParams(cardStillActive), token, config); + clearCardRunPendingApproval(outTrackId); +} + +export async function applyExpiredPatch( + outTrackId: string, + token: string, + cardStillActive: boolean, + config?: Pick, +): Promise { + await updateCardVariables(outTrackId, buildApprovalClearedCardParams(cardStillActive), token, config); + clearCardRunPendingApproval(outTrackId); +} diff --git a/src/approval/approval-card-state.ts b/src/approval/approval-card-state.ts new file mode 100644 index 00000000..5a7202f8 --- /dev/null +++ b/src/approval/approval-card-state.ts @@ -0,0 +1,35 @@ +export const APPROVAL_CARD_KEYS = { + showApproveBtns: "show_approve_btns", + approveId: "approveId", + hasAction: "hasAction", +} as const; + +export type ApprovalCardParams = { + [APPROVAL_CARD_KEYS.showApproveBtns]: "true" | "false"; + [APPROVAL_CARD_KEYS.approveId]: string; + [APPROVAL_CARD_KEYS.hasAction]: "true" | "false"; +}; + +export const APPROVAL_CARD_INITIAL: { + show_approve_btns: "false"; + approveId: ""; +} = { + show_approve_btns: "false", + approveId: "", +}; + +export function buildApprovalPendingCardParams(approvalId: string): ApprovalCardParams { + return { + show_approve_btns: "true", + approveId: approvalId, + hasAction: "false", + }; +} + +export function buildApprovalClearedCardParams(cardStillActive: boolean): ApprovalCardParams { + return { + show_approve_btns: "false", + approveId: "", + hasAction: cardStillActive ? "true" : "false", + }; +} diff --git a/src/approval/approval-markdown-render.ts b/src/approval/approval-markdown-render.ts new file mode 100644 index 00000000..920990b5 --- /dev/null +++ b/src/approval/approval-markdown-render.ts @@ -0,0 +1,73 @@ +import { + resolveExecApprovalRequestAllowedDecisions, + type ExecApprovalRequest, + type PluginApprovalRequest, +} from "openclaw/plugin-sdk/approval-runtime"; +import type { ApprovalDecision } from "../types"; + +const ALL_DECISIONS: readonly ApprovalDecision[] = ["allow-once", "allow-always", "deny"]; + +const DECISION_LABEL: Record = { + "allow-once": "批准(仅一次)", + "allow-always": "批准(总是)", + deny: "拒绝", +}; + +function formatExpireHint(expiresAtMs: number | undefined, nowMs: number): string { + if (!expiresAtMs || expiresAtMs <= nowMs) { + return ""; + } + const minutes = Math.round((expiresAtMs - nowMs) / 60_000); + return minutes > 0 ? `\n**过期时间**: ${minutes} 分钟` : ""; +} + +function normalizePluginAllowedDecisions( + allowedDecisions?: readonly (ApprovalDecision | string)[] | null, +): readonly ApprovalDecision[] { + if (!Array.isArray(allowedDecisions)) { + return ALL_DECISIONS; + } + const explicit = allowedDecisions.filter((decision): decision is ApprovalDecision => + (ALL_DECISIONS as readonly string[]).includes(decision), + ); + return explicit.length > 0 ? explicit : ALL_DECISIONS; +} + +function decisionBlock(id: string, allowed: readonly ApprovalDecision[]): string { + return allowed.map((decision) => `${DECISION_LABEL[decision]}:\`/approve ${id} ${decision}\``).join("\n"); +} + +export function buildExecApprovalMarkdown(request: ExecApprovalRequest, nowMs: number): string { + const payload = request.request; + const allowed = resolveExecApprovalRequestAllowedDecisions({ + ask: payload.ask ?? null, + allowedDecisions: payload.allowedDecisions, + }) as readonly ApprovalDecision[]; + const cwdLine = payload.cwd ? `\n**cwd**: \`${payload.cwd}\`` : ""; + const command = payload.commandPreview || payload.command || "(no command)"; + + return [ + "### 需要审批:命令执行", + `**ID**: \`${request.id}\`${cwdLine}${formatExpireHint(request.expiresAtMs, nowMs)}`, + "", + "```", + command, + "```", + "", + decisionBlock(request.id, allowed), + ].join("\n"); +} + +export function buildPluginApprovalMarkdown(request: PluginApprovalRequest, nowMs: number): string { + const payload = request.request; + const allowed = normalizePluginAllowedDecisions(payload.allowedDecisions); + const tool = payload.toolName || "(unknown tool)"; + + return [ + "### 需要审批:插件调用", + `**ID**: \`${request.id}\`\n**Tool**: \`${tool}\`${formatExpireHint(request.expiresAtMs, nowMs)}`, + payload.description ? `\n${payload.description}` : "", + "", + decisionBlock(request.id, allowed), + ].join("\n"); +} diff --git a/src/approval/approval-native-runtime.ts b/src/approval/approval-native-runtime.ts new file mode 100644 index 00000000..d5ce0924 --- /dev/null +++ b/src/approval/approval-native-runtime.ts @@ -0,0 +1,228 @@ +import type { + ChannelApprovalNativeRuntimeAdapter, +} from "openclaw/plugin-sdk/approval-handler-runtime"; +import { getAccessToken } from "../auth"; +import { + isActiveCardRun, + resolveCardRun, +} from "../card/card-run-registry"; +import { getConfig } from "../config"; +import { getLogger } from "../logger-context"; +import { sendProactiveTextOrMarkdown } from "../send-service"; +import type { ApprovalDecision } from "../types"; +import { findActiveAgentCard } from "./approval-card-locator"; +import { + applyExpiredPatch, + applyPendingPatch, + applyResolvedPatch, +} from "./approval-card-patcher"; +import { getExecApprovalsConfig, listExecApprovers } from "./approval-config"; +import { + buildExecApprovalMarkdown, + buildPluginApprovalMarkdown, +} from "./approval-markdown-render"; +import { normalizeApprovalTargetTo } from "./approval-target-resolver"; + +export type DingTalkApprovalPendingPayload = { + approvalId: string; + markdownText: string; +}; + +export type DingTalkApprovalPreparedTarget = { + route: "card" | "markdown"; + to: string; + accountId: string; + activeCardOutTrackId?: string; +}; + +export type DingTalkApprovalEntry = + | { + mode: "card"; + approvalId: string; + accountId: string; + outTrackId: string; + } + | { + mode: "markdown"; + approvalId: string; + accountId: string; + }; + +export type DingTalkApprovalFinalPayload = + | { phase: "resolved"; decision: ApprovalDecision } + | { phase: "expired" }; + +function isExplicitHttpFailure(error: unknown): boolean { + const candidate = error as { status?: number; response?: { status?: number }; code?: string } | null; + const status = + typeof candidate?.status === "number" ? candidate.status : candidate?.response?.status; + return (typeof status === "number" && status >= 400) || candidate?.code === "EBADREQ"; +} + +function isAmbiguousDeliveryFailure(error: unknown): boolean { + const code = (error as { code?: string } | null)?.code; + return code === "ETIMEDOUT" || code === "ECONNRESET" || code === "ECONNABORTED"; +} + +function isSuccessfulSendResult(result: unknown): boolean { + return !( + result && + typeof result === "object" && + "ok" in result && + (result as { ok?: unknown }).ok === false + ); +} + +export function createDingTalkApprovalNativeRuntime(): ChannelApprovalNativeRuntimeAdapter< + DingTalkApprovalPendingPayload, + DingTalkApprovalPreparedTarget, + DingTalkApprovalEntry, + unknown, + DingTalkApprovalFinalPayload +> { + return { + eventKinds: ["exec", "plugin"], + availability: { + isConfigured: ({ cfg, accountId }) => + getExecApprovalsConfig({ cfg, accountId: accountId ?? "default" }).isNativeDeliveryEnabled, + shouldHandle: ({ cfg, accountId, request }) => { + const resolvedAccountId = accountId ?? "default"; + if (!getExecApprovalsConfig({ cfg, accountId: resolvedAccountId }).isNativeDeliveryEnabled) { + return false; + } + if (request.request.turnSourceChannel !== "dingtalk") { + return false; + } + if (!request.request.turnSourceTo) { + return false; + } + return listExecApprovers({ cfg, accountId: resolvedAccountId }).length > 0; + }, + }, + presentation: { + buildPendingPayload: ({ request, approvalKind, nowMs }) => ({ + approvalId: request.id, + markdownText: + approvalKind === "plugin" + ? buildPluginApprovalMarkdown(request as never, nowMs) + : buildExecApprovalMarkdown(request as never, nowMs), + }), + buildResolvedResult: ({ resolved }) => ({ + kind: "update", + payload: { phase: "resolved", decision: resolved.decision }, + }), + buildExpiredResult: () => ({ + kind: "update", + payload: { phase: "expired" }, + }), + }, + transport: { + prepareTarget: ({ cfg, accountId, plannedTarget, request }) => { + const target = plannedTarget.target as { to: string; accountId?: string | null }; + const resolvedAccountId = + target.accountId ?? accountId ?? request.request.turnSourceAccountId ?? "default"; + const to = normalizeApprovalTargetTo(target.to); + const activeCard = findActiveAgentCard({ + cfg, + accountId: resolvedAccountId, + sessionKey: request.request.sessionKey ?? "", + }); + if (activeCard) { + return { + dedupeKey: `dingtalk:${resolvedAccountId}:${to}:${activeCard.outTrackId}`, + target: { + route: "card", + to, + accountId: resolvedAccountId, + activeCardOutTrackId: activeCard.outTrackId, + }, + }; + } + return { + dedupeKey: `dingtalk:${resolvedAccountId}:${to}:markdown:${request.id}`, + target: { + route: "markdown", + to, + accountId: resolvedAccountId, + }, + }; + }, + deliverPending: async ({ cfg, preparedTarget, pendingPayload }) => { + const dtConfig = getConfig(cfg, preparedTarget.accountId); + const log = getLogger(preparedTarget.accountId); + if (preparedTarget.route === "card" && preparedTarget.activeCardOutTrackId) { + const token = await getAccessToken(dtConfig, log); + try { + await applyPendingPatch( + preparedTarget.activeCardOutTrackId, + pendingPayload.approvalId, + token, + dtConfig, + ); + return { + mode: "card", + approvalId: pendingPayload.approvalId, + accountId: preparedTarget.accountId, + outTrackId: preparedTarget.activeCardOutTrackId, + }; + } catch (error) { + if (isAmbiguousDeliveryFailure(error)) { + return null; + } + if (!isExplicitHttpFailure(error)) { + return null; + } + } + } + + const sent = await sendProactiveTextOrMarkdown( + dtConfig, + preparedTarget.to, + pendingPayload.markdownText, + { forceMarkdown: true, accountId: preparedTarget.accountId, log }, + ); + if (!isSuccessfulSendResult(sent)) { + return null; + } + return { + mode: "markdown", + approvalId: pendingPayload.approvalId, + accountId: preparedTarget.accountId, + }; + }, + updateEntry: async ({ cfg, entry, payload, phase }) => { + if (entry.mode !== "card") { + return; + } + const dtConfig = getConfig(cfg, entry.accountId); + const log = getLogger(entry.accountId); + const token = await getAccessToken(dtConfig, log); + const record = resolveCardRun(entry.outTrackId); + const cardStillActive = record ? isActiveCardRun(record) : false; + if (phase === "resolved" && payload.phase === "resolved") { + await applyResolvedPatch( + entry.outTrackId, + payload.decision, + token, + cardStillActive, + dtConfig, + ); + return; + } + await applyExpiredPatch(entry.outTrackId, token, cardStillActive, dtConfig); + }, + }, + observe: { + onDelivered: ({ accountId, entry, request }) => { + getLogger(accountId)?.info?.( + `[DingTalk][Approval] delivered approval=${request.id} mode=${(entry as DingTalkApprovalEntry).mode}`, + ); + }, + onDeliveryError: ({ accountId, error, request }) => { + getLogger(accountId)?.warn?.( + `[DingTalk][Approval][DeliveryError] approval=${request.id} error=${String(error)}`, + ); + }, + }, + }; +} diff --git a/src/card-callback-service.ts b/src/card-callback-service.ts index c8774c3a..ecc4137c 100644 --- a/src/card-callback-service.ts +++ b/src/card-callback-service.ts @@ -6,6 +6,10 @@ const DINGTALK_API = "https://api.dingtalk.com"; export interface CardCallbackAnalysis { summary: string; actionId?: string; + cardPrivateData?: { + actionIds?: unknown[]; + params?: Record; + }; feedbackTarget?: string; feedbackAckText?: string; userId?: string; @@ -100,6 +104,8 @@ export function analyzeCardCallback(data: unknown): CardCallbackAnalysis { const embeddedCardPrivateData = asRecord(parseEmbeddedJson(record?.cardPrivateData)); const embeddedValuePrivateData = asRecord(parseEmbeddedJson(embeddedValue?.cardPrivateData)); const embeddedContentPrivateData = asRecord(parseEmbeddedJson(embeddedContent?.cardPrivateData)); + const extractedCardPrivateData = + embeddedCardPrivateData ?? embeddedValuePrivateData ?? embeddedContentPrivateData; const candidateRecords = [ record, embeddedValue, @@ -133,6 +139,14 @@ export function analyzeCardCallback(data: unknown): CardCallbackAnalysis { return { summary, actionId, + cardPrivateData: extractedCardPrivateData + ? { + actionIds: Array.isArray(extractedCardPrivateData.actionIds) + ? extractedCardPrivateData.actionIds + : undefined, + params: asRecord(extractedCardPrivateData.params), + } + : undefined, userId, spaceId, processQueryKey, @@ -157,6 +171,14 @@ export function analyzeCardCallback(data: unknown): CardCallbackAnalysis { return { summary, actionId, + cardPrivateData: extractedCardPrivateData + ? { + actionIds: Array.isArray(extractedCardPrivateData.actionIds) + ? extractedCardPrivateData.actionIds + : undefined, + params: asRecord(extractedCardPrivateData.params), + } + : undefined, feedbackTarget: feedbackTarget || undefined, feedbackAckText, userId: userId || undefined, diff --git a/src/card-service.ts b/src/card-service.ts index 79e90708..24e39ea1 100644 --- a/src/card-service.ts +++ b/src/card-service.ts @@ -5,6 +5,7 @@ import axios from "./http-client"; import { getAccessToken } from "./auth"; import { updateCardVariables } from "./card-callback-service"; import { DINGTALK_CARD_TEMPLATE, STOP_ACTION_VISIBLE, STOP_ACTION_HIDDEN } from "./card/card-template"; +import { APPROVAL_CARD_INITIAL } from "./approval/approval-card-state"; import { resolveRobotCode, stripTargetPrefix } from "./config"; import { resolveOriginalPeerId } from "./peer-id-registry"; import { @@ -805,6 +806,7 @@ export async function createAICard( quoteContent: options.quoteContent || "", ...(options.statusLine?.trim() ? { statusLine: options.statusLine } : {}), flowStatus: AICardStatus.INPUTING, + ...APPROVAL_CARD_INITIAL, // V2 template uses hasAction (string), V1 uses stop_action (string) // DingTalk cardParamMap requires all values to be strings hasAction: String(STOP_ACTION_VISIBLE), @@ -1133,6 +1135,7 @@ export async function commitAICardBlocks( [template.streamingKey]: options.content, // markdown content for display [template.copyContentKey]: options.content, // same markdown as String type for card copy action flowStatus: 3, // completed state - V2 template hides stop button automatically + ...APPROVAL_CARD_INITIAL, }; // Optional fields @@ -1428,6 +1431,7 @@ export async function finalizeStoppedAICard( [template.streamingKey]: payload.content, [template.copyContentKey]: payload.content, flowStatus: 3, + ...APPROVAL_CARD_INITIAL, }, card.accessToken, card.config, diff --git a/src/card/card-run-registry.ts b/src/card/card-run-registry.ts index 89306d37..e1793e6e 100644 --- a/src/card/card-run-registry.ts +++ b/src/card/card-run-registry.ts @@ -20,6 +20,7 @@ export interface CardRunRecord { ownerUserId?: string; card?: AICardInstance; controller?: CardDraftController; + pendingApprovalId?: string; stopRequestedAt?: number; registeredAt: number; } @@ -113,6 +114,21 @@ export function resolveActiveCardRunBySession( return latest; } +export function markCardRunPendingApproval(outTrackId: string, approvalId: string): void { + const record = records.get(outTrackId.trim()); + const trimmedApprovalId = approvalId.trim(); + if (record && trimmedApprovalId) { + record.pendingApprovalId = trimmedApprovalId; + } +} + +export function clearCardRunPendingApproval(outTrackId: string): void { + const record = records.get(outTrackId.trim()); + if (record) { + record.pendingApprovalId = undefined; + } +} + /** * Find the most recently registered card run for a given account + conversation. * Uses case-insensitive match of the conversationId within sessionKey. diff --git a/src/card/card-template.ts b/src/card/card-template.ts index a21272c3..95f08e02 100644 --- a/src/card/card-template.ts +++ b/src/card/card-template.ts @@ -4,7 +4,7 @@ export const STOP_ACTION_VISIBLE = true; export const STOP_ACTION_HIDDEN = false; export const BUILTIN_DINGTALK_CARD_TEMPLATE_ID = - process.env.DINGTALK_CARD_TEMPLATE_ID || "675cde2f-f526-40cb-b828-f5b2b57b8b77.schema"; + process.env.DINGTALK_CARD_TEMPLATE_ID || "58f73932-fc3b-46ae-8e90-93313e405061.schema"; export const BUILTIN_DINGTALK_CARD_CONTENT_KEY = "content"; export const BUILTIN_DINGTALK_CARD_BLOCK_LIST_KEY = "blockList"; export const BUILTIN_DINGTALK_CARD_COPY_CONTENT_KEY = "copy_content"; @@ -28,4 +28,3 @@ export const DINGTALK_CARD_TEMPLATE: Readonly = Ob blockListKey: BUILTIN_DINGTALK_CARD_BLOCK_LIST_KEY, copyContentKey: BUILTIN_DINGTALK_CARD_COPY_CONTENT_KEY, }); - diff --git a/src/gateway/channel-gateway.ts b/src/gateway/channel-gateway.ts index 73390a04..94c2a87f 100644 --- a/src/gateway/channel-gateway.ts +++ b/src/gateway/channel-gateway.ts @@ -1,6 +1,7 @@ import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream"; import { analyzeCardCallback } from "../card-callback-service"; import { handleCardAction } from "../card/card-action-handler"; +import { tryHandleApprovalCallback } from "../approval/approval-callback-handler"; import { finalizeActiveCardsForAccount, recoverPendingCardsForAccount, @@ -380,6 +381,15 @@ export function createDingTalkGateway(): NonNullable ({ markMessageProcessedMock: vi.fn(), handleDingTalkMessageMock: vi.fn(), sendProactiveTextMock: vi.fn(), + tryHandleApprovalCallbackMock: vi.fn(), connectionConfig: undefined as any, })); @@ -70,6 +71,10 @@ vi.mock('../../src/send-service', () => ({ uploadMedia: vi.fn(), })); +vi.mock('../../src/approval/approval-callback-handler', () => ({ + tryHandleApprovalCallback: shared.tryHandleApprovalCallbackMock, +})); + import { CHANNEL_INFLIGHT_NAMESPACE_POLICY, dingtalkPlugin } from '../../src/channel'; const startGatewayAccount = (ctx: any) => dingtalkPlugin.gateway!.startAccount!(ctx); @@ -113,6 +118,7 @@ describe('gateway inbound callback pipeline', () => { shared.markMessageProcessedMock.mockReset(); shared.handleDingTalkMessageMock.mockReset(); shared.sendProactiveTextMock.mockReset(); + shared.tryHandleApprovalCallbackMock.mockReset().mockResolvedValue({ handled: false }); shared.connectionConfig = undefined; shared.listeners = {}; @@ -366,6 +372,40 @@ describe('gateway inbound callback pipeline', () => { expect(shared.socketCallBackResponseMock).toHaveBeenCalledWith('card_callback_1', { success: true }); }); + it('routes approval card callbacks before regular card actions', async () => { + shared.tryHandleApprovalCallbackMock.mockResolvedValueOnce({ handled: true, reason: 'resolved' }); + const ctx = createStartContext(); + + await startGatewayAccount(ctx as any); + await shared.listeners.TOPIC_CARD?.({ + headers: { messageId: 'card_callback_approval' }, + data: JSON.stringify({ + outTrackId: 'ot1', + userId: 'staffA', + content: JSON.stringify({ + cardPrivateData: { + actionIds: ['allow-once'], + params: { action: 'allow-once', approveId: 'abc123' }, + }, + }), + }), + }); + + expect(shared.tryHandleApprovalCallbackMock).toHaveBeenCalledWith( + expect.objectContaining({ + accountId: 'main', + analysis: expect.objectContaining({ + actionId: 'allow-once', + outTrackId: 'ot1', + cardPrivateData: expect.objectContaining({ + params: { action: 'allow-once', approveId: 'abc123' }, + }), + }), + }), + ); + expect(shared.socketCallBackResponseMock).toHaveBeenCalledWith('card_callback_approval', { success: true }); + }); + it('clears account in-flight locks on disconnect state change', async () => { shared.isMessageProcessedMock.mockReturnValue(false); let resolveFirst: (() => void) | undefined; diff --git a/tests/unit/approval-callback-handler.test.ts b/tests/unit/approval-callback-handler.test.ts new file mode 100644 index 00000000..a56242b0 --- /dev/null +++ b/tests/unit/approval-callback-handler.test.ts @@ -0,0 +1,187 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../../src/approval/approval-resolver", () => ({ + resolveApproval: vi.fn(), +})); +vi.mock("../../src/approval/approval-card-patcher", () => ({ + applyResolvedPatch: vi.fn().mockResolvedValue(undefined), + applyExpiredPatch: vi.fn().mockResolvedValue(undefined), +})); +vi.mock("../../src/card/card-run-registry", () => ({ + resolveCardRun: vi.fn(), + isActiveCardRun: vi.fn(() => true), +})); +vi.mock("../../src/send-service", () => ({ + sendProactiveTextOrMarkdown: vi.fn().mockResolvedValue({ ok: true }), +})); +vi.mock("../../src/auth", () => ({ + getAccessToken: vi.fn().mockResolvedValue("tok-xxx"), +})); +vi.mock("../../src/config", () => ({ + getConfig: vi.fn(() => ({ clientId: "x", bypassProxyForSend: false })), +})); + +const { tryHandleApprovalCallback } = await import("../../src/approval/approval-callback-handler"); +const { resolveApproval } = await import("../../src/approval/approval-resolver"); +const { applyExpiredPatch, applyResolvedPatch } = await import( + "../../src/approval/approval-card-patcher" +); +const { resolveCardRun } = await import("../../src/card/card-run-registry"); +const { sendProactiveTextOrMarkdown } = await import("../../src/send-service"); +const { getAccessToken } = await import("../../src/auth"); + +const mockResolve = vi.mocked(resolveApproval); +const mockApplyResolved = vi.mocked(applyResolvedPatch); +const mockApplyExpired = vi.mocked(applyExpiredPatch); +const mockResolveCard = vi.mocked(resolveCardRun); +const mockSend = vi.mocked(sendProactiveTextOrMarkdown); +const mockGetAccessToken = vi.mocked(getAccessToken); + +const base = { cfg: {} as never, accountId: "default", log: undefined }; + +function analysis(overrides: Record = {}) { + return { + summary: "allow-once", + actionId: "allow-once", + userId: "staffA", + outTrackId: "ai_card_xxx", + cardPrivateData: { + actionIds: ["allow-once"], + params: { action: "allow-once", approveId: "abc123" }, + }, + ...overrides, + } as never; +} + +describe("approval-callback-handler", () => { + beforeEach(() => { + mockResolve.mockReset().mockResolvedValue({ ok: true }); + mockApplyResolved.mockReset().mockResolvedValue(undefined); + mockApplyExpired.mockReset().mockResolvedValue(undefined); + mockResolveCard.mockReset().mockReturnValue({ + outTrackId: "ai_card_xxx", + card: { state: "2" }, + } as never); + mockSend.mockReset().mockResolvedValue({ ok: true } as never); + mockGetAccessToken.mockReset().mockResolvedValue("tok-xxx"); + }); + + it("ignores non approval callbacks", async () => { + const result = await tryHandleApprovalCallback({ + ...base, + analysis: analysis({ actionId: "feedback_up", cardPrivateData: undefined }), + }); + + expect(result.handled).toBe(false); + expect(mockResolve).not.toHaveBeenCalled(); + }); + + it("resolves approval from params.action and params.approveId", async () => { + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + + expect(mockResolve).toHaveBeenCalledWith( + expect.objectContaining({ + approvalId: "abc123", + decision: "allow-once", + senderId: "staffA", + }), + ); + expect(mockApplyResolved).toHaveBeenCalledWith( + "ai_card_xxx", + "allow-once", + "tok-xxx", + true, + expect.objectContaining({ clientId: "x" }), + ); + }); + + it("falls back to registry pendingApprovalId when callback lacks approveId", async () => { + mockResolveCard.mockReturnValue({ pendingApprovalId: "from-registry", card: { state: "2" } } as never); + + await tryHandleApprovalCallback({ + ...base, + analysis: analysis({ + cardPrivateData: { actionIds: ["allow-once"], params: { action: "allow-once" } }, + }), + }); + + expect(mockResolve).toHaveBeenCalledWith(expect.objectContaining({ approvalId: "from-registry" })); + }); + + it("expires the card when approvalId cannot be resolved", async () => { + mockResolveCard.mockReturnValue(null); + + const result = await tryHandleApprovalCallback({ + ...base, + analysis: analysis({ + cardPrivateData: { actionIds: ["allow-once"], params: { action: "allow-once" } }, + }), + }); + + expect(result).toEqual({ handled: true, reason: "missing-approval-id" }); + expect(mockApplyExpired).toHaveBeenCalled(); + expect(mockResolve).not.toHaveBeenCalled(); + }); + + it("uses action id fallback for decision", async () => { + await tryHandleApprovalCallback({ + ...base, + analysis: analysis({ cardPrivateData: { actionIds: ["deny"], params: { approveId: "abc123" } } }), + }); + + expect(mockResolve).toHaveBeenCalledWith(expect.objectContaining({ decision: "deny" })); + }); + + it("keeps card pending and sends private hint for unauthorized users", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "unauthorized" }); + + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:staffA", + expect.stringContaining("无权"), + expect.objectContaining({ forceMarkdown: true }), + ); + expect(mockApplyResolved).not.toHaveBeenCalled(); + expect(mockApplyExpired).not.toHaveBeenCalled(); + }); + + it("keeps card pending and sends allowed decision hint for invalid decisions", async () => { + mockResolve.mockResolvedValue({ + ok: false, + reason: "invalid-decision", + allowedDecisions: ["allow-once", "deny"], + }); + + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:staffA", + expect.stringMatching(/allow-once.*deny/), + expect.objectContaining({ forceMarkdown: true }), + ); + expect(mockApplyResolved).not.toHaveBeenCalled(); + expect(mockApplyExpired).not.toHaveBeenCalled(); + }); + + it("does not send an invalid user target when callback userId is missing", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "unauthorized" }); + + await tryHandleApprovalCallback({ ...base, analysis: analysis({ userId: undefined }) }); + + expect(mockSend).not.toHaveBeenCalled(); + }); + + it.each(["already-resolved", "not-found", "gateway-error"] as const)( + "expires the card for %s", + async (reason) => { + mockResolve.mockResolvedValue({ ok: false, reason }); + + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + + expect(mockApplyExpired).toHaveBeenCalled(); + }, + ); +}); diff --git a/tests/unit/approval-capability.test.ts b/tests/unit/approval-capability.test.ts index 4c8929d2..a1130d89 100644 --- a/tests/unit/approval-capability.test.ts +++ b/tests/unit/approval-capability.test.ts @@ -4,6 +4,10 @@ vi.mock("openclaw/plugin-sdk/approval-delivery-runtime", () => ({ createApproverRestrictedNativeApprovalCapability: vi.fn(() => ({ mock: "capability" })), })); +vi.mock("../../src/approval/approval-native-runtime", () => ({ + createDingTalkApprovalNativeRuntime: vi.fn(() => ({ marker: "native-runtime" })), +})); + const { createDingTalkApprovalCapability } = await import("../../src/approval/approval-capability"); const { createApproverRestrictedNativeApprovalCapability } = await import( "openclaw/plugin-sdk/approval-delivery-runtime" @@ -36,10 +40,10 @@ describe("createDingTalkApprovalCapability", () => { expect(args?.resolveApproverDmTargets).toBeUndefined(); }); - it("does not attach nativeRuntime in PR-1", () => { + it("attaches nativeRuntime for channel-native approval delivery", () => { createDingTalkApprovalCapability(); - expect(mockFactory.mock.calls.at(-1)?.[0].nativeRuntime).toBeUndefined(); + expect(mockFactory.mock.calls.at(-1)?.[0].nativeRuntime).toEqual({ marker: "native-runtime" }); }); it("does not implement resolveApproveCommandBehavior because DingTalk intercepts early", () => { diff --git a/tests/unit/approval-card-patcher.test.ts b/tests/unit/approval-card-patcher.test.ts new file mode 100644 index 00000000..0d750223 --- /dev/null +++ b/tests/unit/approval-card-patcher.test.ts @@ -0,0 +1,77 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../../src/card-callback-service", () => ({ + updateCardVariables: vi.fn().mockResolvedValue(200), +})); + +vi.mock("../../src/card/card-run-registry", () => ({ + markCardRunPendingApproval: vi.fn(), + clearCardRunPendingApproval: vi.fn(), +})); + +const { applyExpiredPatch, applyPendingPatch, applyResolvedPatch } = await import( + "../../src/approval/approval-card-patcher" +); +const { updateCardVariables } = await import("../../src/card-callback-service"); +const { clearCardRunPendingApproval, markCardRunPendingApproval } = await import( + "../../src/card/card-run-registry" +); + +const mockUpdate = vi.mocked(updateCardVariables); +const mockMark = vi.mocked(markCardRunPendingApproval); +const mockClear = vi.mocked(clearCardRunPendingApproval); + +describe("approval-card-patcher", () => { + beforeEach(() => { + mockUpdate.mockReset().mockResolvedValue(200); + mockMark.mockReset(); + mockClear.mockReset(); + }); + + it("applies pending card variables and records fallback approval id", async () => { + await applyPendingPatch("ot1", "abc123", "tok", { bypassProxyForSend: true }); + + expect(mockUpdate).toHaveBeenCalledWith( + "ot1", + { show_approve_btns: "true", approveId: "abc123", hasAction: "false" }, + "tok", + { bypassProxyForSend: true }, + ); + expect(mockMark).toHaveBeenCalledWith("ot1", "abc123"); + }); + + it("applies resolved variables and clears fallback approval id", async () => { + await applyResolvedPatch("ot1", "allow-once", "tok", true, {}); + + expect(mockUpdate).toHaveBeenCalledWith( + "ot1", + { show_approve_btns: "false", approveId: "", hasAction: "true" }, + "tok", + {}, + ); + expect(mockClear).toHaveBeenCalledWith("ot1"); + }); + + it("does not restore stop action for inactive resolved cards", async () => { + await applyResolvedPatch("ot1", "deny", "tok", false, {}); + + expect(mockUpdate).toHaveBeenCalledWith( + "ot1", + { show_approve_btns: "false", approveId: "", hasAction: "false" }, + "tok", + {}, + ); + }); + + it("applies expired variables using the same cleared field set", async () => { + await applyExpiredPatch("ot1", "tok", false, {}); + + expect(mockUpdate).toHaveBeenCalledWith( + "ot1", + { show_approve_btns: "false", approveId: "", hasAction: "false" }, + "tok", + {}, + ); + expect(mockClear).toHaveBeenCalledWith("ot1"); + }); +}); diff --git a/tests/unit/approval-card-state.test.ts b/tests/unit/approval-card-state.test.ts new file mode 100644 index 00000000..a0a7fda9 --- /dev/null +++ b/tests/unit/approval-card-state.test.ts @@ -0,0 +1,55 @@ +import { describe, expect, it } from "vitest"; +import { + APPROVAL_CARD_INITIAL, + APPROVAL_CARD_KEYS, + buildApprovalClearedCardParams, + buildApprovalPendingCardParams, +} from "../../src/approval/approval-card-state"; + +describe("approval-card-state", () => { + it("centralizes the DingTalk cardParamMap key names", () => { + expect(APPROVAL_CARD_KEYS).toEqual({ + showApproveBtns: "show_approve_btns", + approveId: "approveId", + hasAction: "hasAction", + }); + }); + + it("builds pending approval card variables", () => { + expect(buildApprovalPendingCardParams("abc123")).toEqual({ + show_approve_btns: "true", + approveId: "abc123", + hasAction: "false", + }); + }); + + it("builds cleared variables and restores stop action for active cards", () => { + expect(buildApprovalClearedCardParams(true)).toEqual({ + show_approve_btns: "false", + approveId: "", + hasAction: "true", + }); + }); + + it("builds cleared variables without restoring stop action for inactive cards", () => { + expect(buildApprovalClearedCardParams(false)).toEqual({ + show_approve_btns: "false", + approveId: "", + hasAction: "false", + }); + }); + + it("does not include final-state presentation fields in v1", () => { + const params = buildApprovalClearedCardParams(true); + expect(params).not.toHaveProperty("status"); + expect(params).not.toHaveProperty("statusFooter"); + expect(params).not.toHaveProperty("approval_status"); + }); + + it("exports initial approval defaults for regular AI cards", () => { + expect(APPROVAL_CARD_INITIAL).toEqual({ + show_approve_btns: "false", + approveId: "", + }); + }); +}); diff --git a/tests/unit/approval-markdown-render.test.ts b/tests/unit/approval-markdown-render.test.ts new file mode 100644 index 00000000..c7c974b9 --- /dev/null +++ b/tests/unit/approval-markdown-render.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from "vitest"; +import { + buildExecApprovalMarkdown, + buildPluginApprovalMarkdown, +} from "../../src/approval/approval-markdown-render"; + +const NOW = Date.parse("2026-05-19T10:00:00Z"); + +function execRequest(payload: Record = {}, overrides: Record = {}) { + return { + id: "abc123", + createdAtMs: NOW - 1000, + expiresAtMs: NOW + 10 * 60_000, + request: { + command: 'docker image prune -a -f --filter "until=720h"', + cwd: "/Users/zhumin/projects/openclaw", + ...payload, + }, + ...overrides, + } as never; +} + +function pluginRequest(payload: Record = {}, overrides: Record = {}) { + return { + id: "plugin:xyz789", + createdAtMs: NOW - 1000, + expiresAtMs: NOW + 10 * 60_000, + request: { + title: "数据库查询", + toolName: "query_database", + description: "对 production.orders 表查询近 7 天订单", + ...payload, + }, + ...overrides, + } as never; +} + +describe("approval-markdown-render", () => { + it("renders exec id, command preview, cwd, expiry, and all default decisions", () => { + const markdown = buildExecApprovalMarkdown(execRequest(), NOW); + + expect(markdown).toContain("abc123"); + expect(markdown).toMatch(/```[\s\S]*docker image prune/); + expect(markdown).toContain("/approve abc123 allow-once"); + expect(markdown).toContain("/approve abc123 allow-always"); + expect(markdown).toContain("/approve abc123 deny"); + expect(markdown).toMatch(/10\s*分钟/); + }); + + it("uses upstream exec decision filtering", () => { + const markdown = buildExecApprovalMarkdown(execRequest({ allowedDecisions: ["deny"] }), NOW); + + expect(markdown).toContain("/approve abc123 deny"); + expect(markdown).not.toContain("/approve abc123 allow-once"); + expect(markdown).not.toContain("/approve abc123 allow-always"); + }); + + it("does not render allow-always for ask=always exec requests", () => { + const markdown = buildExecApprovalMarkdown(execRequest({ ask: "always" }), NOW); + + expect(markdown).toContain("/approve abc123 allow-once"); + expect(markdown).toContain("/approve abc123 deny"); + expect(markdown).not.toContain("/approve abc123 allow-always"); + }); + + it("renders plugin id, tool, description, and decisions", () => { + const markdown = buildPluginApprovalMarkdown(pluginRequest(), NOW); + + expect(markdown).toContain("plugin:xyz789"); + expect(markdown).toContain("query_database"); + expect(markdown).toContain("production.orders"); + expect(markdown).toContain("/approve plugin:xyz789 allow-once"); + expect(markdown).toContain("/approve plugin:xyz789 allow-always"); + expect(markdown).toContain("/approve plugin:xyz789 deny"); + }); + + it("filters plugin decisions locally to match upstream semantics", () => { + const markdown = buildPluginApprovalMarkdown( + pluginRequest({ allowedDecisions: ["allow-once"] }), + NOW, + ); + + expect(markdown).toContain("/approve plugin:xyz789 allow-once"); + expect(markdown).not.toContain("/approve plugin:xyz789 allow-always"); + expect(markdown).not.toContain("/approve plugin:xyz789 deny"); + }); + + it("omits expired negative minute hints", () => { + expect(buildPluginApprovalMarkdown(pluginRequest({}, { expiresAtMs: NOW - 1000 }), NOW)).not.toMatch( + /-?\d+\s*分钟/, + ); + }); +}); diff --git a/tests/unit/approval-native-runtime.test.ts b/tests/unit/approval-native-runtime.test.ts new file mode 100644 index 00000000..62ae6d9f --- /dev/null +++ b/tests/unit/approval-native-runtime.test.ts @@ -0,0 +1,229 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../../src/approval/approval-config", () => ({ + getExecApprovalsConfig: vi.fn(() => ({ isNativeDeliveryEnabled: true })), + listExecApprovers: vi.fn(() => ["staffA"]), +})); +vi.mock("../../src/approval/approval-card-locator", () => ({ + findActiveAgentCard: vi.fn(), +})); +vi.mock("../../src/approval/approval-card-patcher", () => ({ + applyPendingPatch: vi.fn().mockResolvedValue(undefined), + applyResolvedPatch: vi.fn().mockResolvedValue(undefined), + applyExpiredPatch: vi.fn().mockResolvedValue(undefined), +})); +vi.mock("../../src/approval/approval-markdown-render", () => ({ + buildExecApprovalMarkdown: vi.fn(() => "exec-md"), + buildPluginApprovalMarkdown: vi.fn(() => "plugin-md"), +})); +vi.mock("../../src/card/card-run-registry", () => ({ + resolveCardRun: vi.fn(), + isActiveCardRun: vi.fn(() => true), +})); +vi.mock("../../src/send-service", () => ({ + sendProactiveTextOrMarkdown: vi.fn().mockResolvedValue({ ok: true }), +})); +vi.mock("../../src/auth", () => ({ + getAccessToken: vi.fn().mockResolvedValue("tok"), +})); +vi.mock("../../src/config", () => ({ + getConfig: vi.fn(() => ({ clientId: "x", bypassProxyForSend: false })), +})); +vi.mock("../../src/logger-context", () => ({ + getLogger: vi.fn(() => ({ info: vi.fn(), warn: vi.fn() })), +})); + +const { createDingTalkApprovalNativeRuntime } = await import( + "../../src/approval/approval-native-runtime" +); +const { getExecApprovalsConfig, listExecApprovers } = await import( + "../../src/approval/approval-config" +); +const { findActiveAgentCard } = await import("../../src/approval/approval-card-locator"); +const { applyExpiredPatch, applyPendingPatch, applyResolvedPatch } = await import( + "../../src/approval/approval-card-patcher" +); +const { sendProactiveTextOrMarkdown } = await import("../../src/send-service"); +const { resolveCardRun, isActiveCardRun } = await import("../../src/card/card-run-registry"); +const { getAccessToken } = await import("../../src/auth"); + +const mockGetApprovalsConfig = vi.mocked(getExecApprovalsConfig); +const mockListApprovers = vi.mocked(listExecApprovers); +const mockFindActiveCard = vi.mocked(findActiveAgentCard); +const mockPending = vi.mocked(applyPendingPatch); +const mockResolved = vi.mocked(applyResolvedPatch); +const mockExpired = vi.mocked(applyExpiredPatch); +const mockSend = vi.mocked(sendProactiveTextOrMarkdown); +const mockResolveCardRun = vi.mocked(resolveCardRun); +const mockIsActiveCardRun = vi.mocked(isActiveCardRun); +const mockGetAccessToken = vi.mocked(getAccessToken); + +function request(payload: Record = {}) { + return { + id: "abc123", + createdAtMs: Date.now() - 1000, + expiresAtMs: Date.now() + 60_000, + request: { + command: "rm -rf tmp", + sessionKey: "session-A", + turnSourceChannel: "dingtalk", + turnSourceTo: "group:cid_xxx", + turnSourceAccountId: "default", + ...payload, + }, + } as never; +} + +describe("approval-native-runtime", () => { + const runtime = createDingTalkApprovalNativeRuntime(); + + beforeEach(() => { + mockGetApprovalsConfig.mockReset().mockReturnValue({ isNativeDeliveryEnabled: true } as never); + mockListApprovers.mockReset().mockReturnValue(["staffA"]); + mockFindActiveCard.mockReset(); + mockPending.mockReset().mockResolvedValue(undefined); + mockResolved.mockReset().mockResolvedValue(undefined); + mockExpired.mockReset().mockResolvedValue(undefined); + mockSend.mockReset().mockResolvedValue({ ok: true } as never); + mockResolveCardRun.mockReset().mockReturnValue({ card: { state: "2" } } as never); + mockIsActiveCardRun.mockReset().mockReturnValue(true); + mockGetAccessToken.mockReset().mockResolvedValue("tok"); + }); + + it("handles only configured DingTalk origin requests with approvers", () => { + expect(runtime.availability.shouldHandle({ cfg: {} as never, accountId: "default", request: request() })).toBe(true); + expect( + runtime.availability.shouldHandle({ + cfg: {} as never, + accountId: "default", + request: request({ turnSourceChannel: "discord" }), + }), + ).toBe(false); + + mockListApprovers.mockReturnValue([]); + expect(runtime.availability.shouldHandle({ cfg: {} as never, accountId: "default", request: request() })).toBe(false); + }); + + it("builds pending payload using the upstream approval kind", async () => { + await expect(Promise.resolve( + runtime.presentation.buildPendingPayload({ + cfg: {} as never, + request: request(), + approvalKind: "exec", + nowMs: Date.now(), + view: {} as never, + }), + )).resolves.toEqual({ approvalId: "abc123", markdownText: "exec-md" }); + }); + + it("prepareTarget returns the required { dedupeKey, target } wrapper for card route", () => { + mockFindActiveCard.mockReturnValue({ outTrackId: "ot1", sessionKey: "session-A" }); + + const prepared = runtime.transport.prepareTarget({ + cfg: {} as never, + accountId: "default", + plannedTarget: { surface: "origin", target: { to: "group:cid_xxx" } }, + request: request(), + approvalKind: "exec", + pendingPayload: { approvalId: "abc123", markdownText: "md" }, + view: {} as never, + } as never); + + expect(prepared).toEqual({ + dedupeKey: "dingtalk:default:group:cid_xxx:ot1", + target: { + route: "card", + to: "group:cid_xxx", + accountId: "default", + activeCardOutTrackId: "ot1", + }, + }); + }); + + it("delivers pending approval by patching active card", async () => { + const entry = await runtime.transport.deliverPending({ + cfg: {} as never, + accountId: "default", + plannedTarget: { surface: "origin", target: { to: "group:cid_xxx" } }, + preparedTarget: { + route: "card", + to: "group:cid_xxx", + accountId: "default", + activeCardOutTrackId: "ot1", + }, + request: request(), + approvalKind: "exec", + pendingPayload: { approvalId: "abc123", markdownText: "md" }, + view: {} as never, + } as never); + + expect(mockPending).toHaveBeenCalledWith("ot1", "abc123", "tok", expect.objectContaining({ clientId: "x" })); + expect(entry).toEqual({ mode: "card", approvalId: "abc123", accountId: "default", outTrackId: "ot1" }); + }); + + it("falls back to markdown on explicit card patch failure", async () => { + mockPending.mockRejectedValueOnce(Object.assign(new Error("400"), { status: 400 })); + + const entry = await runtime.transport.deliverPending({ + cfg: {} as never, + preparedTarget: { + route: "card", + to: "group:cid_xxx", + accountId: "default", + activeCardOutTrackId: "ot1", + }, + pendingPayload: { approvalId: "abc123", markdownText: "md" }, + } as never); + + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "group:cid_xxx", + "md", + expect.objectContaining({ forceMarkdown: true }), + ); + expect(entry).toEqual({ mode: "markdown", approvalId: "abc123", accountId: "default" }); + }); + + it("does not duplicate-send markdown when card patch outcome is ambiguous", async () => { + mockPending.mockRejectedValueOnce(Object.assign(new Error("timeout"), { code: "ETIMEDOUT" })); + + const entry = await runtime.transport.deliverPending({ + cfg: {} as never, + preparedTarget: { + route: "card", + to: "group:cid_xxx", + accountId: "default", + activeCardOutTrackId: "ot1", + }, + pendingPayload: { approvalId: "abc123", markdownText: "md" }, + } as never); + + expect(entry).toBeNull(); + expect(mockSend).not.toHaveBeenCalled(); + }); + + it("updates card entries and no-ops markdown entries", async () => { + await runtime.transport.updateEntry?.({ + cfg: {} as never, + entry: { mode: "card", approvalId: "abc123", accountId: "default", outTrackId: "ot1" }, + payload: { phase: "resolved", decision: "allow-once" }, + phase: "resolved", + } as never); + + expect(mockResolved).toHaveBeenCalledWith( + "ot1", + "allow-once", + "tok", + true, + expect.objectContaining({ clientId: "x" }), + ); + + await runtime.transport.updateEntry?.({ + cfg: {} as never, + entry: { mode: "markdown", approvalId: "abc123", accountId: "default" }, + payload: { phase: "resolved", decision: "allow-once" }, + phase: "resolved", + } as never); + expect(mockExpired).not.toHaveBeenCalled(); + }); +}); diff --git a/tests/unit/card-callback-service.test.ts b/tests/unit/card-callback-service.test.ts index 3f1439de..5378ccec 100644 --- a/tests/unit/card-callback-service.test.ts +++ b/tests/unit/card-callback-service.test.ts @@ -74,4 +74,24 @@ describe("card-callback-service", () => { expect(analysis.userId).toBe("user_2"); expect(analysis.spaceId).toBe("space_2"); }); + + it("exposes cardPrivateData actionIds and params from nested callback payloads", () => { + const analysis = analyzeCardCallback({ + content: JSON.stringify({ + cardPrivateData: { + actionIds: ["allow-once"], + params: { action: "allow-once", approveId: "abc123" }, + }, + }), + }); + + expect(analysis.cardPrivateData).toEqual({ + actionIds: ["allow-once"], + params: { action: "allow-once", approveId: "abc123" }, + }); + }); + + it("leaves cardPrivateData undefined when callback does not contain it", () => { + expect(analyzeCardCallback({ actionValue: "btn_stop" }).cardPrivateData).toBeUndefined(); + }); }); diff --git a/tests/unit/card-run-registry-approval.test.ts b/tests/unit/card-run-registry-approval.test.ts index 6a6a9387..8f2c2c47 100644 --- a/tests/unit/card-run-registry-approval.test.ts +++ b/tests/unit/card-run-registry-approval.test.ts @@ -1,8 +1,11 @@ import { beforeEach, describe, expect, it } from "vitest"; import { clearCardRunRegistryForTest, + clearCardRunPendingApproval, isActiveCardRun, + markCardRunPendingApproval, registerCardRun, + resolveCardRun, resolveActiveCardRunBySession, type CardRunRecord, } from "../../src/card/card-run-registry"; @@ -99,4 +102,14 @@ describe("card-run-registry · approval helpers", () => { expect(resolveActiveCardRunBySession("default", "session-A")?.outTrackId).toBe("out-new"); }); + + it("marks and clears pendingApprovalId for callback fallback", () => { + register("out-active", { sessionKey: "session-A", state: AICardStatus.INPUTING }); + + markCardRunPendingApproval(" out-active ", " approval-123 "); + expect(resolveCardRun("out-active")?.pendingApprovalId).toBe("approval-123"); + + clearCardRunPendingApproval(" out-active "); + expect(resolveCardRun("out-active")?.pendingApprovalId).toBeUndefined(); + }); }); diff --git a/tests/unit/card-service.test.ts b/tests/unit/card-service.test.ts index feefc92e..77a90d0a 100644 --- a/tests/unit/card-service.test.ts +++ b/tests/unit/card-service.test.ts @@ -101,6 +101,8 @@ describe('card-service', () => { config: '{"autoLayout":true,"enableForward":true}', content: '', flowStatus: '2', + show_approve_btns: 'false', + approveId: '', hasAction: 'true', stop_action: 'true', quoteContent: '', From d01b7ec26f8b495435d0738672e6c00920c9d8c7 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:33:12 +0800 Subject: [PATCH 27/44] docs(approval): document DingTalk native approval --- README.md | 6 +- docs/releases/latest.md | 2 +- docs/releases/v3.6.4.md | 33 +++++++++ docs/user/features/ai-card.md | 8 ++- docs/user/features/exec-approval.md | 101 +++++++++++++++++++++++++++ docs/user/index.md | 1 + docs/user/reference/configuration.md | 21 ++++++ package.json | 2 +- 8 files changed, 167 insertions(+), 7 deletions(-) create mode 100644 docs/releases/v3.6.4.md create mode 100644 docs/user/features/exec-approval.md diff --git a/README.md b/README.md index 73639302..d4610978 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ # DingTalk Channel for OpenClaw

    - OpenClaw + OpenClaw npm version npm downloads License @@ -22,6 +22,7 @@ - 支持引用消息恢复和常见文本附件正文抽取 - 支持 Markdown 回复与 AI 卡片流式回复(v2 结构化 block 渲染、taskInfo 元数据、图片内联) - 支持多 Agent、多机器人绑定和实验性的 `@多助手路由` +- 支持 OpenClaw exec/plugin approval 在钉钉内审批(AI Card 按钮或 `/approve` 命令) - 支持 `/btw` 旁路问答,绕过主会话锁立即获得独立快答 - 支持 DingTalk Device Flow 自动注册,扫码授权后自动获取凭证,无需手动复制 - 支持实时中止当前 AI generation。常用停止指令包括 `停止`、`stop`、`/stop`、`esc` 等 @@ -46,7 +47,7 @@ ## 安装 > [!IMPORTANT] -> 最小兼容版本为 `OpenClaw 2026.3.24`。安装前请先升级到最新版 OpenClaw。 +> 最小兼容版本为 `OpenClaw 2026.4.7`。安装前请先升级到最新版 OpenClaw。 ```bash openclaw plugins install @soimy/dingtalk @@ -143,6 +144,7 @@ openclaw configure --section channels - [消息类型支持](docs/user/features/message-types.md) - [回复模式](docs/user/features/reply-modes.md) - [AI 卡片](docs/user/features/ai-card.md) +- [DingTalk Native Approval](docs/user/features/exec-approval.md) - [/btw 旁路问答](docs/user/features/btw.md) - [钉钉文档 API](docs/user/features/dingtalk-docs-api.md) - [反馈学习](docs/user/features/feedback-learning.md) diff --git a/docs/releases/latest.md b/docs/releases/latest.md index 1d09913e..4069bf56 100644 --- a/docs/releases/latest.md +++ b/docs/releases/latest.md @@ -1,2 +1,2 @@ - + diff --git a/docs/releases/v3.6.4.md b/docs/releases/v3.6.4.md new file mode 100644 index 00000000..90d954af --- /dev/null +++ b/docs/releases/v3.6.4.md @@ -0,0 +1,33 @@ +# v3.6.4 发布说明 + +本版本新增 DingTalk Native Approval,并将 OpenClaw 兼容基线提升到 `2026.4.7+`。 + +## 新功能 + +### DingTalk Native Approval + +- 支持 OpenClaw exec approval 与 plugin approval 在钉钉内处理。 +- AI Card 模式下复用当前 agent reply card,显示 `允许一次`、`总是允许`、`拒绝` 三个原生按钮。 +- Markdown 路径发送 `/approve ` 命令模板。 +- `/approve` 命令在 DingTalk inbound 阶段提前拦截,避免被原 agent 会话锁阻塞。 +- 审批结果统一经过 OpenClaw gateway resolver,并区分 unauthorized、not-found、already-resolved、invalid-decision、gateway-error。 + +## 内部改动 + +- AI Card 默认模板升级为 v3,新增 `show_approve_btns` 与 `approveId` 变量,并继续保留既有 `hasAction` stop 控制字段。 +- 新增 approval card patcher,统一 pending、resolved、expired 三种 cardParamMap 变更。 +- `CardCallbackAnalysis` 暴露 `cardPrivateData`,用于读取按钮 action 与 `approveId`。 +- card-run registry 新增 `pendingApprovalId` fallback,仅用于异常回调兜底。 +- channel capability 接入 OpenClaw `nativeRuntime`,按 card / markdown 双路由投递 pending approval。 + +## 升级说明 + +- 最低 OpenClaw 版本为 `2026.4.7`。 +- 如使用自定义卡片模板,需要基于 v3 模板重新导入或确保模板包含 approval 按钮组、`show_approve_btns` 和 `approveId`。 +- 详细配置见 [DingTalk Native Approval](../user/features/exec-approval.md)。 + +## 已知限制 + +- v1 只支持 origin chat 投递,不做 approver DM fan-out。 +- 卡片终态只隐藏按钮,不写入“已批准 / 已拒绝”状态文案。 +- 钉钉真机模板回调仍应在发布前抽检,确认 `cardPrivateData.params.approveId` 可回传。 diff --git a/docs/user/features/ai-card.md b/docs/user/features/ai-card.md index 1f9d2f5b..7914a3fb 100644 --- a/docs/user/features/ai-card.md +++ b/docs/user/features/ai-card.md @@ -6,7 +6,7 @@ AI 卡片模式是钉钉插件最有辨识度的回复方式,基于结构化 b 插件使用统一的预置卡片模板,无需用户配置 `cardTemplateId` / `cardTemplateKey`。 -如需覆盖预置模板 ID,可通过环境变量 `DINGTALK_CARD_TEMPLATE_ID` 设置,默认值为 `675cde2f-f526-40cb-b828-f5b2b57b8b77.schema`。 +如需覆盖预置模板 ID,可通过环境变量 `DINGTALK_CARD_TEMPLATE_ID` 设置,默认值为 `58f73932-fc3b-46ae-8e90-93313e405061.schema`。 AI 卡片生命周期: @@ -119,13 +119,15 @@ AI 卡片生命周期: 如需在预置模板基础上定制卡片样式或新增自定义变量/组件,可以参考以下资产文件,将修改后的模板上传到钉钉开放平台,再通过 `DINGTALK_CARD_TEMPLATE_ID` 环境变量指向新模板 ID: -- **[`card-template-v2.json`](../../assets/card-template-v2.json)** — 当前预置卡片模板的完整低代码 schema(钉钉卡片搭建器导出格式),包含组件映射、组件树、数据源和交互定义,可直接导入钉钉卡片搭建器进行编辑。 +- **[`card-template-v3.json`](../../assets/card-template-v3.json)** — 当前预置卡片模板的完整低代码 schema(钉钉卡片搭建器导出格式),包含组件映射、组件树、数据源、AI Card 展示与 Native Approval 按钮组,可直接导入钉钉卡片搭建器进行编辑。 +- **[`card-template-v2.json`](../../assets/card-template-v2.json)** — 旧版模板,仅用于历史参考。 - **[`card-data-mock-v2.json`](../../assets/card-data-mock-v2.json)** — 卡片渲染时的 mock 数据样例,展示 `blockList`、`content`、`quoteContent`、`statusLine` 等变量结构和取值,方便在搭建器中预览卡片效果。 -定制时注意保持变量 key 与插件输出字段的对齐:`content`、`blockList`、`quoteContent`、`copy_content`、`statusLine`、`hasAction` 等。 +定制时注意保持变量 key 与插件输出字段的对齐:`content`、`blockList`、`quoteContent`、`copy_content`、`statusLine`、`hasAction`、`show_approve_btns`、`approveId` 等。 ## 相关文档 - [回复模式](reply-modes.md) +- [DingTalk Native Approval](exec-approval.md) - [API 消耗说明](../reference/api-usage-and-cost.md) - [配置项参考](../reference/configuration.md) diff --git a/docs/user/features/exec-approval.md b/docs/user/features/exec-approval.md new file mode 100644 index 00000000..2f72db38 --- /dev/null +++ b/docs/user/features/exec-approval.md @@ -0,0 +1,101 @@ +# DingTalk Native Approval + +Native Approval allows OpenClaw approval requests to be handled directly from DingTalk. +It supports command execution approvals and plugin approvals. + +## Enable + +Configure approvers with DingTalk staff IDs. Prefixes such as `dingtalk:`, `dd:`, and `ding:` are optional. + +```json5 +{ + "channels": { + "dingtalk": { + "messageType": "card", + "execApprovals": { + "enabled": "auto", + "approvers": ["staff-id-1", "staff-id-2"] + } + } + } +} +``` + +If `execApprovals.approvers` is empty, the plugin falls back to `commands.ownerAllowFrom`. + +```json5 +{ + "commands": { + "ownerAllowFrom": ["staff-id-1"] + }, + "channels": { + "dingtalk": { + "execApprovals": { + "enabled": true + } + } + } +} +``` + +Set `enabled` to `false` to disable DingTalk native delivery even when approvers are configured. + +## Interaction + +### AI Card Mode + +When `messageType` is `card` and an active AI Card exists for the same DingTalk session, the plugin patches the existing card and shows three buttons: + +- `允许一次`: approve once +- `总是允许`: approve always, when the request allows it +- `拒绝`: deny + +While approval is pending, the normal stop button is hidden. After the approval is resolved or expired, approval buttons are removed. If the agent is still streaming, the stop button is restored. + +### Markdown Mode + +When there is no active AI Card, the plugin sends a Markdown approval message with copyable commands: + +```text +/approve allow-once +/approve allow-always +/approve deny +``` + +Only decisions allowed by the OpenClaw request are shown. + +### Command Fallback + +Approvers can also send `/approve` manually in DingTalk: + +```text +/approve abc123 allow-once +/approve allow-once abc123 +``` + +The command path is intercepted before the normal agent session lock, so it can resolve a pending approval while the original agent turn is paused. + +## Configuration + +| Option | Type | Default | Description | +| --- | --- | --- | --- | +| `execApprovals.enabled` | `boolean \| "auto"` | `"auto"` | `false` disables native delivery. `true` and `"auto"` enable it when approvers exist. | +| `execApprovals.approvers` | `string[]` | `[]` | DingTalk staff IDs allowed to approve. Falls back to `commands.ownerAllowFrom` when empty. | + +## Limits + +- v1 is origin-only: approvals are delivered back to the DingTalk chat that initiated the agent turn. Dedicated approver DM fan-out is not implemented yet. +- The card path removes buttons after resolution but does not write a final approval status line into the card. +- The card callback normally carries `cardPrivateData.params.approveId`. A process-local registry is kept only as a fallback for old cards or abnormal callback payloads. +- Real-device validation is still required after changing the DingTalk low-code card template or overriding `DINGTALK_CARD_TEMPLATE_ID`. + +## Troubleshooting + +- If no approval prompt appears, confirm `execApprovals.approvers` or `commands.ownerAllowFrom` contains the DingTalk staff ID of the approver. +- If the card buttons do not appear, confirm the runtime uses the v3 card template and `messageType` is `card`. +- If `/approve` says the decision is unsupported, choose one of the decisions shown in the Markdown or private hint. + +## Related + +- [AI Card](ai-card.md) +- [Configuration](../reference/configuration.md) diff --git a/docs/user/index.md b/docs/user/index.md index 12e0c4fb..dc2208e3 100644 --- a/docs/user/index.md +++ b/docs/user/index.md @@ -30,6 +30,7 @@ - [消息类型支持](features/message-types.md) - [回复模式](features/reply-modes.md) - [AI 卡片](features/ai-card.md) +- [DingTalk Native Approval](features/exec-approval.md) - [钉钉文档 API](features/dingtalk-docs-api.md) - [反馈学习](features/feedback-learning.md) - [/btw 旁路问答](features/btw.md) diff --git a/docs/user/reference/configuration.md b/docs/user/reference/configuration.md index 89f85624..2c49d464 100644 --- a/docs/user/reference/configuration.md +++ b/docs/user/reference/configuration.md @@ -29,6 +29,8 @@ | `cardStreamingMode` | string | `off`(生效值) | 卡片流式模式:`off` / `answer` / `all` | | `cardStreamInterval` | number | `1000` | 卡片实时更新节奏(毫秒,最小 `200`) | | `cardAtSender` | string | - | 群聊中卡片完成后追加 @发送者 的消息文本;非空时生效 | +| `execApprovals.enabled` | boolean \| `"auto"` | `"auto"` | 是否启用 DingTalk Native Approval;`false` 强制关闭 | +| `execApprovals.approvers` | string[] | `[]` | 允许审批的 DingTalk staffId 列表;为空时回退到 `commands.ownerAllowFrom` | | `cardRealTimeStream` | boolean | `false` | 已弃用;仅兼容旧配置,`true` 会回退到 `cardStreamingMode: all` | | `aicardDegradeMs` | number | `1800000` | 卡片连续失败后的降级时间 | | `debug` | boolean | `false` | 是否输出调试日志 | @@ -144,6 +146,25 @@ SecretInput 对象字段: - 同时设置时,以 `cardStreamingMode` 为准。 - `cardStreamInterval` 控制实时更新节奏(毫秒),在 `answer` / `all` 下生效;值越小,更新越频繁,API 调用通常越高。 +## 关于 `execApprovals` + +`execApprovals` 用于 DingTalk Native Approval。配置 approver 后,OpenClaw 的 exec/plugin approval 可以通过钉钉 AI Card 按钮或 `/approve` 命令处理。 + +```json5 +{ + "channels": { + "dingtalk": { + "execApprovals": { + "enabled": "auto", + "approvers": ["staff-id-1"] + } + } + } +} +``` + +更多交互方式与限制见 [DingTalk Native Approval](../features/exec-approval.md)。 + ## 关于连接参数 连接相关配置用于提升 Stream 连接鲁棒性: diff --git a/package.json b/package.json index 3b29106e..32a4b81e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@soimy/dingtalk", - "version": "3.6.3", + "version": "3.6.4", "description": "DingTalk (钉钉) channel plugin for OpenClaw", "keywords": [ "bot", From a119389b3362d189e2d6746261c66f025120906f Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 13:37:21 +0800 Subject: [PATCH 28/44] fix(approval): align native approval verification fixes --- src/approval/approval-markdown-render.ts | 2 +- tests/unit/docs-homepage-badges.test.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/approval/approval-markdown-render.ts b/src/approval/approval-markdown-render.ts index 920990b5..4d6e21b0 100644 --- a/src/approval/approval-markdown-render.ts +++ b/src/approval/approval-markdown-render.ts @@ -22,7 +22,7 @@ function formatExpireHint(expiresAtMs: number | undefined, nowMs: number): strin } function normalizePluginAllowedDecisions( - allowedDecisions?: readonly (ApprovalDecision | string)[] | null, + allowedDecisions?: readonly string[] | null, ): readonly ApprovalDecision[] { if (!Array.isArray(allowedDecisions)) { return ALL_DECISIONS; diff --git a/tests/unit/docs-homepage-badges.test.ts b/tests/unit/docs-homepage-badges.test.ts index b12a63b6..4e4bd116 100644 --- a/tests/unit/docs-homepage-badges.test.ts +++ b/tests/unit/docs-homepage-badges.test.ts @@ -14,7 +14,7 @@ describe("docs homepage badge layout", () => { expect(readme).toContain('

    '); expect(badgeBlockMatch).not.toBeNull(); expect(badgeAnchors).toHaveLength(5); - expect(badgeBlock).toContain("img.shields.io/badge/OpenClaw-%3E%3D2026.3.24-0A7CFF"); + expect(badgeBlock).toContain("img.shields.io/badge/OpenClaw-%3E%3D2026.4.7-0A7CFF"); expect(badgeBlock).toContain("img.shields.io/npm/v/%40soimy%2Fdingtalk"); expect(badgeBlock).toContain("img.shields.io/npm/dm/%40soimy%2Fdingtalk"); expect(badgeBlock).toContain("img.shields.io/github/license/soimy/openclaw-channel-dingtalk"); From 004ea4fdf2cd99016483e9426910396469611eac Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 14:19:50 +0800 Subject: [PATCH 29/44] fix(approval): enforce plugin approval authorization --- docs/user/features/exec-approval.md | 1 + src/approval/approval-card-locator.ts | 4 +++ src/approval/approval-config.ts | 1 + src/approval/approval-markdown-render.ts | 4 +-- src/approval/approval-native-runtime.ts | 13 ++++----- src/approval/approval-resolver.ts | 8 ++--- tests/unit/approval-card-locator.test.ts | 34 ++++++++++++++++++++++ tests/unit/approval-native-runtime.test.ts | 32 +++++++++++++++++++- tests/unit/approval-resolver.test.ts | 14 +++++++++ 9 files changed, 96 insertions(+), 15 deletions(-) diff --git a/docs/user/features/exec-approval.md b/docs/user/features/exec-approval.md index 2f72db38..99d6e0bb 100644 --- a/docs/user/features/exec-approval.md +++ b/docs/user/features/exec-approval.md @@ -74,6 +74,7 @@ Approvers can also send `/approve` manually in DingTalk: ``` The command path is intercepted before the normal agent session lock, so it can resolve a pending approval while the original agent turn is paused. +Decision aliases are also accepted: `allow`, `once`, and `allowonce` map to `allow-once`; `always` and `allowalways` map to `allow-always`; `reject` and `block` map to `deny`. ## Configuration diff --git a/src/approval/approval-card-locator.ts b/src/approval/approval-card-locator.ts index ce1b4698..066d0af9 100644 --- a/src/approval/approval-card-locator.ts +++ b/src/approval/approval-card-locator.ts @@ -5,6 +5,7 @@ export interface FindActiveAgentCardInput { cfg: OpenClawConfig; accountId: string; sessionKey: string; + approvalId?: string; } export interface ActiveAgentCardLocation { @@ -20,5 +21,8 @@ export function findActiveAgentCard(input: FindActiveAgentCardInput): ActiveAgen if (!record) { return null; } + if (record.pendingApprovalId && record.pendingApprovalId !== input.approvalId) { + return null; + } return { outTrackId: record.outTrackId, sessionKey: record.sessionKey }; } diff --git a/src/approval/approval-config.ts b/src/approval/approval-config.ts index c0a9947b..e23014a6 100644 --- a/src/approval/approval-config.ts +++ b/src/approval/approval-config.ts @@ -66,6 +66,7 @@ export function isExecAuthorizedSender({ export function isPluginAuthorizedSender( query: ApprovalConfigQuery & { senderId: string }, ): boolean { + // v1 intentionally shares the exec approver list for plugin approvals. return isExecAuthorizedSender(query); } diff --git a/src/approval/approval-markdown-render.ts b/src/approval/approval-markdown-render.ts index 4d6e21b0..6b27cca3 100644 --- a/src/approval/approval-markdown-render.ts +++ b/src/approval/approval-markdown-render.ts @@ -8,8 +8,8 @@ import type { ApprovalDecision } from "../types"; const ALL_DECISIONS: readonly ApprovalDecision[] = ["allow-once", "allow-always", "deny"]; const DECISION_LABEL: Record = { - "allow-once": "批准(仅一次)", - "allow-always": "批准(总是)", + "allow-once": "允许一次", + "allow-always": "总是允许", deny: "拒绝", }; diff --git a/src/approval/approval-native-runtime.ts b/src/approval/approval-native-runtime.ts index d5ce0924..02935309 100644 --- a/src/approval/approval-native-runtime.ts +++ b/src/approval/approval-native-runtime.ts @@ -59,9 +59,8 @@ function isExplicitHttpFailure(error: unknown): boolean { return (typeof status === "number" && status >= 400) || candidate?.code === "EBADREQ"; } -function isAmbiguousDeliveryFailure(error: unknown): boolean { - const code = (error as { code?: string } | null)?.code; - return code === "ETIMEDOUT" || code === "ECONNRESET" || code === "ECONNABORTED"; +function shouldFallbackToMarkdown(error: unknown): boolean { + return isExplicitHttpFailure(error); } function isSuccessfulSendResult(result: unknown): boolean { @@ -126,10 +125,11 @@ export function createDingTalkApprovalNativeRuntime(): ChannelApprovalNativeRunt cfg, accountId: resolvedAccountId, sessionKey: request.request.sessionKey ?? "", + approvalId: request.id, }); if (activeCard) { return { - dedupeKey: `dingtalk:${resolvedAccountId}:${to}:${activeCard.outTrackId}`, + dedupeKey: `dingtalk:${resolvedAccountId}:${to}:${activeCard.outTrackId}:${request.id}`, target: { route: "card", to, @@ -166,10 +166,7 @@ export function createDingTalkApprovalNativeRuntime(): ChannelApprovalNativeRunt outTrackId: preparedTarget.activeCardOutTrackId, }; } catch (error) { - if (isAmbiguousDeliveryFailure(error)) { - return null; - } - if (!isExplicitHttpFailure(error)) { + if (!shouldFallbackToMarkdown(error)) { return null; } } diff --git a/src/approval/approval-resolver.ts b/src/approval/approval-resolver.ts index c4839724..2c4fa726 100644 --- a/src/approval/approval-resolver.ts +++ b/src/approval/approval-resolver.ts @@ -69,6 +69,9 @@ function deriveGatewayParams(params: { }): | { resolveMethod?: "plugin"; allowPluginFallback?: boolean } | null { + if (!params.execAuthorized && !params.pluginAuthorized) { + return null; + } if (params.approvalId.startsWith("plugin:")) { return { resolveMethod: "plugin" }; } @@ -78,10 +81,7 @@ function deriveGatewayParams(params: { if (params.pluginAuthorized) { return { resolveMethod: "plugin" }; } - if (params.execAuthorized) { - return { allowPluginFallback: false }; - } - return null; + return { allowPluginFallback: false }; } export async function resolveApproval(input: ResolveApprovalInput): Promise { diff --git a/tests/unit/approval-card-locator.test.ts b/tests/unit/approval-card-locator.test.ts index 68df94d2..b20ecfc4 100644 --- a/tests/unit/approval-card-locator.test.ts +++ b/tests/unit/approval-card-locator.test.ts @@ -37,6 +37,40 @@ describe("approval-card-locator", () => { expect(mockResolveActiveCardRunBySession).not.toHaveBeenCalled(); }); + it("returns null when the active card already has a different pending approval", () => { + mockResolveActiveCardRunBySession.mockReturnValue({ + outTrackId: "ai_card_xxx", + sessionKey: "session-A", + pendingApprovalId: "approval-old", + } as never); + + expect( + findActiveAgentCard({ + cfg: {} as never, + accountId: "default", + sessionKey: "session-A", + approvalId: "approval-new", + }), + ).toBeNull(); + }); + + it("allows retrying the same pending approval on the active card", () => { + mockResolveActiveCardRunBySession.mockReturnValue({ + outTrackId: "ai_card_xxx", + sessionKey: "session-A", + pendingApprovalId: "approval-old", + } as never); + + expect( + findActiveAgentCard({ + cfg: {} as never, + accountId: "default", + sessionKey: "session-A", + approvalId: "approval-old", + }), + ).toEqual({ outTrackId: "ai_card_xxx", sessionKey: "session-A" }); + }); + it("passes accountId through to the registry", () => { mockResolveActiveCardRunBySession.mockReturnValue(null); diff --git a/tests/unit/approval-native-runtime.test.ts b/tests/unit/approval-native-runtime.test.ts index 62ae6d9f..c696f5da 100644 --- a/tests/unit/approval-native-runtime.test.ts +++ b/tests/unit/approval-native-runtime.test.ts @@ -130,7 +130,7 @@ describe("approval-native-runtime", () => { } as never); expect(prepared).toEqual({ - dedupeKey: "dingtalk:default:group:cid_xxx:ot1", + dedupeKey: "dingtalk:default:group:cid_xxx:ot1:abc123", target: { route: "card", to: "group:cid_xxx", @@ -138,6 +138,36 @@ describe("approval-native-runtime", () => { activeCardOutTrackId: "ot1", }, }); + expect(mockFindActiveCard).toHaveBeenCalledWith( + expect.objectContaining({ approvalId: "abc123" }), + ); + }); + + it("includes approval id in card dedupeKey so concurrent approvals on one card stay distinct", () => { + mockFindActiveCard.mockReturnValue({ outTrackId: "ot1", sessionKey: "session-A" }); + + const first = runtime.transport.prepareTarget({ + cfg: {} as never, + accountId: "default", + plannedTarget: { surface: "origin", target: { to: "group:cid_xxx" } }, + request: request(), + approvalKind: "exec", + pendingPayload: { approvalId: "first", markdownText: "md" }, + view: {} as never, + } as never); + const second = runtime.transport.prepareTarget({ + cfg: {} as never, + accountId: "default", + plannedTarget: { surface: "origin", target: { to: "group:cid_xxx" } }, + request: { ...request(), id: "second" }, + approvalKind: "exec", + pendingPayload: { approvalId: "second", markdownText: "md" }, + view: {} as never, + } as never); + + expect(first?.dedupeKey).not.toBe(second?.dedupeKey); + expect(first?.dedupeKey).toBe("dingtalk:default:group:cid_xxx:ot1:abc123"); + expect(second?.dedupeKey).toBe("dingtalk:default:group:cid_xxx:ot1:second"); }); it("delivers pending approval by patching active card", async () => { diff --git a/tests/unit/approval-resolver.test.ts b/tests/unit/approval-resolver.test.ts index 701923ff..1dafdac9 100644 --- a/tests/unit/approval-resolver.test.ts +++ b/tests/unit/approval-resolver.test.ts @@ -86,6 +86,20 @@ describe("approval-resolver · method derivation", () => { expect(result).toEqual({ ok: false, reason: "unauthorized" }); expect(mockGateway).not.toHaveBeenCalled(); }); + + it("returns unauthorized for plugin-prefixed approval IDs when neither auth check passes", async () => { + mockExecAuth.mockReturnValue(false); + mockPluginAuth.mockReturnValue(false); + + const result = await resolveApproval({ + ...base, + approvalId: "plugin:abc", + decision: "allow-once", + }); + + expect(result).toEqual({ ok: false, reason: "unauthorized" }); + expect(mockGateway).not.toHaveBeenCalled(); + }); }); describe("approval-resolver · error classification", () => { From 8c77f053fa53ac4e9d5bc2fa0c56aefe03376b85 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 15:57:02 +0800 Subject: [PATCH 30/44] fix(approval): harden native approval handling --- ...6-05-18-gap-01-approval-native-design.html | 609 +- .../2026-05-19-gap-01-approval-native.md | 4955 +++++++++++++++++ src/approval/approval-callback-handler.ts | 61 +- src/approval/approval-command-intercept.ts | 2 + src/approval/approval-resolver.ts | 2 + tests/unit/approval-callback-handler.test.ts | 45 +- tests/unit/approval-command-intercept.test.ts | 9 +- 7 files changed, 5528 insertions(+), 155 deletions(-) create mode 100644 docs/plans/2026-05-19-gap-01-approval-native.md diff --git a/docs/features/2026-05-18-gap-01-approval-native-design.html b/docs/features/2026-05-18-gap-01-approval-native-design.html index 51f85de2..5d15de16 100644 --- a/docs/features/2026-05-18-gap-01-approval-native-design.html +++ b/docs/features/2026-05-18-gap-01-approval-native-design.html @@ -14,13 +14,14 @@

    Gap #01 · DingTalk Native Approval 设计方案

    为 DingTalk Channel 接入 OpenClaw 的 原生审批能力(exec approval + plugin approval)。v1 实现 ChannelApprovalCapability 与 native runtime 4 个 sub-adapter(availability/presentation/transport/observe;interactions 推迟到 v2)。审批 v1 仅投递到 origin 会话(agent 触发的钉钉群/私聊),approver DM 双投递推迟到 v2。
    v3.3 关键架构:approval 不创建独立卡片,而是按 card-run-registry 实际状态分两路由—— - (card 路径) 若 agent 正在 AI Card 流式输出,把 3 个 approval 按钮挂到原 agent reply card 上(PUT 更新 cardParamMap.btns); + (card 路径) 若 agent 正在 AI Card 流式输出,在原 agent reply card 上 PUT cardParamMap 三个变量(show_approve_btns / hasAction / approveId,参 §1.X 单一事实表)显示 approval 按钮组(按钮定义模板内置); (markdown 路径) 若没有 active card(markdown 模式、card 创建失败降级、plugin approval 无 reply card 等),发独立 markdown 消息含 /approve <id> <decision> 命令模板。 按钮点击与 /approve 文本命令两条入口都收敛到 approval-resolver 单一抽象,再调上游 resolveApprovalOverGateway

    P0 · 核心缺口 平台能力 CONFIRMED + spec v3.12 基线 v3.6.3 设计日期 2026-05-18 作者 zhumin + Claude Opus 4.7 @@ -51,13 +52,89 @@

    1.1 v1 范围明确不做的事

  • 不做 approver-DM 投递(D4 v3 修订)——v1 仅 origin-only,DM 双投递推迟到 v2 且 config-gated
  • 不引入本地 approval-store(D18 v3 新增)——依赖上游 core 的 activeEntries,channel 不复制 pending state
  • 不做停机 finalize-on-stop(D13 v3 推迟)——D18 删除 store 后失去枚举能力;遗留卡片在用户点击时降级为"已过期/已关闭"
  • -
  • 不抽通用 action dispatcher / registry —— peer 三家都没做,approval 走自己的回调前缀分支,与 feedback_up/down / btn_stop 在 TOPIC_CARD listener 中同级并列
  • +
  • 不抽通用 action dispatcher / registry —— peer 三家都没做,approval 走专用 approval 分支(按 params.action / actionId 精确匹配 allow-once / allow-always / deny 命中),与 feedback_up/down / btn_stop 在 TOPIC_CARD listener 中同级并列
  • 不动 btn_stop 与 feedback 既有路径(向后兼容,零回归风险)
  • v3.3 修订删除原条目"不为 markdown 模式做主路径文案"——v3.3 起 markdown 路径就是主路径之一,必须教用户用 /approve 命令完成审批
  • 不做重启后主动 rebind pending approval(v1 范围;用户点过期/失效按钮时显式降级提示)
  • 不引入 select / input / datepicker 等高级组件——仅 button(已 CONFIRMED 平台支持)
  • +

    1.X · 单一事实表(Single Source of Truth · v3.10 新增)

    +
    + 本表是 channel 与 v3 卡片模板交互的全部字段集——下文所有 section 必须与本表一致。任何差异以本表为准。 +
    +
    + + + + + + + + + + + + + + + + + + +
    场景cardParamMap PUT 字段集备注
    pending · approval 出现时
    (card 路径 deliverPending / applyPendingPatch)
    + show_approve_btns: "true"
    + approveId: <approval.id>
    + hasAction: "false" +
    3 个变量同时 PUT;按钮 actionId/params 模板内置,channel 不构造按钮数组
    resolved · 用户点了按钮或敲了命令
    (applyResolvedPatch · §6.3 step 5 / §6.4 mode=card)
    + show_approve_btns: "false"
    + approveId: ""
    + hasAction: cardStillActive ? "true" : "false" +
    v1 不写终态文字("已批准 by..."等);用户感知"按钮消失=已处理"
    expired · 上游 TTL 过期 / not-found / already-resolved 兜底
    (applyExpiredPatch · §6.3 step 4 catch / §6.6 expired event)
    + show_approve_btns: "false"
    + approveId: ""
    + hasAction: cardStillActive ? "true" : "false" +
    与 resolved 同字段集,语义不同(不写终态文字同上)
    + + + + + + + + + + + + + + + + + + + + + +
    环节值约定
    Callback params(钉钉回传) + action: "allow-once" | "allow-always" | "deny"(静态值,按钮 actionId 即语义)
    + approveId: <approval.id>(变量绑定,PUT 时设的字符串原样回传) +
    approvalId 解码主链路 + analysis.cardPrivateData?.params?.approveId(v3.6 D24 主链路;callback 自带) +
    approvalId 解码 fallback + resolveCardRun(outTrackId)?.pendingApprovalId(仅当 callback 没带 approveId 时反查;进程重启 / 多 worker / TTL sweep 都会丢,所以仅作兜底) +
    cardStillActive 判断 + record.card?.state ∈ {PROCESSING, INPUTING}(建议用 card-run-registry 提供的 isActiveCardRun(record) helper 封装) +
    + +
    + 本表 v1 范围明示: + (a) channel 永远只 PUT 上面列的字段,不传 btns / status / statusFooter 等已废弃字段; + (b) 按钮组 actionId / params 都在模板内置,channel 不构造按钮数组; + (c) v1 不实现 allowedDecisions 动态隐藏按钮(§11.1 已坦率列限制); + (d) markdown 路径不在本表——它走 sendProactiveTextOrMarkdown(forceMarkdown:true),不触卡片 cardParamMap。 +
    +

    1.2 按钮 payload 编码与解码(D15/D24 落地,v3.5 对齐用户实配 schema)

    @@ -76,48 +153,78 @@

    编码(v2 模板内置的按钮组定义)

    // event: // type: "sendCardRequest" // ActionId: "allow-once" -// 回传参数: [{ 参数名: "action", 参数类型: 静态值, 参数值: "allow-once" }] +// 回传参数: [ +// { 参数名: "action", 参数类型: 静态值, 参数值: "allow-once" }, +// { 参数名: "approveId", 参数类型: 变量, 参数值: "approveId" } // ← D24 绑定到同名变量 +// ] // // 按钮 2(总是允许): -// ActionId: "allow-always", params={action:"allow-always"} +// ActionId: "allow-always" +// 回传参数: [ +// { 参数名: "action", 参数类型: 静态值, 参数值: "allow-always" }, +// { 参数名: "approveId", 参数类型: 变量, 参数值: "approveId" } +// ] // // 按钮 3(拒绝): -// ActionId: "deny", params={action:"deny"} +// ActionId: "deny" +// 回传参数: [ +// { 参数名: "action", 参数类型: 静态值, 参数值: "deny" }, +// { 参数名: "approveId", 参数类型: 变量, 参数值: "approveId" } +// ] // // 显示控制(条件计算):show_approve_btns 的值为 true → 整组可见 -// channel 端 patch 时只需 PUT 两个变量: +// channel 端 patch 时 PUT 三个变量(D24 v3.6 主链路 + §1.X 单一事实表): updateCardVariables(outTrackId, { show_approve_btns: "true", // ← 显示 approval 按钮组(D23) hasAction: "false", // ← 隐藏 btn_stop(D23) + approveId: "", // ← D24 主链路:approvalId 通过此变量 + // 绑定到三按钮 params,callback 自带 }, token) -// 三个按钮的 actionId / params 都在 template 里固定,channel 不传 +// 三个按钮的 actionId(allow-once / allow-always / deny)和静态参数 action 都在 template 里固定, +// channel 不传按钮定义 -

    回调实测形态(用户配 schema 后的预期)

    +

    回调实测形态(v3.7 用户实配 schema 确认)

    // 用户点"允许一次"按钮,平台 push 的 callback data.content:
     {
       "cardPrivateData": {
         "actionIds": ["allow-once"],          // ← 唯一命名,无 index 后缀
    -    "params":    { "action": "allow-once" }
    +    "params":    {
    +      "action":    "allow-once",          // ← 静态值,按钮 actionId 即语义
    +      "approveId": "abc123"               // ← D24 主链路:channel patch 时设的值原样回传
    +    }
       },
       "outTrackId":  "ai_card_xxx",           // ← agent reply card 的 id
       "userId":      "staffA",                 // ← clicker staffId
       "spaceType":   "im" 或 "group",
       ...
     }
    -// v3.6+ 主链路:approvalId 已通过 approveId 变量绑定到按钮 params,
    -// callback 自带;D24 registry 反查仅作 fallback(详见解码段)
    +// D24 v3.6+ 解码主链路:直接读 cardPrivateData.params.approveId +// Fallback:仅当 callback 不带 approveId 时反查 resolveCardRun(outTrackId).pendingApprovalId

    解码(callback 入口)

    // 第一步:扩展后的 analyzeCardCallback 把 cardPrivateData 整体放进 analysis
     const cpd = analysis.cardPrivateData;          // { actionIds, params }
     
     // 第二步:parseApprovalFromCardPrivateData(cpd)
    -//   actionId 用 exact match(唯一命名无后缀);fallback 用 params.action 兜底
    -//   (万一未来 schema 改动让 actionId 出现后缀也不会断)
    -if (!cpd?.params?.action) return null;
    -const action = cpd.params.action;
    -if (!["allow-once", "allow-always", "deny"].includes(action)) return null;
    +//   主链路:params.action(v3 模板按钮回传静态值;D15 实测形态)
    +//   Fallback:cpd.actionIds[0] / analysis.actionId 精确匹配三种 decision
    +//             (应对未来 schema 变更让 params.action 字段丢失)
    +const ALLOWED = ["allow-once", "allow-always", "deny"] as const;
    +
    +// 主链路:优先读 params.action
    +const fromParams = typeof cpd?.params?.action === "string" ? cpd.params.action : null;
    +
    +// Fallback:actionIds[0] 或 analysis.actionId 精确匹配
    +const fromActionId = (() => {
    +  const aid = (Array.isArray(cpd?.actionIds) && typeof cpd.actionIds[0] === "string")
    +    ? cpd.actionIds[0]
    +    : (typeof analysis.actionId === "string" ? analysis.actionId : null);
    +  return aid && (ALLOWED as readonly string[]).includes(aid) ? aid : null;
    +})();
    +
    +const action = fromParams ?? fromActionId;
    +if (!action || !(ALLOWED as readonly string[]).includes(action)) return null;
     const decision = action as "allow-once" | "allow-always" | "deny";
     
     // 第三步(D24 v3.6):approvalId 主链路从 cardPrivateData.params.approveId 直接取;
    @@ -256,7 +363,7 @@ 

    2. 已确认的决策清单

    D14 终态展示(v3.3 修订:原 agent card patch) - card 路径kind: "update"——PUT /v1.0/card/instances 改原 agent card 的 cardParamMap(清除 approval 按钮 + 写终态指示 "✅ 已批准 by @user · allow-once" / "ℹ️ 已处理或已过期" 到 agent card 的某个变量位)。agent card 本身的状态机(PROCESSING/INPUTING/FINISHED)不被 approval 触碰,approval 只对 cardParamMap 做字段级 patch。
    具体写入位置(candidate):append 到 contentKey 末尾、或写到独立变量 approvalStatus、或加 system block——PR-2 实施时按 AI Card v2 模板现有支持决定,可能轻微调整模板。 + card 路径kind: "update"——PUT /v1.0/card/instances 改原 agent card 的 cardParamMap(参 §1.X 单一事实表:show_approve_btns:"false" + approveId:"" + hasAction 按 cardStillActive 恢复)。v1 不写终态文字("✅ 已批准 by..." / "ℹ️ 已处理或已过期" 等)——schema 无干净字段位(§7.1 已坦率列限制),用户感知"按钮消失=已处理"。
    agent card 本身的状态机(PROCESSING/INPUTING/FINISHED)不被 approval 触碰,approval 只对 cardParamMap 三个变量做字段级 patch。
    v1.x 升级路径:让维护者在模板加 approval_status 变量后再 PUT 写入"✅ 已批准 by @<name> · <decision>"。
    markdown 路径:v1 不发终态通知消息(markdown 不能 edit,发新消息会刷屏;用户从命令成功的事实自然感知。v2 future 可视用户反馈再加) v3.3 用户拍板 + Slack patch 模式参考 @@ -316,9 +423,9 @@

    2. 已确认的决策清单

    approval kind 推导规则(v3.2 修订 D20 子规则) 3 段判断(按 Slack/Telegram 模式):
    (1) approvalId.startsWith("plugin:"){ resolveMethod: "plugin" } -
    (2) 无前缀 + exec 与 plugin 都授权 → { resolveMethod: "exec", allowPluginFallback: true }(让 resolveApprovalOverGateway 在 exec store 找不到时回退尝试 plugin store) +
    (2) 无前缀 + exec 与 plugin 都授权 → { allowPluginFallback: true }(不传 resolveMethod 即默认 exec;让 resolveApprovalOverGateway 在 exec store 找不到时回退尝试 plugin store)
    (3) 无前缀 + 仅 plugin 授权 → { resolveMethod: "plugin" } -
    (4) 无前缀 + 仅 exec 授权 → { resolveMethod: "exec" } +
    (4) 无前缀 + 仅 exec 授权 → { allowPluginFallback: false }(不传 resolveMethod,默认 exec)
    (5) 都未授权 → 拒绝(私聊提示 + 不调 gateway) 对齐上游 Slack/Telegram 当前做法 @@ -381,6 +488,42 @@

    2. 已确认的决策清单

    (6) §10 PR-1 措辞改"resolve 通道生效,approval id 可见性依赖外部界面,完整 UX 在 PR-2"; (7) §6.7 删除 store.register 残留; (8) §6.3 / §6.6 / §8 / §9 等所有 already-resolved 文案改"ℹ️ 已处理或已过期"并明确 catch 后 return 避免覆盖;§6.8 alias 范围显式列清;§11.2 风险表新增 kind 派发边界条目
  • +
  • v3.12(2026-05-19 第十二轮 review · 6 处剩余一致性 fix): + (1) §1.2 解码段注释与代码不一致——v3.11 注释说"actionId 用 exact match;fallback 用 params.action",但代码实际 if (!cpd?.params?.action) return null params.action 才是主链路。v3.12 改注释 + 代码加 fallback:主链路 params.action,缺失 fallback actionIds[0] / analysis.actionId 精确匹配三种 decision(应对未来 schema 变更让 params.action 字段丢失); + (2) §6.7 callback-handler 模块表"TOPIC_CARD 入口(前缀 'approval' 命中)" + §1.1 "回调前缀分支" 是旧按钮命名假设(v3.2 时按钮叫 approve0/1/2)。改为"按 params.action / actionId 精确匹配 allow-once / allow-always / deny 命中";同时把 callback-handler 模块描述里的"按 result.ok 决定终态 update(resolved)或 catch update(ℹ️ 已处理或已过期)"展开为 5 类 reason 完整分支表(含 v3.11 新增的 invalid-decision); + (3) §7.3 双状态机段"approval 只 patch 2 个 Boolean cardParamMap 字段"老语义改为 "三个变量(show_approve_btns / approveId / hasAction,参 §1.X)";§6.6 Channel 重启用例手写 PUT { show_approve_btns: "false" } 改为 applyExpiredPatch 引用 §1.X;§6.6 stop-time-finalize 推迟说明里"卡片刷成 ℹ️ 已处理或已过期" 改为"调 applyExpiredPatch + v1 不写终态文字"; + (4) §8 错误矩阵 not-found 行"callback-handler 更新卡片为 ℹ️ 已处理或已过期"老语义改为"调 applyExpiredPatch;v1 不写终态文字"; + (5) §9 测试矩阵补 invalid-decision 覆盖——approval-resolver.test 错误分类从 3 类(已 / 不存在 / gateway)扩到 5 类(+ unauthorized + invalid-decision),列出两种 invalid-decision details 形态 + helper 单测;approval-callback-handler.test 加 invalid-decision 分支验证(不调 applyExpiredPatch、按钮保持 pending、调 sendProactiveTextOrMarkdown 含 allowedDecisions 文案);approval-command-intercept.test 从"resolve-failed 仅 log"扩展为按 reason 5 分支处理(含 invalid-decision 私聊重选提示);integration #10 channel 重启用例 mock not-found 后断言"PUT 三变量 + 无终态文字 PUT"; + (6) §7.1 模板字段表头"两个新增变量 + 一个 ButtonGroup" 容易让人把 approve_btns 误解为 PUT 变量。改为精确表述:"2 个 cardParamMap 变量(show_approve_btns + approveId,channel 需 PUT)+ 1 个内置 ButtonGroup(approve_btns,按钮组定义,不属 PUT 字段集)"
  • +
  • v3.11(2026-05-19 第十一轮 review · 1 处 v1 策略重新设计 + 6 处实施一致性 fix): + (1 · 策略重新设计 · 最关键) allowedDecisions 失败路径重新设计——v3.10 写"点了不允许 decision → applyExpiredPatch 清按钮",但下一句又说"再点正确 decision",按钮已经没了自相矛盾,且让用户卡死。 +
    核实上游 openclaw/src/gateway/server-methods/exec-approval.ts:45-46,468 + plugin-approval.ts:184,200:错误形态明确(exec details.reason="APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" / plugin details.allowedDecisions=[...]gatewayCode="INVALID_REQUEST")。 +
    v3.11 新策略:approval-resolver 新增 reason="invalid-decision" 错误分类 + isInvalidApprovalDecisionError(err) helper;callback handler / command intercept 命中此分支时不调 applyExpiredPatch,私聊提示用户重选,卡片保持 pending;plugin 错误透传 allowedDecisions 数组生成精确提示文案; + (2) §6.3 step 4 catch 分支 / §6.4 mode=card / §6.6 重复点击三处手写 updateCardVariables 字段全部正向化——统一为"调 applyResolvedPatch / applyExpiredPatch,字段集见 §1.X 单一事实表",正文里不再出现手写 updateCardVariables 字面字段,避免漏字段或与单一事实表脱节; + (3) 模板按钮 params 示例补 approveId——§1.2 / §7.2 模板按钮 schema 之前只写 action 静态参数,与实际 docs/assets/card-template-v3.json(按钮 params 含 action + approveId 变量绑定)不一致,会误导制模/实现。两处全部改为 2-tuple;同时页眉 v3.3 关键架构里"PUT cardParamMap.btns"老语义改为"PUT 三变量(参 §1.X)"; + (4) src/card/card-template.ts 改否文档内部冲突——拓扑图 / §3.3 接触面表写"不修改",§10 阶段 0 又写"必须替换 templateId"。统一为"不新增 approval 专用模板,但必须替换内置 AI Card 模板 ID 为 v3"。改 §3.3 接触面表行明确:替换 src/card/card-template.ts:6 默认值(v2 ID → v3 ID),env 覆盖保留作开发期 escape; + (5) §3.3 src/config.ts 接触面指错函数——v3.10 强调 mergeAccountWithDefaults 必须显式拷贝,但实际它用 spread 自动保留。真正会丢字段的resolveDingTalkAccountsrc/config.ts:279-310)default-account 路径的 rawConfig 字面量列表——当前覆盖约 25 个字段但未含 execApprovals,必须显式补 execApprovals: dingtalk?.execApprovals,; + (6) §3.2 模块表 approval-native-runtime.ts 描述"实现 5 个子 adapter"残留(依赖列还有 render / store)——主线 v1 已明确 4 个 sub-adapter(availability/presentation/transport/observe),此处对齐为 4 个;依赖列改为实际依赖(card-locator / card-patcher / markdown-render / target-resolver / card-service),删除 render / store; + (7) §9 测试矩阵删 tests/unit/approval-card-render.test.ts 旧条目——line 1937 列出,line 1946 又说 approval-card-patcher.test.ts 替代它,删除避免实施时多写一套废弃测试
  • +
  • v3.10(2026-05-19 第十轮 review,结构性新增 + 10 处实施一致性 fix): + (0 · 结构性) 新增 §1.X 单一事实表(Single Source of Truth):把 pending / resolved / expired 三场景的 cardParamMap PUT 字段集 + callback params 约定 + approvalId 解码主链路与 fallback 全部固化在一张表里,作为后续所有 section 引用的 anchor。此表是 v3.10 后续口径漂移防御机制; + (1) §1.2 + §7.2 D24 主链路统一——清理"只 PUT 两个变量" / callback 示例缺 approveId / "approvalId 主要靠 registry 反查"等 v3.5 残留,统一为 "PUT 三变量 + callback 自带 approveId + registry 仅 fallback"; + (2) §5.3 deliverPending card 路径补 approveId——v3.9 修订只提及两变量,v3.10 明确三变量 PUT; + (3) §11.2 风险表清理 actionId.startsWith("approval") 旧按钮 ID 前缀 fallback(v3.2 残留)——改为优先 params.action + actionId 精确匹配三种 decision; + (4) 清理 §D14 / 拓扑图 / §5.3 updateEntry / §7.3 / §10 / 测试矩阵等多处"清按钮 + 写终态指示"老语义——统一为"toggle 三变量 + v1 不写终态文字"(§1.X 单一事实表为准); + (5) /approve 拦截路径补 forceMarkdown:true——malformed / unauthorized / not-found / already-resolved 三种私聊提示都要带,否则 messageType=card 配置下会被发成卡片(src/send-service.ts:371-393); + (6) §3.3 接触面表补三处真实改动面——src/types.ts(DingTalkConfig / ExecApprovalsConfig 接口)+ src/config.ts(mergeAccountWithDefaults 显式字段拷贝,对齐 allowFrom 模式)+ tests/unit/config.test.ts(account override 单测),否则 execApprovals 在部分账号路径可能被丢; + (7) §6.8 + §3.3 /approve intercept 位置精确到具体行号——必须早于 L817 sub-agent routing 分支(否则 @agent /approve 被 messageTarget=subagent-* 路由吞掉),晚于 L575 content extract + L671/729 授权通过,建议位置 L770; + (8) 多处"用户手敲 /approve 兜底"措辞收紧——明确不是天然兜底,是 out-of-band 操作(用户必须先从 CLI/WebUI/日志拿到 approval id 才能用此命令);钉钉端本身无 approval id 可见路径在 v1; + (9) §8 错误矩阵 + §9 测试矩阵补 allowedDecisions 边界 case——上游 exec ask="always" 时禁 allow-always,v1 模板固定三按钮全显示。明确策略:用户点不允许的 decision → gateway 拒绝 → 走 catch 分支 applyExpiredPatch(不 patch 成成功态,不重试 RPC); + (10) 历史背景与实施段落分离——§3.2 patcher 模块描述 / §3.3 types.ts 改动 / §5.2 buildPendingPayload 都改为正向描述当前做法("toggle 三变量" / "无按钮数据结构"),删 "v3.5 不再 X" 历史口吻;版本演进 callout 与 D17 / §10 阶段 0 保留 CardBtn / sendCardRequest 历史引用作为来源对照
  • +
  • v3.9(2026-05-19 第九轮 review,3 处 spec 残留 fix + 2 处文字精化): + (1) §6.3 step 2 同步 D24 v3.6 主链路——v3.8 修订列表声明已 fix"approvalId 不在 payload"老语义,但 §6.3 step 2 实际仍是 v3.5 单链路 registry 反查写法。v3.9 改为"主链路 cardPrivateData.params.approveId + fallback registry pendingApprovalId"两段式,与 §1.2 解码段一致; + (2) §6.3 step 4 catch 分支字段名清理——还在用 v3.3 的 { status, hasAction, btns: "[]" }(status / btns 字段在 v3 模板都不存在,§7.1 已坦率列限制)。改为统一调 approval-card-patcher.applyExpiredPatch(outTrackId, token, cardStillActive),与 §6.4 / §6.6 上游过期事件路径一致; + (3) §6.3 step 5 resolved update 同样替换已废弃的 buildResolvedCardParamMap,改用 approval-card-patcher.applyResolvedPatch(outTrackId, decision, token, cardStillActive) — v3.5+ patcher 已统一封装 show_approve_btns / hasAction / approveId 字段集,§6.3 step 5 不能再绕过 patcher 直 PUT; + (4) §6.8 regex 与上游 openclaw/src/auto-reply/reply/commands-approve.ts:16 完全对齐 /^\/?approve\b/i(前导斜杠可选)—— 之前的 /^\/approve\b/i 强制 /,会让"用户敲裸 approve abc once"绕过 channel intercept 进入 normal pipeline 触发 dispatcher → acquireSessionLock → 死锁,正是 D2 要避免的场景; + (5) §10 阶段 2 BLOCKER 描述收紧 + §3.3 channel-gateway 改动顺序精确化—— card-callback-service.ts 实测改动 ~5 行而非"大改"(analyzeCardCallback 内部已在解析三层 embedded JSON);channel-gateway approval 分支放 handleCardAction 之前即可,feedback path 因 actionId 不冲突不影响顺序; +
    不变更:D17 SDK 基线仍保留 >= 2026.4.7(虽然本地 openclaw 已是 2026.5.17,但 4.7 是包含 ChannelApprovalNativeRuntimeAdapter 契约 + resolveApprovalOverGateway 公开 API 的最低门槛,bump 到 5.17 会推高老用户升级成本)
  • v3.8(2026-05-19 第八轮 review):5 处实施一致性 fix + 1 处 limitation 备注—— (1) D24 主链路全局收敛:清理 §1.2 / D15 / D24 / §6.3 等多处"approvalId 不在 payload,必须 outTrackId 反查"等 v3.5 老语义残留,统一为"params.approveId 主链路 + registry pendingApprovalId fallback"; (2) 备注 v1 limitation:上游 ExecApprovalRequest / PluginApprovalRequest 的 allowedDecisions 允许 per-request 限制可选 decision(例 ask=always 时 exec 只允许 allow-once+deny),v3 模板固定 3 按钮全显示,点击不支持的 decision 会被上游拒绝、卡片刷"ℹ️ 已处理或已过期"——v1 不实现按 allowedDecisions 动态隐藏按钮,§11.1 加 limitation 条目; @@ -489,8 +632,9 @@

    3.1 上下游分工

    │ ├─ approval-card-locator.ts ★ v3.3 新增:按 sessionKey 查 │ │ │ card-run-registry,决定 route │ │ ├─ approval-card-patcher.ts ★ v3.3 替代 card-template+render: │ -│ │ 在原 agent card 上注入/清除按钮 │ -│ │ + 写入 approval 终态指示 │ +│ │ 在原 agent card 上 toggle 三个变量 │ +│ │ (show_approve_btns/approveId/ │ +│ │ hasAction) · v1 不写终态文字 │ │ ├─ approval-markdown-render.ts ★ v3.3 替代 fallback-render: │ │ │ markdown 路径主路径(不再"fallback") │ ├─ approval-target-resolver.ts v1: 仅 origin;v2: + DM │ @@ -511,7 +655,8 @@

    3.1 上下游分工

    │ ├─ src/channel.ts 新增 approvalCapability 字段 │ │ ├─ src/config-schema.ts 新增 execApprovalsSchema │ │ ├─ src/gateway/channel-gateway.ts TOPIC_CARD listener 加 approve 分支│ -│ ├─ src/card/card-template.ts v3.3 不修改(复用 AI Card v2) │ +│ ├─ src/card/card-template.ts 替换 BUILTIN_DINGTALK_CARD_TEMPLATE_ID │ +│ │ 旧→新(v2→v3,参 §10 阶段 0) │ │ └─ src/types.ts 加 ApprovalEntry / Decision │ │ │ │ 资产 │ @@ -535,29 +680,29 @@

    3.2 模块单一职责表

    approval-native-runtime.ts - 实现 5 个子 adapter,用 createLazyChannelApprovalNativeRuntimeAdapter 懒加载 - render / store / target-resolver / card-service + 实现 4 个 子 adapter(availability / presentation / transport / observe;interactions 推迟 v2),用 createLazyChannelApprovalNativeRuntimeAdapter 懒加载 + approval-card-locator / approval-card-patcher / approval-markdown-render / approval-target-resolver / card-service ~220 approval-card-locator.ts
    ★ v3.3 新增(D22 落地核心) - 导出 findActiveAgentCard({ cfg, accountId, sessionKey }):按 sessionKey 查 card-run-registry,仅在 record 存在且 record.card?.state ∈ {PROCESSING, INPUTING} 时返回 { outTrackId, sessionKey };否则返回 null(caller 走 markdown 路径)。注意:state 在 record.card.statesrc/card/card-run-registry.ts:13 + src/types.ts:689),不是 record 顶层;建议在 card-run-registry 加一个 helper isActiveCardRun(record: CardRunRecord): boolean 把这个判断封装起来,approval-card-locator 与未来其它消费方共用。 + 导出 findActiveAgentCard({ cfg, accountId, sessionKey, approvalId }):按 sessionKey 查 card-run-registry,仅在 record 存在且 record.card?.state ∈ {PROCESSING, INPUTING} 时返回 { outTrackId, sessionKey };否则返回 null(caller 走 markdown 路径)。若同一卡片已有不同 pendingApprovalId,也返回 null,让并发审批降级到 markdown;同一 approvalId 重试保持 card 路径幂等。注意:state 在 record.card.statesrc/card/card-run-registry.ts:13 + src/types.ts:689),不是 record 顶层;建议在 card-run-registry 加一个 helper isActiveCardRun(record: CardRunRecord): boolean 把这个判断封装起来,approval-card-locator 与未来其它消费方共用。
    v3.4 关键依赖:card-run-registry 必须新增 sessionKey 查询 API——当前源码只导出 resolveCardRun(outTrackId) / resolveCardRunByConversation / resolveCardRunByOwnersrc/card/card-run-registry.ts:91-145),无 by-sessionKey 查询。需在 src/card/card-run-registry.ts 新增 resolveActiveCardRunBySession(accountId: string, sessionKey: string): CardRunRecord | null——遍历 records Map,按 accountId + sessionKey 精确匹配且 state ∈ active 集合。Record 已有 sessionKey 字段(card-run-registry.ts:16),实现仅是新增一个 export。不要让 locator 依赖 conversation contains 模糊匹配或私有 Map 访问 card-run-registry(既有模块;需新增 export) ~60(+ card-run-registry 内 ~20 行新 API) - approval-card-patcher.ts
    ★ v3.5 大幅简化(不再构造按钮) - v3.5:按钮在 v2 模板内置("按钮组来源:指定",3 按钮 actionId/params 都固化),patcher 只 toggle 变量 + 维护 D24 反查映射。 -
    applyPendingPatch(outTrackId, approvalId, token):PUT updateCardVariables({ show_approve_btns: "true", hasAction: "false", approveId: "<id>" }) + markCardRunPendingApproval(outTrackId, approvalId)(D24 v3.6:主链路靠 approveId 变量带到按钮回调,registry 写入仅作 fallback); -
    applyResolvedPatch(outTrackId, decision, token, cardStillActive):PUT updateCardVariables({ show_approve_btns: "false", hasAction: cardStillActive ? "true" : "false" }) + clearCardRunPendingApproval(outTrackId); -
    applyExpiredPatch(outTrackId, token, cardStillActive):同上但语义不同。 + approval-card-patcher.ts
    ★ 三个 patcher 函数 + D24 fallback 写入 + 按钮 actionId/params 在 v3 模板内置("按钮组来源:指定"),patcher 仅 toggle cardParamMap 三个变量 + 维护 D24 fallback 映射。所有字段集与 §1.X 单一事实表对齐: +
    applyPendingPatch(outTrackId, approvalId, token):PUT updateCardVariables({ show_approve_btns:"true", hasAction:"false", approveId:"<id>" }) + markCardRunPendingApproval(outTrackId, approvalId)(D24 v3.6 主链路是 approveId 变量经按钮 params 带回 callback,registry 写入仅作 fallback); +
    applyResolvedPatch(outTrackId, decision, token, cardStillActive):PUT updateCardVariables({ show_approve_btns:"false", approveId:"", hasAction: cardStillActive ? "true" : "false" }) + clearCardRunPendingApproval(outTrackId); +
    applyExpiredPatch(outTrackId, token, cardStillActive):同 resolved 字段集,语义不同(兼用于 not-found / already-resolved 分支;gateway-error 保持 pending 并提示稍后重试)。
    -
    v3.5 已知限制(v1):用户实测 schema 里没专门的 approval 终态文字位(statusLine 被 taskInfo 占用,contentKey 与 stream 写冲突)。v1 终态仅靠"按钮消失"——用户感知按钮没了就是已处理。若需明确终态文字,v1.x 让用户在模板加 approval_status 变量后再 PUT 写入。 +
    v1 已知限制:v3 模板没专门的 approval 终态文字位(statusLine 被 taskInfo 占用,contentKey 与 stream 写冲突)。v1 终态仅靠"按钮消失"——用户感知按钮没了就是已处理。v1.x 升级路径:让维护者在模板加 approval_status 变量后再 PUT 写入。
    -
    v3.5 不再需要自定义 CardBtn 类型(v3.4 关心的事在 v3.5 自动失效——按钮 channel 端不构造) +
    模块内无按钮数据结构(所有按钮定义都在模板)。仅依赖 updateCardVariables(outTrackId, { ...variables }, token) API。 card-callback-service.updateCardVariables; card-run-registry pendingApprovalId API(v3.5 新增) - ~80(v3.4 是 ~150) + ~80 approval-markdown-render.ts
    ★ v3.3 替代 fallback-render @@ -577,7 +722,11 @@

    3.2 模块单一职责表


    (1) 按 §D21 推导 { resolveMethod, allowPluginFallback } + 调 isExecAuthorizedSender / isPluginAuthorizedSender 做权限校验;
    (2) 未授权 → return { ok: false, reason: "unauthorized" }(caller 决定怎么提示);
    (3) 调 resolveApprovalOverGateway({ cfg, approvalId, decision, senderId, clientDisplayName: "DingTalk", resolveMethod, allowPluginFallback }); -
    (4) catch 分类错误:{ ok: false, reason: "already-resolved" | "not-found" | "gateway-error", error }; +
    (4) catch 分类错误:{ ok: false, reason: "already-resolved" | "not-found" | "invalid-decision" | "gateway-error", error, allowedDecisions?: string[] }; +
        v3.11 新增 invalid-decision 分类:内部 isInvalidApprovalDecisionError(err) 识别上游两种错误形态: +
        • exec: err.gatewayCode === "INVALID_REQUEST" && err.details?.reason === "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" +
        • plugin: err.gatewayCode === "INVALID_REQUEST" && Array.isArray(err.details?.allowedDecisions)(同时把 allowedDecisions 透传给 caller 用于提示文案) +
        识别命中即返 reason="invalid-decision";否则归为 gateway-error。
    成功返回 { ok: true }。两条入口(callback + intercept)共享同一行为 approval-config, SDK gateway runtime ~140 @@ -594,8 +743,14 @@

    3.2 模块单一职责表

    approval-callback-handler.ts - TOPIC_CARD 入口(前缀 "approval" 命中):parseApprovalFromCardPrivateData{ approvalId, decision } → 调 approval-resolver.resolveApproval(权限 / fallback / gateway 都在内)→ 按 result.ok 决定终态 update(resolved)或 catch update(ℹ️ 已处理或已过期)→ ack 平台 - parseApprovalFromCardPrivateData, approval-resolver + TOPIC_CARD 入口(按 params.action 主链路 / actionId fallback 精确匹配三种 decision 命中——非 approval 按钮 return 让位 handleCardAction):parseApprovalFromCardPrivateData{ approvalId, decision } → 调 approval-resolver.resolveApproval(权限 / fallback / gateway / invalid-decision 分类都在内)→ 按 result.reason 五分支处理:
    + • ok=true → applyResolvedPatch(参 §1.X 单一事实表)
    + • unauthorized → 私聊 + 卡片保留
    + • invalid-decision(v3.11)→ 不调 patcher,私聊提示重选,卡片保 pending
    + • already-resolved / not-found → applyExpiredPatch
    + • gateway-error → 私聊提示稍后重试,卡片保 pending(避免瞬时网关失败误关有效审批)
    + → ack 平台 + parseApprovalFromCardPrivateData, approval-resolver, approval-card-patcher ~110 @@ -636,15 +791,43 @@

    3.3 与现有代码的接触面

    新增 execApprovalsSchema,加到 DingTalkConfigSchema 与 account override schema 低 + + src/types.ts
    (v3.10 新增改动面) + config 落地必须三件套同步(v3.10 用户 review 发现 v3.9 spec 漏 types/config 两处): +
    (a) DingTalkConfig / DingTalkChannelConfig 接口加 execApprovals?: ExecApprovalsConfig 字段; +
    (b) 同时定义 ExecApprovalsConfig = { enabled?: boolean | "auto"; approvers?: string[] } 类型; +
    否则 config schema 加了但 TypeScript 类型对不上,运行时拿不到(除非 type-cast) + 低(仅追加字段;既有调用方不受影响) + + + src/config.ts:279 resolveDingTalkAccount
    (v3.11 修订:指向真正会丢字段的函数) + 关键:必须在 resolveDingTalkAccount 的 rawConfig 显式字段列表里加 execApprovals: dingtalk?.execApprovals
    + v3.10 spec 错误指向 mergeAccountWithDefaults——核实 src/config.ts:60-80 后发现该函数用 { ...defaults, ...overrides } spread 模式,新字段会自动保留,不会丢
    + 真正会丢字段的是 src/config.ts:279-310 起的 default-account 路径 rawConfig 字面量构造(每个字段必须显式 field: dingtalk?.field,未列字段直接消失)。当前列表覆盖 clientId / dmPolicy / allowFrom / messageType 等约 25 个字段,未含 execApprovals——必须补一行:
    + execApprovals: dingtalk?.execApprovals,
    + account-level path(同函数后段)走 mergeAccountWithDefaults,spread 自动 cover,无需改。
    + account-level "完全替换 channel-level" 语义由 spread 自然实现(...overrides 覆盖 ...defaults)与 allowFrom 一致。 + 低(单字段拷贝;但忘加这行 = 多账号场景 default 账号完全拿不到 execApprovals) + + + tests/unit/config.test.ts
    (v3.10 新增改动面) + config 落地必须三件套同步: +
    account-level override 单测——验证 channel-level execApprovals 在有 account-level 配置时被完全替换(不合并),无 account override 时被继承;同时验证 commands.ownerAllowFrom fallback 链; +
    这类测试与 既有 allowFrom override 测试同 fixture,复制粘贴改字段名即可 + 低(test only,可作为 PR-1 部分) + src/gateway/channel-gateway.ts:330-407 TOPIC_CARD listener 在调 handleCardAction 之前插入 tryHandleApprovalCallback();命中则 return,否则继续既有路径 中(需谨慎保持 feedback / btn_stop 行为不变) - src/card/card-template.ts - v3.3 不修改——D9 v3.3 已废弃新建 approval 模板,复用现有 AI Card v2 模板字段 - — + src/card/card-template.ts:6
    (v3.11 修订:必须替换默认 templateId) + 不新增 approval 专用模板(D9 v3.3 决策保持),但 必须替换内置 AI Card 模板 ID 为 v3。当前源码仍是旧 v2 ID:
    + BUILTIN_DINGTALK_CARD_TEMPLATE_ID = "675cde2f-...8b77.schema" (v2)
    + 必须改为:"58f73932-fc3b-46ae-8e90-93313e405061.schema" (v3,含 approve_btns / show_approve_btns / approveId 三变量)。
    + 详见 §10 阶段 0 BLOCKER。注:env 覆盖 DINGTALK_CARD_TEMPLATE_ID 仍可保留(开发期测试用),release default 必须是 v3 + 低(单常量替换,但前置阶段 0 模板已发布) src/card-service.ts
    (v3.6 新增改动面) @@ -666,7 +849,7 @@

    3.3 与现有代码的接触面

    src/types.ts - 新增 ApprovalEntry / ApprovalDecision / ApprovalPhase 类型。
    v3.5 修订:不再需要 CardBtn 类型——v3.4 关心的"channel 端构造按钮"在 v3.5 失效(按钮在 v2 模板内置)。channel 端只 PUT cardParamMap 的 show_approve_btnshasAction 两个 Boolean 字段,无任何按钮数据结构 + 新增 ApprovalEntry / ApprovalDecision / ApprovalPhase 类型;v3.10 新增 ExecApprovalsConfig(接 §4.2 schema)。
    无按钮数据结构——按钮 actionId/params 全在 v3 模板内置(参 §1.X 单一事实表),channel 端只 PUT cardParamMap 三个字符串变量(show_approve_btns / hasAction / approveId) 极低 @@ -675,11 +858,16 @@

    3.3 与现有代码的接触面

    低(仅追加字段,既有调用方不受影响;现有 extractCardActionId 抽 actionIds[0] 的逻辑不变) - src/inbound-handler.ts
    (D2:v2 新增改动面) - 在 handleDingTalkMessage 早期插入 /approve 命令 intercept 分支。 -
    v3.8 精确定位:插入点必须在 access control + content extract + senderId 确定之后(需要这些上下文做 approver 权限校验),但必须 早于本地命令 dispatch(src/inbound-handler.ts:874)+ 早于 sub-agent targeted command 递归——否则 @agent /approve abc once 会被 sub-agent 路由先吃掉。 -
    更必须 早于 acquireSessionLock(src/inbound-handler.ts:2053——后者一旦持有锁,resolveApprovalOverGateway 内部的 plugin approval waitDecision 会等同一把锁就死锁。详见 §6.8 - 中(需谨慎确保 intercept 之前的 dedup/self-filter/content-extract 行为不变) + src/inbound-handler.ts
    (D2;v3.10 精确到 sub-agent routing 前) + 在 handleDingTalkMessage 早期插入 /approve 命令 intercept 分支。/approve 是全局控制命令,必须在 routing 决策之前拦截。 +
    v3.10 精确插入点(行号参当前 main): +
    ✓ 晚于 L575(content extract 完成)+ L671/729(DM/Group 授权通过) +
    早于 L817 sub-agent routing 分支——否则 @agent /approve abc once 会被 messageTarget=subagent-* 路由吞掉 +
    ✓ 早于 L874 handleInboundCommandDispatch(本地命令 dispatcher) +
    ✓ 早于 L2053 acquireSessionLock——否则 resolveApprovalOverGateway 内部 plugin waitDecision 会等同一把锁 → 死锁 +
    建议位置:L770 sessionPeer 解析之后、L780 routing 解析之前。 +
    insert block 内必须先剥前置 @mention(extractedContent.text.replace(/^(?:@\S+\s+)*/u, "").trim()),再 regex 判 /^\/?approve\b/i。详见 §6.8 + 中(需谨慎确保 intercept 之前的 dedup/self-filter/content-extract 行为不变;插入点错位会让 @agent /approve 失效) package.json · peerDependencies.openclaw
    (D17:v2 新增) @@ -782,7 +970,7 @@

    5.1 availability

    shouldHandle({ cfg, accountId, request }) 四连判(v1 origin-only 严格判定):
    (1) isConfigured 为 true; -
    (2) request.turnSourceChannel === "dingtalk"(非 dingtalk 触发的 approval 跳过;CLI 触发由用户在钉钉里手敲 /approve 兜底); +
    (2) request.turnSourceChannel === "dingtalk"(非 dingtalk 触发的 approval 跳过;CLI 触发由用户**从 CLI/WebUI/日志获取 approval id 后**在钉钉里敲 /approve 命令——非天然兜底,用户必须先有 id 才能用此命令);
    (3) request.turnSourceTo 非空且可被 normalizeApprovalTargetTo 解析(保证有 origin target);
    (4) listExecApprovers().length > 0(有 approver 名单才有 channel-side 权限校验意义)。
    v2 改为 requireMatchingTurnSourceChannel: false 后才放开 (2),让 CLI 触发也能走 DM 兜底 @@ -796,20 +984,19 @@

    5.2 presentation

    buildPendingPayload({ request, nowMs, view }) - v3.5 简化(D15 + 模板内置按钮):channel 不再构造按钮数组。 -
    返回 { approvalId: request.id, markdownText: string }。 -
    approvalId:transport.deliverPending(card) 用来 markCardRunPendingApproval(outTrackId, approvalId)(D24); -
    markdownText(markdown 路径用):由 approval-markdown-render.buildExecApprovalMarkdown(request, nowMs)buildPluginApprovalMarkdown 输出,含 approval id、command/tool preview、3 个 /approve <id> <decision> 复制即用块。 -
    -
    按钮本身的 actionId / params 都在 v2 模板的 approve_btns 按钮组里固化(D15),channel 端永远不传按钮定义 + 返回 { approvalId: request.id, markdownText: string }(payload 抽象层,按钮定义全在 v3 模板内置)。 +
    approvalId:transport.deliverPending(card) 用来作 approveId 变量值(D24 主链路)+ markCardRunPendingApproval fallback 写入; +
    markdownText(markdown 路径用):由 approval-markdown-render.buildExecApprovalMarkdown(request, nowMs)buildPluginApprovalMarkdown 输出,含 approval id、command/tool preview、3 个 /approve <id> <decision> 复制即用块。 buildResolvedResult({ request, resolved, view, entry }) - 返回 { kind: "update", payload: buildResolvedCardParamMap(request, resolved) }。payload 把 statusFooter 改成"✅ 已批准 by @<resolverDisplayName>"或对应的拒绝文案;buttonGroupVisible=false + v3.9 修订(清理 v3.3 残留):返回抽象的 { kind: "update", payload: { phase: "resolved", decision: resolved.decision, resolverDisplayName: view.resolvedBy } }。具体字段集(show_approve_btns / hasAction / approveId)由 transport.updateEntry 按 entry.mode 分支调 patcher 时确定(v3.5+ patcher 已统一封装;v1 不写终态文字位,§7.1 已坦率列限制)。 +
    不再返回 buildResolvedCardParamMap(...) / statusFooter / buttonGroupVisible 等 v3.3 老字段 buildExpiredResult({ request, view, entry }) - 返回 { kind: "update", payload: buildExpiredCardParamMap(request) }。statusFooter = "⏰ 已过期(未在 <X> 分钟内响应)";buttonGroupVisible=false + v3.9 修订(清理 v3.3 残留):返回 { kind: "update", payload: { phase: "expired" } }。同样由 transport.updateEntry 调 patcher.applyExpiredPatch 时落地具体字段(show_approve_btns:"false" + approveId:"" + hasAction 视 card 状态恢复 stop)。 +
    不再返回 buildExpiredCardParamMap(...) / statusFooter @@ -831,7 +1018,11 @@

    5.3 transport

    deliverPending({ cfg, accountId, preparedTarget, request, pendingPayload }) v3.4 修订:按 preparedTarget.route 分两条路径 + card 失败时降级 markdown
    route="card": -
    (1) 调 approval-card-patcher.applyPendingPatch(activeCardOutTrackId, request.id, token)——内部 PUT updateCardVariables({ show_approve_btns: "true", hasAction: "false" }) 显示 approval 按钮组(D15 + D22)+ 隐藏 btn_stop(D23)+ 把 approvalId 写到 card-run-registry record.pendingApprovalId(D24 反查映射)。按钮 actionId/params 都在模板内置,channel 不传按钮定义。 +
    (1) 调 approval-card-patcher.applyPendingPatch(activeCardOutTrackId, request.id, token)——内部 PUT updateCardVariables(三个变量,参见 §1.X 单一事实表): +
        show_approve_btns:"true"(显示 approval 按钮组 · D15+D22) +
        hasAction:"false"(隐藏 btn_stop · D23) +
        approveId:<approval.id>(D24 v3.6 主链路:approvalId 经此变量绑定到按钮 params,callback 自带) +
    同时 markCardRunPendingApproval(outTrackId, approval.id) 写到 registry 作为 fallback(应对老卡片 / 平台异常等 callback 不带 params.approveId 的情况)。按钮 actionId/params 都在模板内置,channel 不传按钮定义。
    (2) 成功 → 返回 entry = { approvalId, accountId, mode: "card", outTrackId: activeCardOutTrackId }
    (3) 明确失败(HTTP 4xx/5xx 非超时、模板字段不支持、PUT 被钉钉拒等可确定失败的错误)→ WARN log + 降级 markdown(调下方 route="markdown" 同一段逻辑),成功后返回 entry = { approvalId, accountId, mode: "markdown" }。这样用户至少能看到 approval id 与 /approve 命令模板。
    (4) 模糊失败(请求超时但可能已成功)→ WARN log + return null(不重发避免双消息)。 @@ -845,7 +1036,7 @@

    5.3 transport

    updateEntry({ cfg, accountId, entry, payload, phase }) v3.3 修订:按 entry.mode 分支。 -
    entry.mode === "card":phase=resolved → approval-card-patcher.applyResolvedPatch(entry.outTrackId, payload.decision, payload.resolverDisplayName, token);phase=expired → applyExpiredPatch(entry.outTrackId, token)。两个 patcher 内部都做"清按钮 + 写终态指示 + 视情况恢复 btn_stop"(D14 D23)。 +
    entry.mode === "card":phase=resolved → approval-card-patcher.applyResolvedPatch(entry.outTrackId, payload.decision, token, cardStillActive);phase=expired → applyExpiredPatch(entry.outTrackId, token, cardStillActive)。两个 patcher 内部 PUT 三变量(参 §1.X 单一事实表:show_approve_btns:"false" + approveId:"" + hasAction 按 cardStillActive 恢复)。v1 不写终态文字(D14 / §7.1 已坦率列限制)。
    entry.mode === "markdown":no-op——markdown 消息不能 edit;v1 不发新通知消息避免刷屏(D14 markdown 路径说明)。
    entry 由 core 从 activeEntries 带回,channel 不查任何本地 store(D18) @@ -1020,9 +1211,11 @@

    场景 E:CLI 触发的 exec approval(turnSourceChannel 非 dingtalk)前提:用户从 CLI 跑 codex,approval 触发时 turnSourceChannel ≠ "dingtalk"。 → availability.shouldHandle 直接返 false(§5.1 v1 origin-only 四连判第 2 条) -→ DingTalk 端不投递;用户在钉钉里需要自己手敲 /approve 命令兜底 +→ DingTalk 端不投递;钉钉群里**无任何 approval 痕迹**。 + 用户必须先从 CLI 终端 / WebUI / 日志获取 approval id,再在钉钉里敲 /approve 完成。 + v1 不存在"用户在钉钉里被动看见 approval id"的路径——这不是天然兜底,是显式 out-of-band 操作。 -v2 future:approver-DM 投递启用后,CLI 场景能自动 DM 给 approver。 +v2 future:approver-DM 投递启用后,CLI 场景能自动 DM 给 approver(钉钉端就能看见 id)。

    6.3 点击 approve → 上游 resolve(核心交互链路)

    用户在卡片上点"允许一次"
    @@ -1058,15 +1251,27 @@ 

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ if (!parsed) return { handled: false } │ parsed = { decision: "allow-once" } ← 注意:还没有 approvalId │ - │ 2. 反查 approvalId(D24) - │ const cardRun = resolveCardRun(analysis.outTrackId); - │ const approvalId = cardRun?.pendingApprovalId; + │ 2. 取 approvalId(D24 v3.6:主链路 params.approveId + fallback registry) + │ // 主链路:v3 模板已把 approveId 绑定到三按钮 params,callback 自带 + │ let approvalId = + │ (typeof analysis.cardPrivateData?.params?.approveId === "string" && + │ analysis.cardPrivateData.params.approveId) || null; + │ // Fallback:老卡片 / 平台异常时反查 registry + │ if (!approvalId) { + │ const cardRun = resolveCardRun(analysis.outTrackId); + │ approvalId = cardRun?.pendingApprovalId ?? null; + │ } │ if (!approvalId) { - │ // card 不存在(重启清空 / TTL sweep)/ 未挂 pending approval - │ // → 卡片刷"ℹ️ 已处理或已过期"提示用户 - │ await updateCardVariables(analysis.outTrackId, { - │ show_approve_btns: "false", - │ }, token).catch(() => {}); + │ if (!isApprovalDecision(analysis.cardPrivateData?.params?.action)) { + │ // 只有 actionIds[0]/analysis.actionId 命中但没有 approvalId 时, + │ // 可能是其它卡片用了 deny 这类泛用 actionId,不接管。 + │ return { handled: false }; + │ } + │ // 主链路 + fallback 都没拿到,但 params.action 明确来自 approval 模板 + │ // → 视为"已处理或已过期",best-effort 走 patcher 保证字段集完整 + │ await patchCardBestEffort(() => applyExpiredPatch( + │ analysis.outTrackId, token, cardStillActive, + │ )); │ return { handled: true, reason: "no-pending-approval" }; │ } │ parsed.approvalId = approvalId; ← 现在有了 @@ -1094,23 +1299,47 @@

    6.3 点击 approve → 上游 resolve(核心交互链路)

    │ { forceMarkdown: true, accountId, log }) // 强制 markdown │ return { handled: true, reason: "unauthorized" } │ + │ case "invalid-decision": + │ // v3.11 新策略:上游拒绝该 decision(allowedDecisions 限制) + │ // ⚠️ 不调 applyExpiredPatch —— 卡片保持 pending,按钮全部保留可再点 + │ const allowedHint = result.allowedDecisions?.length + │ ? `请选择:${result.allowedDecisions.join(" / ")}` + │ : "请选择允许一次或拒绝"; + │ await sendProactiveTextOrMarkdown( + │ dingtalkConfig, + │ `user:${analysis.userId}`, + │ `ℹ️ 该审批不支持 ${parsed.decision}。${allowedHint}(${parsed.approvalId})`, + │ { forceMarkdown: true, accountId, log }).catch(() => {}) + │ return { handled: true, reason: "invalid-decision" } + │ + │ case "gateway-error": + │ // 瞬时网关失败不代表审批失效,保留 pending 给用户重试 + │ await sendProactiveTextOrMarkdown( + │ dingtalkConfig, + │ `user:${analysis.userId}`, + │ `ℹ️ 审批暂时处理失败,请稍后重试(${parsed.approvalId})`, + │ { forceMarkdown: true, accountId, log }).catch(() => {}) + │ return { handled: true, reason: "gateway-error" } + │ │ case "already-resolved": │ case "not-found": - │ case "gateway-error": - │ // 兜底卡片刷成中性终态(不写死"已过期",避免误导) - │ await updateCardVariables(payload.outTrackId, { - │ status: "ℹ️ 已处理或已过期", - │ hasAction: "false", - │ btns: "[]", - │ }, token).catch(() => {}) + │ // 兜底卡片刷成中性终态(参 §1.X 单一事实表) + │ await approval-card-patcher.applyExpiredPatch( + │ analysis.outTrackId, token, cardStillActive, + │ ).catch(() => {}) │ return { handled: true, reason: result.reason } │ } │ } │ - │ 5. result.ok === true:立即 update 本机这张为 resolved 终态 - │ await updateCardVariables(payload.outTrackId, - │ buildResolvedCardParamMap(parsed.decision, analysis.userId), - │ token) + │ 5. result.ok === true:立即 update 本机这张为 resolved 终态(v3.9 改用 patcher) + │ // 不再用已废弃的 buildResolvedCardParamMap —— v3.5+ patcher 已统一封装 + │ // toggle 字段集(show_approve_btns / hasAction / approveId / D14 终态文字位 v1 不写) + │ await patchCardBestEffort(() => approval-card-patcher.applyResolvedPatch( + │ analysis.outTrackId, + │ parsed.decision, + │ token, + │ cardStillActive, // 视 record.card?.state 是否在 active 集合(D23) + │ )) │ // 即使上游 updateEntry 异步事件还没回来,用户也能立刻看到终态。 │ // 上游事件回来后会再做一次同样内容的 update,幂等覆盖 OK。 │ @@ -1128,11 +1357,11 @@

    mode === "card":在原 agent reply card 上 toggle 变量

    ┌──────────────────────────────────────┐ │ channel callback handler(§6.3 step 5)│ │ approval-card-patcher.applyResolvedPatch(outTrackId, - │ decision, cardStillActive, token) - │ → PUT updateCardVariables(outTrackId, { - │ show_approve_btns: "false", // 隐藏 approval 3 按钮 - │ hasAction: cardStillActive ? "true" : "false", // 恢复 btn_stop - │ }, token) + │ decision, token, cardStillActive) + │ → 内部 PUT 三个变量(参 §1.X 单一事实表): + │ show_approve_btns: "false" + │ approveId: "" + │ hasAction: cardStillActive ? "true" : "false" │ → clearCardRunPendingApproval(outTrackId) // D24 清反查映射 └──────────────────┬───────────────────┘ ← 第一次 update(同步) │ @@ -1165,7 +1394,8 @@

    mode === "card":在原 agent reply card 上 toggle 变量

    变量后再写 agent reply card 自己的状态机不变(PROCESSING/INPUTING/FINISHED 由 - agent reply 流程控制,approval 只 patch 2 个 Boolean cardParamMap 字段)
    + agent reply 流程控制,approval 只 patch cardParamMap 三个变量: + show_approve_btns / approveId / hasAction,参 §1.X 单一事实表)

    mode === "markdown":v1 不发新通知消息

    transport.updateEntry:entry.mode === "markdown" 分支
    @@ -1196,8 +1426,8 @@ 

    用户重复点击(按钮看起来还在但已 resolved)

  • callback-handler 解析按钮 → 主链路读 params.approveId(D24 v3.6);缺失 fallback 到 resolveCardRun(outTrackId).pendingApprovalId
  • 反查可能命中(resolved 后还没来得及 clear)或未命中(已 clear):
      -
    • 命中:调 resolver → 上游返回 already-resolved → catch 分支 PUT { show_approve_btns: "false" } 把按钮再次隐藏
    • -
    • 未命中:直接 PUT { show_approve_btns: "false" },return "no-pending-approval"
    • +
    • 命中:调 resolver → 上游返回 already-resolved → catch 分支调 applyExpiredPatch(字段集见 §1.X 单一事实表:show_approve_btns=false + approveId="" + hasAction 恢复)把按钮再次隐藏
    • +
    • 未命中:直接调 applyExpiredPatch,return "no-pending-approval"
  • 对用户:第二次点击 = 按钮立即消失,与第一次结果一致,无打扰提示
  • @@ -1213,8 +1443,8 @@

    非 approver 点击

    Channel 重启后用户点旧卡片(v3.5)

      -
    1. channel 端 card-run-registry 内存清空 → resolveCardRun(outTrackId).pendingApprovalId 返 null
    2. -
    3. callback-handler 进 "no-pending-approval" 分支 → PUT { show_approve_btns: "false" },return
    4. +
    5. channel 端 card-run-registry 内存清空 → 主链路 params.approveId 仍能命中(D24 v3.6 改进),但若卡片是 v3 之前版本则 fallback resolveCardRun(outTrackId).pendingApprovalId 返 null
    6. +
    7. callback-handler 进 "no-pending-approval" 分支 → applyExpiredPatch(outTrackId, token, cardStillActive)(字段集见 §1.X 单一事实表:show_approve_btns=false + approveId="" + hasAction 按 cardStillActive 恢复),return
    8. 不调上游 resolve(没 approvalId 可传);上游 approval 由它自己的 TTL 兜底
    9. 用户体感:按钮点了一下就消失——降级到"按钮自动消失",对长时间下线可接受
    @@ -1226,11 +1456,11 @@

    上游过期事件触达

    │ }} └─ transport.updateEntry({ entry, payload, phase: "expired" }) → entry.mode === "card" 分支: - approval-card-patcher.applyExpiredPatch(entry.outTrackId, cardStillActive, token) - → PUT updateCardVariables({ - show_approve_btns: "false", - hasAction: cardStillActive ? "true" : "false", - }) + approval-card-patcher.applyExpiredPatch(entry.outTrackId, token, cardStillActive) + → 内部 PUT 三变量(参 §1.X 单一事实表): + show_approve_btns: "false" + approveId: "" // D24 主链路载体清空 + hasAction: cardStillActive ? "true" : "false" → clearCardRunPendingApproval(entry.outTrackId) → entry.mode === "markdown" 分支:no-op → core 从 activeEntries 移除该 entry @@ -1240,7 +1470,7 @@

    上游过期事件触达

    Channel stopClient(账号停用 / gateway 重启)—— v3 推迟到 v2

    v1 不实现 stop-time finalize(D13 v3 推迟)。原因:D18 删除本地 store 后 channel 端无法枚举 pending entries。 - v1 行为:停机时遗留 approval 卡片保留按钮态;用户点击 → §6.6"Channel 重启后用户点旧卡片"路径 → 卡片刷成"ℹ️ 已处理或已过期"。 + v1 行为:停机时遗留 approval 卡片保留按钮态;用户点击 → §6.6"Channel 重启后用户点旧卡片"路径 → 调 applyExpiredPatch(隐藏按钮 + 清 approveId,参 §1.X;v1 不写终态文字)。 v2 future:若 SDK 暴露 activeEntries 查询 API,或 channel 引入轻量 outTrackId Set(仅供 stop-time 清理用,非完整 entry store),再实现 finalize。
    @@ -1294,18 +1524,28 @@

    6.8 /approve 命令必须早期 intercept(D2 落地)

    解决方案:在 handleDingTalkMessage 入口最早处直接拦截,不走正常 dispatch

    // src/inbound-handler.ts 内 handleDingTalkMessage 函数:
    -//   既有顺序:dedup → self-filter → content extract → authorization
    -//             → session routing → command dispatch → reply 派发
    +//   既有顺序(行号参 v3.10 实测当前 main):
    +//     L575  content extract → extractedContent ready
    +//     L671/729  authorization passed (DM / Group)
    +//     L770  sessionPeer 解析
    +//     L780-811  routing 解析 (messageTarget / route)
    +//     L817  sub-agent routing 分支(messageTarget.kind !== "default")
    +//     L874  handleInboundCommandDispatch(本地命令 dispatch)
    +//     L2053 acquireSessionLock → reply 派发
    +//
    +//   v3.10 精确:/approve 是全局控制命令,必须在 routing 决策之前拦截。
     //
    -//   v2 修订:在 "command dispatch" 之后、"reply 派发"之前,加入 /approve early intercept。
    -//            插入点(v3.8 精确):
    -//            ✓ 必须在:dedup / self-filter / access control / content extract /
    -//              senderId 确定 之后(需要 senderId 做 approver 权限校验)
    -//            ✓ 必须早于:本地 command dispatch (src/inbound-handler.ts:874)
    -//              + sub-agent targeted command 递归(@agent /approve 否则被吞)
    -//            ✓ 必须早于:acquireSessionLock (src/inbound-handler.ts:2053)
    -//              否则 resolveApprovalOverGateway 内部 plugin waitDecision
    -//              会等同一把锁 → 死锁
    +//   插入点必须满足:
    +//   ✓ 晚于 L575(需要 extractedContent.text)+ L671/729(需要授权通过)
    +//   ✓ 早于 L817 sub-agent routing 分支
    +//     —— 否则 @agent /approve abc once 会被 messageTarget=subagent-* 路由先吃掉,
    +//        被当成 agent 输入而不是 approval 命令
    +//   ✓ 早于 L874 handleInboundCommandDispatch(本地命令 dispatcher)
    +//   ✓ 早于 L2053 acquireSessionLock —— 否则 resolveApprovalOverGateway 内部
    +//     plugin waitDecision 会等同一把锁 → 死锁
    +//
    +//   建议插入位置:L770 之前或之后均可(不依赖 sessionPeer);最简单是在
    +//   L770 sessionPeer 解析之后、L780 routing 解析之前插入此 block
     
     // ---- Early /approve bypass:collapsed into approval-resolver ----
     // 关键:parse 与 resolve 都走专用模块(D20),不在 inbound-handler 内堆 200 行
    @@ -1313,7 +1553,12 @@ 

    解决方案:在 handleDingTalkMessage 入口最早处直接 ? extractedContent.text.replace(/^(?:@\S+\s+)*/u, "").trim() // 群里剥前导 @mention : extractedContent.text.trim(); -if (/^\/approve\b/i.test(textForApproveCheck)) { +// v3.9 修订:regex 与上游 commands-approve.ts:16 COMMAND_REGEX 完全对齐 +// 上游 = /^\/?approve(?:\s|$)/i —— 前导斜杠可选(接受裸 "approve abc once") +// 若 channel 端只接受带 / 形式,会出现"用户敲裸 approve → 没 intercept → +// 走 normal pipeline → 上游 dispatcher 接受 → acquireSessionLock → 死锁" +// 正是 D2 要避免的场景。所以必须 100% 对齐上游 regex +if (/^\/?approve\b/i.test(textForApproveCheck)) { const { tryInterceptApproveCommand } = await import("./approval/approval-command-intercept"); const intercepted = await tryInterceptApproveCommand({ cfg, @@ -1337,6 +1582,14 @@

    approval-command-intercept.ts 内部职责(伪代码)

    }): Promise<boolean> { const parsed = parseApproveCommand(text); // approval-command-parser if (!parsed) { + // v3.10:malformed 命令也给私聊提示,引导用户用正确格式 + // 同样必须 forceMarkdown:true(messageType=card 配置下,否则会被发成卡片) + await sendProactiveTextOrMarkdown( + getConfig(cfg, accountId), + `user:${senderId}`, + "⚠️ /approve 命令格式错误。用法:`/approve <approvalId> <allow-once|allow-always|deny>`", + { forceMarkdown: true, accountId, log }, // ← v3.10 必传 forceMarkdown + ).catch(() => {}); log?.warn?.("[DingTalk] /approve malformed"); return true; // 是 /approve 但格式错;仍 return true // 让 inbound 不再走 reply 派发 @@ -1358,11 +1611,39 @@

    approval-command-intercept.ts 内部职责(伪代码)

    getConfig(cfg, accountId), `user:${senderId}`, // user: 前缀走 oto `⛔ 你不在 approver 名单,无权批准此请求(${parsed.approvalId})`, - { accountId, log }, + { forceMarkdown: true, accountId, log }, // ← v3.10 必传 forceMarkdown + // 否则 messageType=card 时被发成卡片 + // (src/send-service.ts:371-393) ).catch(() => {}); // 提示失败不影响主流程 + } else if (!result.ok && result.reason === "invalid-decision") { + // v3.11 新策略:上游 allowedDecisions 限制(如 ask=always 时不允许 allow-always) + // 命令路径不需 patcher(命令本身无原卡片),私聊提示用户重选 + const allowedHint = result.allowedDecisions?.length + ? `请选择:${result.allowedDecisions.join(" / ")}` + : "请选择允许一次或拒绝"; + await sendProactiveTextOrMarkdown( + getConfig(cfg, accountId), + `user:${senderId}`, + `ℹ️ 该审批不支持 ${parsed.decision}。${allowedHint}(${parsed.approvalId})`, + { forceMarkdown: true, accountId, log }, + ).catch(() => {}); + } else if (!result.ok && (result.reason === "not-found" || result.reason === "already-resolved")) { + // v3.10:not-found / already-resolved 也给私聊轻提示,避免用户敲完命令完全无反馈 + // 命令路径无原卡片可 update,所以走私聊(不是群聊,避免打扰) + await sendProactiveTextOrMarkdown( + getConfig(cfg, accountId), + `user:${senderId}`, + `ℹ️ 审批 ${parsed.approvalId} 已处理或已过期,无需再次操作。`, + { forceMarkdown: true, accountId, log }, // ← v3.10 必传 forceMarkdown + ).catch(() => {}); + } else if (!result.ok && result.reason === "gateway-error") { + await sendProactiveTextOrMarkdown( + getConfig(cfg, accountId), + `user:${senderId}`, + `ℹ️ 审批 ${parsed.approvalId} 暂时处理失败,请稍后重试。`, + { forceMarkdown: true, accountId, log }, + ).catch(() => {}); } - // 其它失败(already-resolved / not-found / gateway-error)由用户在卡片侧/ - // 下次操作时自然感知;命令路径无原卡片可 update,所以仅 log 不发新消息 if (!result.ok) { log?.info?.(`[DingTalk] /approve resolver returned ${result.reason}`); } @@ -1404,7 +1685,10 @@

    为什么 D2 不能"纯复用上游 /approve dispatcher"

    7. 审批卡片设计

    7.1 v2 模板字段映射(v3.5 对齐用户实配 schema)

    -

    v3.5 用户已在 AI Card v2 模板上配好两个新增变量(schema id 末尾 876de.schema)+ 一个 ButtonGroup。完整低代码 schema(v3.0.0)见 docs/assets/card-template-v3.json(267KB,从开发者平台导出)——可重新导入开放平台卡片搭建器对比 / 定制 / 版本迁移用。

    +

    v3 模板(schema id 末尾 05061.schema)相比 v2 新增的内容(v3.12 精确化描述): +
    2 个 cardParamMap 变量(channel 需要 PUT)show_approve_btns(Boolean 开关)+ approveId(字符串,D24 主链路载体) +
    1 个内置 ButtonGroupapprove_btns——按钮组定义本身(含 3 按钮 actionId/params 绑定),不属于 PUT 字段集,channel 永不传它 +
    完整低代码 schema(v3.0.0)见 docs/assets/card-template-v3.json(268KB,从开发者平台导出)——可重新导入开放平台卡片搭建器对比 / 定制 / 版本迁移用。

    @@ -1454,29 +1738,40 @@

    7.2 三按钮配置(v3.5:在 v2 模板内置)

    // ───────────────────────────────────────────────────────────── // 按钮 1 · 允许一次(color: green) // ActionId: "allow-once" -// 回传参数: [{ name: "action", type: 静态值, value: "allow-once" }] +// 回传参数: [ +// { name: "action", type: 静态值, value: "allow-once" }, +// { name: "approveId", type: 变量, value: "approveId" } // ← D24 绑定到变量 +// ] // // 按钮 2 · 总是允许(color: blue) // ActionId: "allow-always" -// 回传参数: [{ name: "action", type: 静态值, value: "allow-always" }] +// 回传参数: [ +// { name: "action", type: 静态值, value: "allow-always" }, +// { name: "approveId", type: 变量, value: "approveId" } +// ] // // 按钮 3 · 拒绝(color: red) // ActionId: "deny" -// 回传参数: [{ name: "action", type: 静态值, value: "deny" }] +// 回传参数: [ +// { name: "action", type: 静态值, value: "deny" }, +// { name: "approveId", type: 变量, value: "approveId" } +// ] // // 显示控制 → 条件计算:show_approve_btns 的值为 true → 整组可见 // ───────────────────────────────────────────────────────────── -// channel 端 patch 只 PUT 这两个变量(approval-card-patcher.ts,v3.5): +// channel 端 patch PUT 三个变量(v3.10 对齐 §1.X 单一事实表 + D24 主链路): updateCardVariables(outTrackId, { show_approve_btns: "true", // ← 显示 approval 3 按钮(D15) hasAction: "false", // ← 隐藏 btn_stop(D23) + approveId: "<approval.id>", // ← D24 主链路:approvalId 通过此变量 + // 绑定到按钮 params,callback 自带 }, token) -// approvalId 通过 markCardRunPendingApproval(outTrackId, approvalId) 写到 -// card-run-registry record,callback 时反查(D24) +// fallback:markCardRunPendingApproval(outTrackId, approvalId) 写到 card-run-registry +// 仅当 callback 不带 params.approveId 时反查(D24 兜底)

    v3.5 实证:actionId 唯一命名时 DingTalk callback 原样回传,无 index 后缀。 - 回调解析以 params.action 取 decision,详见 §1.2 与 §6.3。 + 回调解析主链路读 params.approveIdparams.action 取 decision,详见 §1.X 单一事实表 + §6.3。

    7.3 双状态机并存(v3.3)

    @@ -1500,7 +1795,7 @@

    7.3 双状态机并存(v3.3)

    两状态机的协同(D22 关键不变量): • approval 只对 cardParamMap 字段做 patch,不触碰 card 的 state -• card 自然走到 FINISHED 后,approval 终态指示仍保留在卡片正文 +• card 自然走到 FINISHED 后,approval 按钮已隐藏(show_approve_btns=false 永久态) • card 进入 STOPPED(用户点 btn_stop 后)会 final 化,approval 若还 pending 会孤儿——v1 已知降级,v2 future 可考虑 deny propagate @@ -1714,8 +2009,8 @@

    8. 错误处理矩阵

    - - + + @@ -1729,6 +2024,21 @@

    8. 错误处理矩阵

    + + + + + +
    cardParamMap 变量类型approval 用途pending / resolved / expired 时的值
    上游 resolve 返回 not-found approvalId 在上游已过期/清除(如 channel 长时间下线)callback-handler 更新卡片为"ℹ️ 已处理或已过期"点击后卡片直接刷成过期态callback-handler 调 applyExpiredPatch:隐藏 approval 按钮组 + 清 approveId 变量(字段集见 §1.X 单一事实表;v1 不写"已处理或已过期"等终态文字,§7.1 已说明 schema 无字段位)按钮自动消失,用户感知"已是终态"
    turnSourceChannel 不是 dingtalk(CLI 触发) 第一个点击的成功 → 上游标记 resolved → 第二个点击触发 already-resolved → catch 分支 update 为已过期/已关闭 第二个点击者看到卡片立即刷成终态,明确反馈
    用户点了 request 不允许的 decision(v3.11 策略重新设计)上游 ExecApprovalRequest.allowedDecisions 限制(如 ask="always" 时仅允许 allow-once+deny,不允许 allow-always)。v1 模板固定 3 按钮全显示,用户可能点不被允许的按钮(参 §11.1 v1 limitation)。
    + 上游错误形态(核实 openclaw/src/gateway/server-methods/exec-approval.ts:45-46,468):
    + • exec: err.gatewayCode === "INVALID_REQUEST" && err.details.reason === "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE"
    + • plugin: err.gatewayCode === "INVALID_REQUEST" && Array.isArray(err.details.allowedDecisions)
    v3.11 新策略:approval-resolver 增加错误分类 "invalid-decision"——实现 isInvalidApprovalDecisionError(err) helper 识别上述两种 details 形态。
    + callback handler / command intercept 拿到 { ok: false, reason: "invalid-decision", allowedDecisions?: string[] } 后:
    + (1) applyExpiredPatch——卡片保持 pending(按钮全部保留)
    + (2) 私聊 clicker 提示"该审批不支持 <decision>,请选择 <allowedDecisions 列表 or '允许一次/拒绝'>"(含 approval id 方便用户用 /approve 兜底)
    + (3) 不重试 RPC
    + 明确不要把 invalid-decision 当作 gateway-error 走 catch 终态分支
    用户点按钮后卡片不变(按钮保留可再点);同时收到私聊提示"该审批不支持这个 decision,请改用 X 或 Y";可再点正确按钮或敲 /approve 命令完成审批。
    + 用户感知一致:approval 仍 pending,可继续操作;不会出现"按钮消失但 agent 未继续"的卡死
    @@ -1743,20 +2053,33 @@

    9.1 测试文件布局

    tests/unit/approval-config.test.tsschema 解析、normalize、enabled=auto、fallback chain~12 - tests/unit/approval-card-render.test.tspending/resolved/expired/canceled × exec/plugin = 8 个矩阵~16 + tests/unit/approval-target-resolver.test.tsorigin 解析(含 turnSourceChannel=null)、DM 列表构造~10 - tests/unit/approval-resolver.test.ts
    ★ v3.2D20/D21 核心:kind 推导 4 情况(plugin: 前缀 / 无前缀 + 两边授权 / 仅 plugin 授权 / 仅 exec 授权)、未授权返 unauthorized、resolveApprovalOverGateway 调用参数(含 resolveMethod 与 allowPluginFallback)、错误分类(already-resolved / not-found / gateway-error);mock SDK gateway~18 + tests/unit/approval-resolver.test.ts
    ★ v3.2 / v3.12 补 invalid-decisionD20/D21 核心:kind 推导 4 情况(plugin: 前缀 / 无前缀 + 两边授权 / 仅 plugin 授权 / 仅 exec 授权)、未授权返 unauthorized、resolveApprovalOverGateway 调用参数(含 resolveMethod 与 allowPluginFallback)、错误分类 5 类(unauthorized / already-resolved / not-found / invalid-decision / gateway-error);mock SDK gateway。
    + v3.12 新增 invalid-decision 分类测试:(a) mock gateway 抛 { gatewayCode: "INVALID_REQUEST", details: { reason: "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" } } → 验证 resolver 返 { ok: false, reason: "invalid-decision" }(exec 路径);(b) mock gateway 抛 { gatewayCode: "INVALID_REQUEST", details: { allowedDecisions: ["allow-once", "deny"] } } → 验证 resolver 返 { ok: false, reason: "invalid-decision", allowedDecisions: ["allow-once", "deny"] }(plugin 路径,透传 allowedDecisions);(c) gateway 抛非 invalid-decision 的 INVALID_REQUEST → 仍归 gateway-error;(d) isInvalidApprovalDecisionError helper 单测覆盖 4 个 边界 case~22 tests/unit/approval-command-parser.test.ts
    ★ v3.2(v3.4 修订)两种顺序 × 10 个 alias = 20 个合法 case + 5 个 malformed case + 上游 commands-approve.ts:19-30 alias 集合对照断言(CI 断言:上游 alias 数 = channel 端 regex 覆盖数)~26 - tests/unit/approval-callback-handler.test.tscardPrivateData 结构化 parse(含 button index 后缀)、调 resolver、按 result 分支处理(resolved/unauthorized/已处理或已过期);以 §1.2 的真机回调样本作 fixture(不重复测 resolver 内部逻辑——那由 resolver test 覆盖)~14 + tests/unit/approval-callback-handler.test.ts
    ★ v3.12 补 invalid-decision 分支cardPrivateData 结构化 parse(含 button index 后缀 + v3.12 新增 fallback:actionIds[0] / analysis.actionId 精确匹配)、调 resolver、按 result 分支处理 5 类 reason:
    + • ok=true → 验证调 applyResolvedPatch(参 §1.X 三变量)
    + • unauthorized → 验证调 sendProactiveTextOrMarkdown(forceMarkdown:true) + 卡片不变
    + • invalid-decision(v3.12)→ 验证 不调 applyExpiredPatch,调 sendProactiveTextOrMarkdown 含 allowedDecisions 文案,断言按钮保持 pending(mock updateCardVariables 不被 invoke)
    + • already-resolved / not-found → 验证调 applyExpiredPatch(参 §1.X 三变量,无终态文字 PUT)
    + • gateway-error → 验证私聊提示稍后重试,且不调 patcher(按钮保持 pending)
    + 以 §1.2 的真机回调样本作 fixture(不重复测 resolver 内部逻辑——那由 resolver test 覆盖)~18 tests/unit/card-callback-service.test.ts(扩展既有)D16 改动:analyzeCardCallbackcardPrivateData 含 actionIds + params;既有 feedback / btn_stop 用例不受影响+6 - tests/unit/approval-command-intercept.test.tsparser 命中 → 调 resolver;未授权 → sendProactiveTextOrMarkdown(mock);resolve-failed 仅 log;非 /approve 命令 return false;不重复测 parser / resolver 内部~8 + tests/unit/approval-command-intercept.test.ts
    ★ v3.12 补 invalid-decision 分支parser 命中 → 调 resolver;按 result 分支:
    + • unauthorized → sendProactiveTextOrMarkdown(forceMarkdown:true) 私聊拒绝
    + • malformed → sendProactiveTextOrMarkdown(forceMarkdown:true) 格式提示
    + • invalid-decision(v3.12)→ sendProactiveTextOrMarkdown(forceMarkdown:true) 提示重选 + 含 allowedDecisions 文案
    + • not-found / already-resolved → sendProactiveTextOrMarkdown 轻提示
    + • gateway-error → sendProactiveTextOrMarkdown(forceMarkdown:true) 私聊提示稍后重试
    + 非 /approve 命令 return false;不重复测 parser / resolver 内部~12 tests/unit/inbound-handler-approve-intercept.test.ts§6.8 在 inbound-handler 的接入:群里带 @mention 前缀、私聊、return 后不进 reply 派发(验证 session lock 不被触发)~8 - tests/unit/approval-card-locator.test.ts
    ★ v3.3 新增D22 核心:active card 命中(PROCESSING/INPUTING)返 entry;FINISHED/STOPPED/FAILED 返 null;无 entry 返 null;mock card-run-registry~8 - tests/unit/approval-card-patcher.test.ts
    ★ v3.3 替代 card-render.testapplyPendingPatch(注入 3 按钮 + 隐藏 btn_stop D23)、applyResolvedPatch(清按钮 + 写终态指示 + 恢复 btn_stop)、applyExpiredPatch;mock updateCardVariables~14 + tests/unit/approval-card-locator.test.ts
    ★ v3.3 新增D22 核心:active card 命中(PROCESSING/INPUTING)返 entry;FINISHED/STOPPED/FAILED 返 null;无 entry 返 null;同一卡片已有不同 pendingApprovalId 时返 null 让并发审批降级 markdown;mock card-run-registry~8 + tests/unit/approval-card-patcher.test.ts
    ★ v3.3 替代 card-render.test三个 patcher 函数与 §1.X 单一事实表对照:applyPendingPatch(show_approve_btns=true + approveId=<id> + hasAction=false D23)、applyResolvedPatch(show_approve_btns=false + approveId="" + hasAction 按 cardStillActive)、applyExpiredPatch(同 resolved 字段集);mock updateCardVariables;断言 v1 不写终态文字(无 status / statusFooter 字段 PUT)~14 tests/unit/approval-markdown-render.test.ts
    ★ v3.3 替代 fallback-render.testbuildExecApprovalMarkdown / buildPluginApprovalMarkdown:含 /approve 三种 decision 命令模板、过期 hint、id 显示~8 tests/unit/approval-capability.test.tsSDK 工厂参数装配正确、capability 单例~6 tests/unit/approval-native-runtime.test.ts4 子 adapter(availability/presentation/transport/observe)集成 mock;含 v1 origin-only 路径~14 - tests/integration/approval-end-to-end.test.ts模拟 createApprovalRequest → 投递(仅 origin)→ 点击 → resolve 回写 → 卡片刷新;含 self-approval、multi-approver 竞争点击、非 approver 拒绝~10 + tests/integration/approval-end-to-end.test.ts模拟 createApprovalRequest → 投递(仅 origin)→ 点击 → resolve 回写 → 卡片刷新;含 self-approval、multi-approver 竞争点击、非 approver 拒绝。当前功能分支尚未创建该 integration 文件,若作为合并门禁需补齐;若先合并,应转为后续验证 TODO。~10

    @@ -1768,8 +2091,8 @@

    9.1 测试文件布局

    9.2 Mock 策略

    • 所有 DingTalk HTTP(createAndDeliver / updateCardVariables / sendProactiveTextOrMarkdown):vi.mock("../../src/http-client")vi.mock("../../src/auth"),不打真实 API
    • -
    • 上游 SDK 工厂:vi.mock("openclaw/plugin-sdk") 注入 spy,验证传入参数完整
    • -
    • 上游 gateway resolve:vi.mock 抽象的 invoke,断言 method + payload
    • +
    • 上游 SDK 工厂:按实现真实 import subpath mock(例如 openclaw/plugin-sdk/approval-delivery-runtime / openclaw/plugin-sdk/approval-gateway-runtime),不要 mock 根 entry 后假设能拦截全部 subpath import
    • +
    • 上游 gateway resolve:mock resolveApprovalOverGateway,断言 resolveMethod 仅在 plugin 路径传入;exec 默认路径不传 resolveMethod,只传 allowPluginFallback
    • 测试用 fresh instance / clearMocks 避免跨 case 污染(现有 vitest clearMocks/restoreMocks/mockReset 全局开启)
    @@ -1783,8 +2106,9 @@

    9.3 关键 integration 场景(v3:仅 origin)

  • card patch 模糊失败不降级(v3.4):route="card" → mock applyPendingPatch 抛 socket timeout → deliverPending 返 null(不重发避免双消息)→ 验证 sendProactiveTextOrMarkdown 未被调
  • 用户敲 /approve 命令:群里 @bot /approve abc once → inbound-handler 早期 intercept → resolveApprovalOverGateway → 不进 reply 派发(不触发 session lock)
  • 未配置 approver:execApprovals 缺省 → isConfigured=false → shouldHandle=false → 上游不会调 DingTalk 路径
  • -
  • turnSourceChannel 非 dingtalk(CLI 触发):shouldHandle=false → 跳过 DingTalk 投递;用户在钉钉里手敲 /approve 完成审批
  • -
  • channel 重启后点旧按钮:mock resolveApprovalOverGateway 抛 not-found → catch 分支 update 卡片为"ℹ️ 已处理或已过期"
  • +
  • turnSourceChannel 非 dingtalk(CLI 触发):shouldHandle=false → 跳过 DingTalk 投递;钉钉里无 approval 痕迹。用户须**先从 CLI/WebUI/日志拿到 approval id**,再到钉钉敲 /approve 完成(非天然兜底,是 out-of-band 操作)
  • +
  • channel 重启后点旧按钮:mock resolveApprovalOverGateway 抛 not-found → callback handler 调 applyExpiredPatch(断言 PUT 三变量:show_approve_btns=false + approveId="" + hasAction 按 cardStillActive 恢复;断言无 status / statusFooter 终态文字 PUT
  • +
  • 用户点了 request 不允许的 decision(v3.11 策略重新设计):mock ExecApprovalRequest.allowedDecisions=["allow-once","deny"](ask="always" 场景)→ 用户点"总是允许"按钮 → callback handler 解码 decision="allow-always" → resolver 调 gateway → mock gateway 抛 { gatewayCode: "INVALID_REQUEST", details: { reason: "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" } } → resolver 识别为 reason="invalid-decision" → 验证 callback handler 不调 applyExpiredPatch调 sendProactiveTextOrMarkdown(forceMarkdown:true,含 approval id + 提示文案);断言卡片按钮保持 pending 状态;plugin 路径 mock { gatewayCode: "INVALID_REQUEST", details: { allowedDecisions: ["allow-once"] } } 同源覆盖
  • 9.4 覆盖率目标

    @@ -1846,14 +2170,14 @@

    阶段 1 · 接口骨架 + 统一 resolver + 命令链路(PR-1)

    阶段 2 · 完整 native runtime(PR-2)

      -
    • 实现 v3.3 渲染层:approval-card-patcher.ts(D22 落地:在原 agent card 上 patch 按钮 / 终态指示)+ approval-markdown-render.ts(markdown 路径主路径)
    • +
    • 实现 v3.3 渲染层:approval-card-patcher.ts(D22 落地:在原 agent card 上 toggle 三变量 show_approve_btns / approveId / hasAction,参 §1.X 单一事实表;v1 不写终态文字)+ approval-markdown-render.ts(markdown 路径主路径)
    • 实现 approval-callback-handler.ts(用 parseApprovalFromCardPrivateData → 调阶段 1 已有的 approval-resolver.resolveApproval——零新增权限逻辑;resolved 后调 approval-card-patcher.applyResolvedPatch 更新原 agent card)、approval-native-runtime.ts(4 子 adapter;interactions 不实现;transport 内部按 preparedTarget.route 分支调 patcher 或 markdown-render)
    • -
    • 【BLOCKER】修改 src/card-callback-service.ts(D16):当前 src/card-callback-service.ts:6CardCallbackAnalysis 接口只暴露 actionId,approval handler 拿不到 params.actionparams.approveId。必须扩展接口加 cardPrivateData?: { actionIds?: string[]; params?: Record<string, unknown> } 字段,analyzeCardCallbackcontent / value 嵌套 JSON 抽 params 并附到 analysis。这是 callback handler 能工作的硬前置——没这步什么都跑不通
    • +
    • 【BLOCKER】修改 src/card-callback-service.ts(D16;v3.9 改动面收紧):当前 src/card-callback-service.ts:6CardCallbackAnalysis 接口只暴露 actionId,approval handler 拿不到 params.actionparams.approveIdv3.9 实测改动面 ~5 行——核对 src/card-callback-service.ts:100-110analyzeCardCallback 内部已经在解析三层 embedded JSON(record / value / content)找到了 cardPrivateData 对象,只是没把它附到返回的 analysis。所以改动只有两件事:(a) interface 加 cardPrivateData?: { actionIds?: string[]; params?: Record<string, unknown> } 字段;(b) 函数末尾把已解析的 cardPrivateData 拷到返回值。仍是硬前置 BLOCKER(缺这步什么都跑不通),但工作量比"重写嵌套 JSON 解析"小得多
    • 修改 src/card-service.ts(D24 v3.6):createAICard 显式写 show_approve_btns:"false" + approveId:"";finalize / stop / 错误路径也写 false——避免 v3 模板默认值导致 agent reply 一上线就显示未绑 approval 的按钮
    • -
    • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支(在 feedback / btn_stop 之前;callback 命中后通过 outTrackId 找到原 agent card 调 patcher)
    • +
    • src/gateway/channel-gateway.ts 接入 tryHandleApprovalCallback 分支(v3.9 精确:放 handleCardAction 之前src/gateway/channel-gateway.ts:383;feedback path channel-gateway.ts:352-382 因 actionId 仅匹配 feedback_up/down,与 approval 三按钮 actionId 永远不冲突,放前后均可;callback 命中后通过 outTrackId 找到原 agent card 调 patcher)
    • 在阶段 1 的 capability 里挂上 nativeRuntime(4 子 adapter;v1 不实现 interactions)
    • 测试:approval-card-patcher(核心:按钮注入 + 终态 patch + btn_stop 恢复策略 D23)+ approval-markdown-render(文案 + alias) + callback-handler(cardPrivateData 解析 → resolver → patcher);resolver 的 unit test 在 PR-1 已完成,PR-2 仅加 callback 入口集成测试覆盖"按钮 → resolver → patcher"链路 + agent card 状态正确同步
    • -
    • 真机回归(参照 skills/dingtalk-real-device-testing/SKILL.md):重点验证 agent reply card 中途出现 approval 按钮 + 点按钮后按钮消失 + agent 继续 stream + 终态指示正确写入
    • +
    • 真机回归(参照 skills/dingtalk-real-device-testing/SKILL.md):重点验证 agent reply card 中途出现 approval 按钮(show_approve_btns 切 true)+ 点按钮后按钮消失(show_approve_btns 切 false + approveId 清空)+ agent 继续 stream(hasAction 按 cardStillActive 恢复 btn_stop);v1 不验证终态文字(schema 无字段位)
    • 这阶段交付后:完整 v3.3 双路由 UX 在真机可用
    @@ -1899,11 +2223,16 @@

    11.1 明确不在本 spec 范围

  • 统一交互状态模型(gap 文档 #01 sub-6):v1 仅为 approval 实现 pending/resolved/expired 状态机;将之普适到其它交互需要更多用例验证,留待 #12 message tool action surface
  • 主动 rebind on restart(D12 B 选项):留待 v2
  • interactions sub-adapter:v1 不实现,仅在 DM 启用后才有实际收益
  • -
  • respect 上游 allowedDecisions 字段(v3.8 limitation 备注,明确不做): +
  • respect 上游 allowedDecisions 字段(v3.11 策略重新设计)
    上游 ExecApprovalRequest.allowedDecisionsopenclaw/src/infra/exec-approvals.ts:1241)与 PluginApprovalRequest.allowedDecisionsopenclaw/src/infra/plugin-approvals.ts:54)允许 per-request 限制可选 decision。例如 ask="always" 时 exec 只允许 allow-once + deny(不允许 allow-always)。 -
    v1 模板按钮组 固定 3 按钮全部显示,未读取 request 的 allowedDecisions 来动态隐藏不支持的选项。 -
    用户行为:若点击 approval 不支持的 decision(如 ask=always 时点 allow-always),上游 resolveApprovalOverGateway 会拒绝该 decision,channel 端 callback handler 会进 resolve-failed 分支 → 卡片刷"ℹ️ 已处理或已过期"。用户体感 = "点了一下卡片刷掉但没成功"。 -
    已知降级。修复需要:(a) v3 模板新增 3 个 boolean 变量(如 show_allow_once_btn / show_allow_always_btn / show_deny_btn)+ 按钮显示控制条件细化;(b) channel 端 patcher 按 request.allowedDecisions 设变量值。v1 不实现,留待用户反馈后再加
  • +
    v1 模板按钮组 固定 3 按钮全部显示动态隐藏不支持的选项(这是 limitation)。 +
    +
    v3.11 新策略(修订 v3.8 之前的"刷过期态"错误处理):用户点了不允许的 decision 时,channel 把卡片刷成终态——而是: +
    (a) approval-resolver 识别上游 invalid-decision 错误(exec details.reason="APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" / plugin details.allowedDecisions=[...],参 openclaw/src/gateway/server-methods/exec-approval.ts:45-46,468 + plugin-approval.ts:184,200),返 reason="invalid-decision"; +
    (b) callback handler 私聊提示用户"该审批不支持,请选其它"(含 approval id),卡片保持 pending; +
    (c) 用户可再次点其它按钮或敲 /approve 命令完成审批。 +
    +
    未来 v1.x 升级路径(按用户反馈再加):v3 模板新增 3 个 boolean 变量(show_allow_once_btn / show_allow_always_btn / show_deny_btn)+ patcher 按 request.allowedDecisions 动态设变量值,从根本避免用户点到不支持的按钮
  • 11.2 已知风险

    @@ -1928,7 +2257,7 @@

    11.2 已知风险

    DingTalk 平台未来变更 cardPrivateData 字段命名 所有 approval 按钮点击解析失败 → 卡片按钮点了无反应 - parseApprovalFromCardPrivateData 失败时降级到 actionId.startsWith("approval") 简单匹配 + 从 actionId 末尾索引推 decision(兜底但精度差);同时上游 monitor warn 上报,触发 hotfix + v3.10 修订(清理 v3.2 残留):parseApprovalFromCardPrivateData 主路径 = params.action ∈ {allow-once, allow-always, deny};fallback 路径 = actionId 精确匹配三种 decision(不再用已废弃的 actionId.startsWith("approval") 前缀匹配——旧 v3.2 按钮命名才有 approval0/1/2 形态,v3.5+ 用户实配独立 actionId 后该 fallback 永不命中)。同时上游 monitor warn 上报,触发 hotfix;老卡片 approval0/1/2 历史兼容仅作测试 case 保留 机器人 DM approver 失败(企业权限) @@ -1947,7 +2276,7 @@

    11.2 已知风险

    v1 origin-only 在 CLI 触发场景下无 DM 兜底 - 用户必须主动到钉钉里手敲 /approve <id> <decision> 才能批准 + 用户必须**先从外部界面获取 approval id**(CLI 终端 / WebUI / OpenClaw 日志),再到钉钉敲 /approve <id> <decision>。**不是天然兜底**——v1 没有"钉钉端被动看见 id"的路径 v1:在 OpenClaw CLI 输出里清晰打印 approval id 与命令模板;v2:启用 DM 投递自动到达 approver diff --git a/docs/plans/2026-05-19-gap-01-approval-native.md b/docs/plans/2026-05-19-gap-01-approval-native.md new file mode 100644 index 00000000..718466e0 --- /dev/null +++ b/docs/plans/2026-05-19-gap-01-approval-native.md @@ -0,0 +1,4955 @@ +# Gap #01 · DingTalk Native Approval Implementation Plan + +> **For implementers:** 本文保留原 task-by-task 执行结构;实施前仍以当前源码和上游 OpenClaw 真实签名为准,不要只照伪代码搬运。 + +**Goal:** 为 `openclaw-channel-dingtalk` 接入 OpenClaw 原生审批能力(exec + plugin approval),交付 `/approve` 命令路径 + AI Card 按钮路径双轨 UX。 + +**Architecture:** +- v1 仅 origin-only 投递(D4),不投 approver DM、不引入本地 approval store(D18)、不做 finalize-on-stop(D13)。 +- AI Card 路径在原 agent reply card 上 PUT cardParamMap 三变量(`show_approve_btns` / `approveId` / `hasAction`,参 spec §1.X 单一事实表),按钮组定义在 v3 模板内置;markdown 路径发独立消息含 `/approve ` 模板。 +- 按钮回调 + `/approve` 命令两条入口都收敛到 `approval-resolver.ts` 单点,再调上游 SDK 公开 API `resolveApprovalOverGateway`。 +- 原设计按 3 个 PR 顺序交付:PR-1 接口骨架 + 命令通道;PR-2 完整 native runtime + 模板替换 + 真机回归;PR-3 文档。当前功能分支已把这些改动收敛到同一分支,文中的 PR 标签仅作为历史边界和 review 分区。 + +**Tech Stack:** TypeScript(strict, ES2023)· Vitest(V8 coverage)· oxlint + oxfmt · pnpm · OpenClaw `>=2026.4.7` peer SDK · DingTalk Stream SDK + Open API。 + +**Source spec:** `docs/features/2026-05-18-gap-01-approval-native-design.html` (v3.12, 2329 行)。所有 §X.Y 引用均指向该 spec。 + +--- + +## 文件结构 + +### 新增(src/approval/ 目录,~900 行业务 + ~2000 行测试) + +| 文件 | 单一职责 | PR | +|---|---|---| +| `src/approval/approval-config.ts` | 读 `execApprovals` schema helper(list/isAuth/resolveMode) | PR-1 | +| `src/approval/approval-command-parser.ts` | 纯解析 `/approve` 文本(20 合法形式) | PR-1 | +| `src/approval/approval-target-resolver.ts` | v1 仅 `resolveOriginTarget` | PR-1 | +| `src/approval/approval-resolver.ts` | D20 单点:kind 推导 + 授权 + gateway 调用 + 5 类错误分类 | PR-1 | +| `src/approval/approval-card-locator.ts` | 按 sessionKey 查 card-run-registry,D22 路由决策 | PR-1 | +| `src/approval/approval-command-intercept.ts` | `/approve` early intercept 入口 | PR-1 | +| `src/approval/approval-capability.ts` | SDK 工厂装配 `ChannelApprovalCapability` | PR-1(不含 nativeRuntime)+ PR-2(接上) | +| `src/approval/approval-card-patcher.ts` | 三 patcher:pending/resolved/expired | PR-2 | +| `src/approval/approval-markdown-render.ts` | buildExec/PluginApprovalMarkdown | PR-2 | +| `src/approval/approval-callback-handler.ts` | TOPIC_CARD 入口 → resolver → patcher | PR-2 | +| `src/approval/approval-native-runtime.ts` | 4 子 adapter(availability/presentation/transport/observe) | PR-2 | + +### 修改(向后兼容) + +| 文件 | 改动要点 | PR | +|---|---|---| +| `package.json` | `peerDependencies.openclaw` bump `2026.3.28` → `>=2026.4.7` | PR-1(前置) | +| `pnpm-lock.yaml` | 同步 lockfile,确认 `node_modules/openclaw` 版本 | PR-1(前置) | +| `src/types.ts` | 加 `ExecApprovalsConfig` + `ApprovalDecision` + `ApprovalPhase`;`DingTalkConfig` 加 `execApprovals?` 字段 | PR-1 | +| `src/config-schema.ts` | 加 `execApprovalsSchema`;挂到 `DingTalkConfigSchema` + account schema | PR-1 | +| `src/config.ts:279-310` | `resolveDingTalkAccount` default-account 路径 rawConfig 加 `execApprovals: dingtalk?.execApprovals,` | PR-1 | +| `src/channel.ts:22-127` | plugin 对象加 `approvalCapability` 字段 | PR-1 | +| `src/inbound-handler.ts:~770` | 早期 intercept `/approve`(早于 L817/L874/L2053) | PR-1 | +| `src/card/card-template.ts:6` | `BUILTIN_DINGTALK_CARD_TEMPLATE_ID` v2 → v3:`58f73932-fc3b-46ae-8e90-93313e405061.schema` | PR-2 | +| `src/card-callback-service.ts` | `CardCallbackAnalysis` 加 `cardPrivateData?` 字段(~5 行) | PR-2 | +| `src/card-service.ts:~802` | createAICard/finalize 路径 cardParamMap 显式补 `show_approve_btns:"false"` + `approveId:""` | PR-2 | +| `src/card/card-run-registry.ts` | 加 `resolveActiveCardRunBySession` / `isActiveCardRun` / `pendingApprovalId` 字段 / mark/clear API | PR-1(locator 需要的部分)+ PR-2(pendingApprovalId) | +| `src/gateway/channel-gateway.ts:~383` | TOPIC_CARD listener 在 `handleCardAction` 之前插 `tryHandleApprovalCallback` 分支 | PR-2 | + +### 资产 + +- `docs/assets/card-template-v3.json`(268KB,已 commit) — v3 模板低代码 schema,含 `approve_btns` + `show_approve_btns` + `approveId`。 + +### 测试(tests/unit/ + tests/integration/) + +| 测试文件 | PR | ~case 数 | +|---|---|---| +| `tests/unit/approval-config.test.ts` | PR-1 | 12 | +| `tests/unit/approval-command-parser.test.ts` | PR-1 | 26 | +| `tests/unit/approval-target-resolver.test.ts` | PR-1 | 10 | +| `tests/unit/approval-resolver.test.ts` | PR-1 | 22 | +| `tests/unit/approval-card-locator.test.ts` | PR-1 | 8 | +| `tests/unit/approval-command-intercept.test.ts` | PR-1 | 12 | +| `tests/unit/approval-capability.test.ts` | PR-1 + PR-2 增量 | 6 | +| `tests/unit/inbound-handler-approve-intercept.test.ts` | PR-1 | 8 | +| `tests/unit/config.test.ts`(扩) | PR-1 | +4 | +| `tests/unit/approval-card-patcher.test.ts` | PR-2 | 14 | +| `tests/unit/approval-markdown-render.test.ts` | PR-2 | 8 | +| `tests/unit/approval-callback-handler.test.ts` | PR-2 | 18 | +| `tests/unit/approval-native-runtime.test.ts` | PR-2 | 14 | +| `tests/unit/card-callback-service.test.ts`(扩) | PR-2 | +6 | +| `tests/integration/approval-end-to-end.test.ts` | PR-2 | 12(DEFERRED:当前分支尚未落地,见 Task 21) | + +--- + +## 通用约定 + +- TDD 严格遵循:每个 task 都是「写失败测试 → 跑确认 fail → 实现 → 跑确认 pass → commit」。 +- 测试 mock 网络:`vi.mock("../../src/http-client")` + `vi.mock("../../src/auth")`;上游 SDK **必须按 impl 真实 import subpath mock**(参 Stage 0.A 导入子路径表),如 `vi.mock("openclaw/plugin-sdk/approval-gateway-runtime")` —— 不要 mock 根 entry `"openclaw/plugin-sdk"`,因为 impl 从 subpath import 时根 mock **不会拦截**;`vi.mock("../../src/card-callback-service", ...)` 视模块需要。`clearMocks/restoreMocks/mockReset` 已在 vitest 全局开启。 +- 日志统一前缀 `[DingTalk][Approval]`(参 CLAUDE.md 约定 + 现有 `[DingTalk][AICard]` 模式)。 +- 所有 `sendProactiveTextOrMarkdown` 调用 **必须** 传 `forceMarkdown: true`(messageType=card 配置下否则会回退发卡片,参 `src/send-service.ts:371-393`)。 +- 不引入 `@ts-ignore`;oxlint 必须通过;commit 前跑 `pnpm run type-check && pnpm run lint && pnpm test`。 +- Commit 信息使用约定:`feat(approval): ...` / `test(approval): ...` / `chore(approval): ...` / `docs(approval): ...`;BREAKING 在 footer 注明。 +- **署名按需添加**:commit 模板里不预填 `Co-Authored-By:`。如果执行者是 AI agent,按各自约定追加(如 Claude Code 按 `.claude/settings.json` 行为;其他 agent 按其规则;人工执行可省)。 +- 每完成一个 Task 立即 commit;PR 边界处提示用户开 PR review。 + +--- + +# PR-1 · 接口骨架 + 统一 resolver + `/approve` 命令链路 + +**交付目标:** DingTalk 端具备 `/approve` 命令的 resolve 通道(权限校验 + 早期 intercept 绕过 session lock + resolver 单点收敛);按钮 UX 留待 PR-2。 +**授权边界:** `/approve` 是消息路径能力,当前实现位置在 DM/group access control 之后、session lock/routing 之前;因此它同时受 channel 普通访问策略和 `execApprovals.approvers` 约束。卡片按钮回调不是普通消息路径,只受 approver 名单约束。若后续产品要求“approver 可在被普通 allowlist 拦截的会话中仍通过 `/approve`”,需要把 intercept 前移并补对应访问控制测试。 + +**PR-1 任务清单:** Stage 0 + Task 1 ~ Task 11。 + +--- + +## Stage 0 · 源码签名核对表 + SDK 基线四件套(D17,PR-1 前置 BLOCKER) + +### Stage 0.A · 源码签名核对表(实施前用 Read 核对,不要相信 spec / plan 内的伪代码签名) + +| 名称 | 真实签名 / 字段 | 路径 | +|---|---|---| +| `ExecApprovalRequest` | `{ id, request: ExecApprovalRequestPayload, createdAtMs, expiresAtMs }`
    **`turnSourceChannel` / `turnSourceTo` / `turnSourceAccountId` / `turnSourceThreadId` / `sessionKey` / `allowedDecisions` / `agentId` / `command` / `cwd` 全部在 `request.request.*`**(嵌套 payload,不是顶层!) | `openclaw/src/infra/exec-approvals.ts:117-140` | +| `PluginApprovalRequest` | 同形:`{ id, request: PluginApprovalRequestPayload, createdAtMs, expiresAtMs }`,payload 含 `pluginId / title / description / severity / toolName / toolCallId / allowedDecisions / agentId / sessionKey / turnSourceChannel / turnSourceTo / turnSourceAccountId / turnSourceThreadId` | `openclaw/src/infra/plugin-approvals.ts:3-30` | +| `ChannelApprovalNativeRuntimeAdapter` | 3 必需 + 2 可选:`availability / presentation / transport` + `interactions? / observe?` | `openclaw/src/infra/approval-handler-runtime-types.ts:216-235` | +| `createLazyChannelApprovalNativeRuntimeAdapter` | **不是简单的字面量打包**——只接受 `{ load: () => Promise, isConfigured, shouldHandle, eventKinds?, resolveApprovalKind? }`,把 availability/presentation/transport/observe 都塞同一对象会 type-check fail。
    **v1 不用此 lazy 包装**——直接 `return { eventKinds, availability, presentation, transport, observe }`。 | `openclaw/src/infra/approval-handler-adapter-runtime.ts:10-...` | +| `createApproverRestrictedNativeApprovalCapability` | SDK 工厂(16 参数) | `openclaw/src/plugin-sdk/approval-delivery-helpers.ts:30-261` | +| **`createChannelNativeOriginTargetResolver`** | **target-resolver 上游 helper**:channel + shouldHandleRequest + resolveTurnSourceTarget(request) + resolveSessionTarget(sessionTarget, request) + normalizeTarget?(target, request);接受 `ApprovalResolverParams { cfg, accountId, request }`,**input.request 是 ApprovalRequest 整体;helper 内部访问 input.request.request.\* payload** | `openclaw/src/plugin-sdk/approval-native-helpers.ts:137-153` | +| `createChannelApproverDmTargetResolver` | DM 目标 helper(v1 不用,v2 future) | `openclaw/src/plugin-sdk/approval-native-helpers.ts:155-182` | +| `resolveApprovalOverGateway` | SDK 公开 API(v2026.4.7+) | `openclaw/src/plugin-sdk/approval-gateway-runtime.ts:1`(re-export 也在 `approval-handler-runtime.ts:31`) | +| `resolveDingTalkAccount(cfg, accountId)` | 返回 **`ResolvedDingTalkAccount extends DingTalkConfig`**——**配置字段直接挂顶层**(如 `account.execApprovals`),不是 `account.config.execApprovals`;额外字段 `{ accountId, configured }` | `src/config.ts:263-272` | +| `registerCardRun(outTrackId, params)` | **签名是 `(outTrackId: string, params: { accountId, sessionKey, agentId, ownerUserId?, card?, registeredAt? })`**;**字段名是 `registeredAt`**(不是 `createdAt`) | `src/card/card-run-registry.ts:13-25`(record)+ `:56`(fn 签名) | +| `CardRunRecord` | `{ outTrackId, accountId, sessionKey, agentId, ownerUserId?, card?, controller?, stopRequestedAt?, registeredAt }`,无 `createdAt` 字段 | `src/card/card-run-registry.ts:13` | +| `CardCallbackAnalysis` | 当前接口字段(PR-2 之前)—— 实施前 Read 核对扩展点 | `src/card-callback-service.ts:1-30` | +| **`updateCardVariables(outTrackId, params, token, config?)`** | **返回 `Promise` HTTP status code**;失败靠 axios throw(**不是** `{ ok, error }`);第 4 个可选 `config` 仅取 `bypassProxyForSend`。patcher / runtime 调用必须传 `config` 否则会绕过 proxy 配置 | `src/card-callback-service.ts:175-201` | +| **`getAccessToken(config, log?)`** | **签名是 `(config: DingTalkConfig, log?: Logger)`**——传入**已解析的 DingTalkConfig**(用 `getConfig(cfg, accountId)` 先解析),不是 `(cfg, accountId)` | `src/auth.ts:18` | +| `sendProactiveTextOrMarkdown(config, target, text, opts)` | `opts` 含 `forceMarkdown?: boolean`;`messageType=card` 时不传 forceMarkdown 会被发成卡片 | `src/send-service.ts:352-482`(判断在 :371-393) | +| `getLogger(accountId?)` | 项目约定的 logger 入口;**不要用 `console.*`**——`src/` 全部用 `getLogger()?.info/warn/error` | `src/logger-context.ts:25` | +| `Logger` 类型 | 本仓库别名 `ChannelLogSink`;从 `../types` 导入,**不是** SDK 根 | `src/types.ts:583` | + +> **使用规则:** +> 1. 写代码 / 测试前 `Read` 上面任何一个文件,**以源文件为准**,本 plan 内 / spec 内的伪代码签名都是辅助说明。 +> 2. 任何 task 实施时如果发现 plan 内的伪代码与真实签名冲突,**fix plan 而非编造类型**——把 Drift 记在 commit message。 + +### Stage 0.A · 导入子路径权威表(openclaw/plugin-sdk 根入口很瘦;不要从根 import 这些符号) + +| 符号 | 子路径 | peer 引用样本 | +|---|---|---| +| `OpenClawConfig` | `openclaw/plugin-sdk/core` | `src/channel.ts:1` / `src/types.ts:17`(本仓库现有用法) | +| `Logger` 类型 | 本仓库 `../types`(再导出 `ChannelLogSink`) | `src/types.ts:583` | +| `getLogger` | 本仓库 `../logger-context` | `src/logger-context.ts:25` | +| `ChannelApprovalCapability` | `openclaw/plugin-sdk/channel-contract` | `openclaw/extensions/telegram/src/approval-native.ts:15` | +| `createApproverRestrictedNativeApprovalCapability`
    `splitChannelApprovalCapability` | `openclaw/plugin-sdk/approval-delivery-runtime` | telegram :1-4 | +| `createLazyChannelApprovalNativeRuntimeAdapter` | `openclaw/plugin-sdk/approval-handler-adapter-runtime` | telegram :5 | +| `ChannelApprovalNativeRuntimeAdapter` (type) | `openclaw/plugin-sdk/approval-handler-runtime` | telegram :6 | +| `createChannelNativeOriginTargetResolver`
    `createChannelApproverDmTargetResolver` | `openclaw/plugin-sdk/approval-native-runtime` | telegram :7-10 | +| `NativeApprovalTarget` (type) | **不从公共子路径导出**——上游定义在 internal `approval-native-helpers.ts`。channel 模块自定义本地 `DingTalkApprovalTarget` 类型,通过 helper 泛型参数传入(参 Task 5 + telegram peer 同模式) | `openclaw/src/plugin-sdk/approval-native-helpers.ts:51`(仅参考,不 import) | +| `ExecApprovalRequest` / `PluginApprovalRequest` (types) | `openclaw/plugin-sdk/approval-runtime` | telegram :11-14 | +| `resolveApprovalOverGateway` | `openclaw/plugin-sdk/approval-gateway-runtime`
    (或 `openclaw/plugin-sdk/approval-handler-runtime` re-export) | `openclaw/src/plugin-sdk/approval-handler-runtime.ts:31` | +| 字符串/normalize helper | `openclaw/plugin-sdk/string-coerce-runtime` | telegram :17-20 | + +> **不要从 `openclaw/plugin-sdk` 根 import 上述符号**——根入口故意做得很瘦(仅 channel plugin 公共 surface),上述类型/函数都在 subpath。如发现 plan 内还有从根 import 的,按本表纠正。 + +### Stage 0.B · SDK 基线四件套(peerDep + lockfile + manifest + tsconfig) + +**Files:** +- Modify: `package.json`(peer + manifest 三处) +- Modify: `pnpm-lock.yaml` +- Verify: `node_modules/openclaw/package.json`、`tsconfig.json` + +- [ ] **Step 0.1: bump peerDependency + manifest 三处** + +修改 `package.json` 内 4 处 `2026.3.28` → `2026.4.7`: + +```json +"peerDependencies": { + "openclaw": ">=2026.4.7" +}, +... +"openclaw": { + "compat": { + "pluginApi": ">=2026.4.7" + }, + "build": { + "openclawVersion": "2026.4.7" + }, + ... + "install": { + "minHostVersion": ">=2026.4.7", + ... + } +} +``` + +> 4 处必须同步——`peerDependencies` 控 npm 安装时的 peer 提示;`openclaw.compat.pluginApi` 与 `openclaw.install.minHostVersion` 控 OpenClaw 主程序的 manifest 兼容性检查;`openclaw.build.openclawVersion` 控构建期 SDK 类型基线。少改任何一处都会出现"npm 装得上但 OpenClaw 拒绝加载"或反之的不一致。 + +- [ ] **Step 0.2: 同步 lockfile + 安装** + +Run: `pnpm install` +Expected: 安装成功,`node_modules/openclaw/package.json` `"version"` 字段 `>=2026.4.7`。 + +- [ ] **Step 0.3: 验证 type-check 通过** + +Run: `pnpm run type-check` +Expected: 0 错误。若 fail 提示 `ChannelApprovalNativeRuntimeAdapter` 等类型找不到,确认 `tsconfig.json` paths 优先级;monorepo 场景可临时把 `../openclaw/src/plugin-sdk` 调到 `node_modules/openclaw/dist` 之前。 + +- [ ] **Step 0.4: Commit** + +```bash +git add package.json pnpm-lock.yaml +git commit -m "$(cat <<'EOF' +chore(deps): bump openclaw peer/manifest baseline to >=2026.4.7 + +4 处同步:peerDependencies + openclaw.compat.pluginApi + +openclaw.build.openclawVersion + openclaw.install.minHostVersion。 +获取 ChannelApprovalNativeRuntimeAdapter 契约 + +resolveApprovalOverGateway 公开 API。 + +BREAKING CHANGE: openclaw peer 升级到 2026.4.7+,老版本(2026.3.28)不再支持;OpenClaw host 端 manifest 兼容性同步收紧。 +EOF +)" +``` + +--- + +## Task 1 · 类型与配置 schema 准备 + +**Files:** +- Modify: `src/types.ts` +- Modify: `src/config-schema.ts` +- Test: `tests/unit/approval-config-schema.test.ts`(新建) + +- [ ] **Step 1.1: 在 src/types.ts 加 approval 类型** + +文件末尾追加: + +```typescript +export type ApprovalDecision = "allow-once" | "allow-always" | "deny"; +export type ApprovalPhase = "pending" | "resolved" | "expired"; + +export interface ExecApprovalsConfig { + enabled?: boolean | "auto"; + approvers?: string[]; +} +``` + +并在 `DingTalkConfig` 接口加 `execApprovals?: ExecApprovalsConfig;` 字段(紧贴 `learningEnabled` 等同级配置之后)。 + +- [ ] **Step 1.2: 写 config schema 失败测试** + +新增 `tests/unit/approval-config-schema.test.ts`: + +```typescript +import { describe, it, expect } from "vitest"; +import { DingTalkConfigSchema } from "../../src/config-schema"; + +describe("DingTalkConfigSchema · execApprovals", () => { + it("接受 enabled=auto + approvers 列表", () => { + const parsed = DingTalkConfigSchema.parse({ + clientId: "x", + clientSecret: "y", + execApprovals: { enabled: "auto", approvers: ["staff001"] }, + }); + expect(parsed.execApprovals?.enabled).toBe("auto"); + expect(parsed.execApprovals?.approvers).toEqual(["staff001"]); + }); + + it("接受 enabled=true / false", () => { + expect(() => + DingTalkConfigSchema.parse({ + clientId: "x", clientSecret: "y", + execApprovals: { enabled: true, approvers: [] }, + }), + ).not.toThrow(); + expect(() => + DingTalkConfigSchema.parse({ + clientId: "x", clientSecret: "y", + execApprovals: { enabled: false }, + }), + ).not.toThrow(); + }); + + it("execApprovals 完全省略时合法(向后兼容)", () => { + expect(() => + DingTalkConfigSchema.parse({ clientId: "x", clientSecret: "y" }), + ).not.toThrow(); + }); + + it("approvers 元素必须是 string", () => { + expect(() => + DingTalkConfigSchema.parse({ + clientId: "x", clientSecret: "y", + execApprovals: { approvers: [123 as unknown as string] }, + }), + ).toThrow(); + }); +}); +``` + +- [ ] **Step 1.3: 跑测试确认 fail** + +Run: `pnpm vitest run tests/unit/approval-config-schema.test.ts` +Expected: FAIL(schema 不识别 execApprovals 字段或字段缺失)。 + +- [ ] **Step 1.4: 在 src/config-schema.ts 加 schema** + +在 `src/config-schema.ts` 加入: + +```typescript +const ExecApprovalsConfigSchema = z.object({ + enabled: z.union([z.boolean(), z.literal("auto")]).optional(), + approvers: z.array(z.string()).optional(), +}).strict(); +``` + +并把 `execApprovals: ExecApprovalsConfigSchema.optional(),` 加到 `DingTalkConfigSchema` 与 account override schema 两处。导出 `ExecApprovalsConfigSchema` 供测试用。 + +> **`.strict()` 的语义边界**:v1 schema **有意**拒绝 `target` / `ttlMs` 等 spec §4.2 列为"v2 future"的字段。这样可以让用户取消 v2 注释的 yaml 时立即得到清晰的 parse 错误,而不是默默忽略导致部分行为不生效。 +> +> 若想让 schema 接受未知字段以便 forward-compat,去掉 `.strict()` 即可——但这会让 v2 future config 出现"看起来生效但 v1 不读"的迷惑行为。**v1 选 strict + future-field test 强对齐**。 +> +> 在 Step 1.2 的测试里**加一个 case**显式断言 strict 行为: +> +> ```typescript +> it("拒绝 v2 future 字段 target / ttlMs(strict 边界)", () => { +> expect(() => +> DingTalkConfigSchema.parse({ +> clientId: "x", clientSecret: "y", +> execApprovals: { approvers: ["s"], target: "dm" } as never, +> }), +> ).toThrow(); +> expect(() => +> DingTalkConfigSchema.parse({ +> clientId: "x", clientSecret: "y", +> execApprovals: { approvers: ["s"], ttlMs: 600000 } as never, +> }), +> ).toThrow(); +> }); +> ``` +> +> 当 v2 future 实施时**同时**更新 schema 接受这两个字段 + 删掉本测试。 + +- [ ] **Step 1.5: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/approval-config-schema.test.ts` +Expected: 5 PASS(含 strict 边界 case)。 + +- [ ] **Step 1.6: 类型一致性验证** + +Run: `pnpm run type-check` +Expected: 0 错误。 + +- [ ] **Step 1.7: Commit** + +```bash +git add src/types.ts src/config-schema.ts tests/unit/approval-config-schema.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 ExecApprovalsConfig 类型与 schema + +为 DingTalk channel 增加 execApprovals 配置块(enabled + approvers), +为 Gap #01 approver 名单与 enabled 三态做类型与 schema 准备。 + +EOF +)" +``` + +--- + +## Task 2 · config.ts default-account 路径补 execApprovals + +**Files:** +- Modify: `src/config.ts:279-310`(`resolveDingTalkAccount` 的 default-account rawConfig 字面量) +- Modify: `tests/unit/config.test.ts` + +- [ ] **Step 2.1: 写 default account 配置遗漏测试** + +在 `tests/unit/config.test.ts` 末尾追加: + +```typescript +// resolveDingTalkAccount 返回 ResolvedDingTalkAccount extends DingTalkConfig +// 字段直接挂顶层(参 Stage 0.A 签名核对表):account.execApprovals,不是 account.config.execApprovals +describe("resolveDingTalkAccount · execApprovals 字段传递", () => { + it("default 账号能拿到 channel 级 execApprovals", () => { + const cfg = { + channels: { + dingtalk: { + clientId: "x", clientSecret: "y", + execApprovals: { enabled: "auto", approvers: ["staff001"] }, + }, + }, + }; + const account = resolveDingTalkAccount(cfg, undefined); + expect(account.execApprovals?.approvers).toEqual(["staff001"]); + expect(account.execApprovals?.enabled).toBe("auto"); + }); + + it("account override 完全替换 channel 级 approvers(不合并)", () => { + const cfg = { + channels: { + dingtalk: { + clientId: "x", clientSecret: "y", + execApprovals: { approvers: ["staffA"] }, + accounts: { + acme: { + clientId: "x", clientSecret: "y", + execApprovals: { approvers: ["staffB"] }, + }, + }, + }, + }, + }; + const acme = resolveDingTalkAccount(cfg, "acme"); + expect(acme.execApprovals?.approvers).toEqual(["staffB"]); + }); + + it("account 未配 execApprovals 时继承 channel-level(spread 自动 cover)", () => { + const cfg = { + channels: { + dingtalk: { + clientId: "x", clientSecret: "y", + execApprovals: { approvers: ["staffA"] }, + accounts: { acme: { clientId: "x", clientSecret: "y" } }, + }, + }, + }; + const acme = resolveDingTalkAccount(cfg, "acme"); + expect(acme.execApprovals?.approvers).toEqual(["staffA"]); + }); + + it("channel 级未配 execApprovals 时 default 账号 execApprovals 为 undefined", () => { + const cfg = { channels: { dingtalk: { clientId: "x", clientSecret: "y" } } }; + const account = resolveDingTalkAccount(cfg, undefined); + expect(account.execApprovals).toBeUndefined(); + }); +}); +``` + +- [ ] **Step 2.2: 跑测试确认 fail(至少第 1 个 case fail)** + +Run: `pnpm vitest run tests/unit/config.test.ts -t "execApprovals 字段传递"` +Expected: 第 1 个 case FAIL(default 账号拿不到 execApprovals);第 3 个可能 PASS(spread 自动 cover)。 + +- [ ] **Step 2.3: 在 src/config.ts:279-310 rawConfig 字面量加 execApprovals** + +在 default-account 路径 `rawConfig` 字面量末尾(与 `learningNoteTtlMs` 同级,保持字母顺序或紧贴 `learningEnabled` 等扩展字段后)补一行: + +```typescript +execApprovals: dingtalk?.execApprovals, +``` + +`mergeAccountWithDefaults` 的 spread 模式(src/config.ts:60-85)自动保留新字段,不需要改。 + +- [ ] **Step 2.4: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/config.test.ts -t "execApprovals 字段传递"` +Expected: 4 PASS。 + +- [ ] **Step 2.5: Commit** + +```bash +git add src/config.ts tests/unit/config.test.ts +git commit -m "$(cat <<'EOF' +fix(config): default 账号补 execApprovals 字段拷贝 + +resolveDingTalkAccount default 路径用字面量构造 rawConfig,必须显式列出 +execApprovals 字段否则多账号场景 default 账号完全拿不到该配置。 +account-level 路径走 mergeAccountWithDefaults 的 spread 模式自动 cover。 + +EOF +)" +``` + +--- + +## Task 3 · approval-config.ts(读 helper) + +**Files:** +- Create: `src/approval/approval-config.ts` +- Test: `tests/unit/approval-config.test.ts` + +- [ ] **Step 3.1: 写失败测试** + +```typescript +import { describe, it, expect } from "vitest"; +import { + getExecApprovalsConfig, + listExecApprovers, + isExecAuthorizedSender, + isPluginAuthorizedSender, + resolveNativeDeliveryMode, +} from "../../src/approval/approval-config"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; + +const cfg = (approvers: string[], opts: { ownerAllowFrom?: string[]; enabled?: boolean | "auto" } = {}): OpenClawConfig => + ({ + channels: { + dingtalk: { + clientId: "x", clientSecret: "y", + execApprovals: { enabled: opts.enabled ?? "auto", approvers }, + }, + }, + commands: { ownerAllowFrom: opts.ownerAllowFrom }, + }) as unknown as OpenClawConfig; + +describe("approval-config", () => { + it("listExecApprovers 返回去重 normalize 后的 staffId 列表", () => { + const c = cfg(["staff001", "dingtalk:staff002", "DD:staff003", "ding:staff001"]); + expect(listExecApprovers({ cfg: c, accountId: "default" })).toEqual([ + "staff001", "staff002", "staff003", + ]); + }); + + it("approvers 为空时 fallback 到 commands.ownerAllowFrom", () => { + const c = cfg([], { ownerAllowFrom: ["staff999"] }); + expect(listExecApprovers({ cfg: c, accountId: "default" })).toEqual(["staff999"]); + }); + + it("isExecAuthorizedSender 名单内 staffId 返回 true", () => { + const c = cfg(["staff001"]); + expect(isExecAuthorizedSender({ cfg: c, accountId: "default", senderId: "staff001" })).toBe(true); + expect(isExecAuthorizedSender({ cfg: c, accountId: "default", senderId: "staff999" })).toBe(false); + }); + + it("isExecAuthorizedSender 接受 dingtalk:/dd:/ding: 前缀的 senderId", () => { + const c = cfg(["staff001"]); + expect(isExecAuthorizedSender({ cfg: c, accountId: "default", senderId: "dingtalk:staff001" })).toBe(true); + }); + + it("isPluginAuthorizedSender 默认 = isExecAuthorizedSender", () => { + const c = cfg(["staff001"]); + expect(isPluginAuthorizedSender({ cfg: c, accountId: "default", senderId: "staff001" })).toBe(true); + }); + + it("enabled=false 时 getExecApprovalsConfig.enabled 显式 false(即使 approvers 非空)", () => { + const c = cfg(["staff001"], { enabled: false }); + const conf = getExecApprovalsConfig({ cfg: c, accountId: "default" }); + expect(conf.enabled).toBe(false); + }); + + it("enabled=auto + approvers 非空时 isNativeDeliveryEnabled 返回 true", () => { + const c = cfg(["staff001"]); + const conf = getExecApprovalsConfig({ cfg: c, accountId: "default" }); + expect(conf.isNativeDeliveryEnabled).toBe(true); + }); + + it("enabled=auto + approvers 为空时 isNativeDeliveryEnabled 返回 false", () => { + const c = cfg([]); + expect(getExecApprovalsConfig({ cfg: c, accountId: "default" }).isNativeDeliveryEnabled).toBe(false); + }); + + it('resolveNativeDeliveryMode 在 v1 永远返回 "channel"', () => { + const c = cfg(["staff001"]); + expect(resolveNativeDeliveryMode({ cfg: c, accountId: "default" })).toBe("channel"); + }); +}); +``` + +- [ ] **Step 3.2: 跑测试确认 fail** + +Run: `pnpm vitest run tests/unit/approval-config.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 3.3: 实现 src/approval/approval-config.ts** + +```typescript +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import { getConfig } from "../config"; + +const PREFIX_RE = /^(dingtalk|dd|ding):/i; +const normalizeStaffId = (raw: string): string => raw.replace(PREFIX_RE, "").trim(); + +export interface ApprovalConfigQuery { + cfg: OpenClawConfig; + accountId: string; +} + +export interface ResolvedExecApprovalsConfig { + enabled: boolean | "auto" | undefined; + approvers: string[]; + isNativeDeliveryEnabled: boolean; +} + +export function listExecApprovers({ cfg, accountId }: ApprovalConfigQuery): string[] { + const account = getConfig(cfg, accountId); + const raw = account?.execApprovals?.approvers ?? []; + const fallback = raw.length === 0 ? (cfg.commands?.ownerAllowFrom ?? []) : raw; + const seen = new Set(); + const out: string[] = []; + for (const item of fallback) { + const id = normalizeStaffId(item); + if (id && !seen.has(id)) { + seen.add(id); + out.push(id); + } + } + return out; +} + +export function getExecApprovalsConfig( + q: ApprovalConfigQuery, +): ResolvedExecApprovalsConfig { + const account = getConfig(q.cfg, q.accountId); + const enabled = account?.execApprovals?.enabled; + const approvers = listExecApprovers(q); + const isNativeDeliveryEnabled = enabled === false ? false : approvers.length > 0; + return { enabled, approvers, isNativeDeliveryEnabled }; +} + +export function isExecAuthorizedSender({ + cfg, accountId, senderId, +}: ApprovalConfigQuery & { senderId: string }): boolean { + const approvers = listExecApprovers({ cfg, accountId }); + const normalized = normalizeStaffId(senderId); + return approvers.includes(normalized); +} + +export function isPluginAuthorizedSender( + q: ApprovalConfigQuery & { senderId: string }, +): boolean { + return isExecAuthorizedSender(q); +} + +export function resolveNativeDeliveryMode(_q: ApprovalConfigQuery): "channel" { + return "channel"; +} +``` + +- [ ] **Step 3.4: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/approval-config.test.ts` +Expected: 9 PASS。 + +- [ ] **Step 3.5: Lint + type-check** + +Run: `pnpm run type-check && pnpm run lint` +Expected: 0 错误。 + +- [ ] **Step 3.6: Commit** + +```bash +git add src/approval/approval-config.ts tests/unit/approval-config.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 approval-config 读 helper + +新增 listExecApprovers / getExecApprovalsConfig / isExecAuthorizedSender / +isPluginAuthorizedSender / resolveNativeDeliveryMode 5 个纯读 helper。 +支持 dingtalk:/dd:/ding: 前缀 normalize、commands.ownerAllowFrom fallback。 +enabled=auto 即"有 approvers 就启用";v1 deliveryMode 永远 channel。 + +EOF +)" +``` + +--- + +## Task 4 · approval-command-parser.ts(纯解析 10 alias × 2 顺序) + +**Files:** +- Create: `src/approval/approval-command-parser.ts` +- Test: `tests/unit/approval-command-parser.test.ts` + +- [ ] **Step 4.1: 写失败测试** + +```typescript +import { describe, it, expect } from "vitest"; +import { parseApproveCommand } from "../../src/approval/approval-command-parser"; + +const ALIAS_ALLOW_ONCE = ["allow", "once", "allow-once", "allowonce"] as const; +const ALIAS_ALLOW_ALWAYS = ["always", "allow-always", "allowalways"] as const; +const ALIAS_DENY = ["deny", "reject", "block"] as const; + +describe("parseApproveCommand", () => { + describe('order A: /approve ', () => { + for (const a of ALIAS_ALLOW_ONCE) + it(`/approve abc ${a} → allow-once`, () => + expect(parseApproveCommand(`/approve abc ${a}`)).toEqual({ approvalId: "abc", decision: "allow-once" })); + for (const a of ALIAS_ALLOW_ALWAYS) + it(`/approve abc ${a} → allow-always`, () => + expect(parseApproveCommand(`/approve abc ${a}`)).toEqual({ approvalId: "abc", decision: "allow-always" })); + for (const a of ALIAS_DENY) + it(`/approve abc ${a} → deny`, () => + expect(parseApproveCommand(`/approve abc ${a}`)).toEqual({ approvalId: "abc", decision: "deny" })); + }); + + describe('order B: /approve ', () => { + for (const a of ALIAS_ALLOW_ONCE) + it(`/approve ${a} abc → allow-once`, () => + expect(parseApproveCommand(`/approve ${a} abc`)).toEqual({ approvalId: "abc", decision: "allow-once" })); + for (const a of ALIAS_ALLOW_ALWAYS) + it(`/approve ${a} abc → allow-always`, () => + expect(parseApproveCommand(`/approve ${a} abc`)).toEqual({ approvalId: "abc", decision: "allow-always" })); + for (const a of ALIAS_DENY) + it(`/approve ${a} abc → deny`, () => + expect(parseApproveCommand(`/approve ${a} abc`)).toEqual({ approvalId: "abc", decision: "deny" })); + }); + + it("接受裸 approve(无前导斜杠)", () => { + expect(parseApproveCommand("approve abc once")).toEqual({ approvalId: "abc", decision: "allow-once" }); + }); + + it("大小写不敏感的 decision alias", () => { + expect(parseApproveCommand("/approve abc ALLOW")).toEqual({ approvalId: "abc", decision: "allow-once" }); + }); + + it("approvalId 保留原始大小写(不 normalize)", () => { + expect(parseApproveCommand("/approve ABC-123 deny")?.approvalId).toBe("ABC-123"); + }); + + it("malformed: 缺 decision 或 id 返 null", () => { + expect(parseApproveCommand("/approve")).toBeNull(); + expect(parseApproveCommand("/approve abc")).toBeNull(); + expect(parseApproveCommand("/approve abc xyz")).toBeNull(); + expect(parseApproveCommand("approve foo bar baz qux")).toBeNull(); + expect(parseApproveCommand("")).toBeNull(); + }); + + it("alias 数量 == 上游 commands-approve.ts:19-30 的 10 个", () => { + const channelAliasCount = ALIAS_ALLOW_ONCE.length + ALIAS_ALLOW_ALWAYS.length + ALIAS_DENY.length; + expect(channelAliasCount).toBe(10); + }); +}); +``` + +- [ ] **Step 4.2: 跑测试确认 fail** + +Run: `pnpm vitest run tests/unit/approval-command-parser.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 4.3: 实现 src/approval/approval-command-parser.ts** + +```typescript +import type { ApprovalDecision } from "../types"; + +const ALIAS_MAP: Record = { + // allow-once(4) + "allow": "allow-once", "once": "allow-once", + "allow-once": "allow-once", "allowonce": "allow-once", + // allow-always(3) + "always": "allow-always", "allow-always": "allow-always", "allowalways": "allow-always", + // deny(3) + "deny": "deny", "reject": "deny", "block": "deny", +}; + +export interface ParsedApproveCommand { + approvalId: string; + decision: ApprovalDecision; +} + +const HEAD = /^\/?approve(?:\s|$)/i; + +export function parseApproveCommand(text: string): ParsedApproveCommand | null { + if (!text) return null; + const trimmed = text.trim(); + if (!HEAD.test(trimmed)) return null; + const tokens = trimmed.split(/\s+/); + if (tokens.length !== 3) return null; + const [, a, b] = tokens; + const aDecision = ALIAS_MAP[a.toLowerCase()]; + const bDecision = ALIAS_MAP[b.toLowerCase()]; + if (aDecision && !bDecision) return { approvalId: b, decision: aDecision }; + if (bDecision && !aDecision) return { approvalId: a, decision: bDecision }; + return null; +} +``` + +- [ ] **Step 4.4: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/approval-command-parser.test.ts` +Expected: 26 PASS。 + +- [ ] **Step 4.5: Commit** + +```bash +git add src/approval/approval-command-parser.ts tests/unit/approval-command-parser.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 /approve 命令解析器 + +支持 10 个 decision alias × 2 种顺序 = 20 合法形式, +对齐上游 openclaw/src/auto-reply/reply/commands-approve.ts:19-30。 +regex 与上游 COMMAND_REGEX = /^\\/?approve(?:\\s|$)/i 完全对齐(前导斜杠可选;严格要求空格或行尾分隔, +避免 /approve! /approve-x 等误命中——\\b 比上游更宽松,不要用)。 + +EOF +)" +``` + + +--- + +## Task 5 · approval-target-resolver.ts(薄壳——复用上游 helper) + +**Files:** +- Create: `src/approval/approval-target-resolver.ts` +- Test: `tests/unit/approval-target-resolver.test.ts` + +**关键设计决策:** 复用上游 `createChannelNativeOriginTargetResolver`(`openclaw/src/plugin-sdk/approval-native-helpers.ts:137-153`,参 Stage 0.A)做 turnSource / session target / fallback / accountId 一致性校验,channel 端只保留 DingTalk 专属的 `normalizeApprovalTargetTo`(前缀补全)。peer telegram 同模式(`openclaw/extensions/telegram/src/approval-native.ts:64`)。 + +注意上游 helper 的 input 签名:`(input: ApprovalResolverParams) => target | null`,其中 `input.request` 是 `ExecApprovalRequest | PluginApprovalRequest`(嵌套),**payload 字段在 `input.request.request.turnSourceChannel`** 等(参 Stage 0.A)。 + +- [ ] **Step 5.1: 写失败测试** + +```typescript +import { describe, it, expect } from "vitest"; +import { + normalizeApprovalTargetTo, + resolveDingTalkOriginTarget, +} from "../../src/approval/approval-target-resolver"; + +const req = (payload: Partial<{ + turnSourceChannel: string | null; + turnSourceTo: string | null; + turnSourceAccountId: string | null; + turnSourceThreadId: string | number | null; + sessionKey: string | null; +}>) => + ({ + id: "abc", createdAtMs: 0, expiresAtMs: 0, + request: { ...payload }, + }) as never; + +describe("normalizeApprovalTargetTo", () => { + it("带 group: 前缀的输入原样保留", () => { + expect(normalizeApprovalTargetTo("group:cidxxxxx")).toBe("group:cidxxxxx"); + }); + it("带 user: 前缀的输入原样保留", () => { + expect(normalizeApprovalTargetTo("user:staff001")).toBe("user:staff001"); + }); + it("裸 cid 开头加 group: 前缀", () => { + expect(normalizeApprovalTargetTo("cidxxxxx")).toBe("group:cidxxxxx"); + }); + it("裸 staffId 加 user: 前缀", () => { + expect(normalizeApprovalTargetTo("staff001")).toBe("user:staff001"); + }); +}); + +describe("resolveDingTalkOriginTarget(用上游 helper 装配)", () => { + it("turnSourceChannel != dingtalk → null", () => { + const r = resolveDingTalkOriginTarget({ + cfg: {} as never, accountId: "default", + request: req({ turnSourceChannel: "discord", turnSourceTo: "group:c" }), + }); + expect(r).toBeNull(); + }); + + it("turnSourceTo 为空 → null", () => { + const r = resolveDingTalkOriginTarget({ + cfg: {} as never, accountId: "default", + request: req({ turnSourceChannel: "dingtalk", turnSourceTo: null }), + }); + expect(r).toBeNull(); + }); + + it("dingtalk + group:cid 形态 → normalize 并返带 prefix 的 target(threadId null)", () => { + const r = resolveDingTalkOriginTarget({ + cfg: {} as never, accountId: "default", + request: req({ turnSourceChannel: "dingtalk", turnSourceTo: "group:cidxxx" }), + }); + expect(r).toEqual(expect.objectContaining({ to: "group:cidxxx" })); + }); + + it("dingtalk + 裸 cid 形态 → 加 group: 前缀", () => { + const r = resolveDingTalkOriginTarget({ + cfg: {} as never, accountId: "default", + request: req({ turnSourceChannel: "dingtalk", turnSourceTo: "cidxxx" }), + }); + expect(r?.to).toBe("group:cidxxx"); + }); + + it("dingtalk + 裸 staffId 形态 → 加 user: 前缀", () => { + const r = resolveDingTalkOriginTarget({ + cfg: {} as never, accountId: "default", + request: req({ turnSourceChannel: "dingtalk", turnSourceTo: "staff001" }), + }); + expect(r?.to).toBe("user:staff001"); + }); + + it("turnSourceAccountId != input.accountId → null(上游 helper 内置校验)", () => { + const r = resolveDingTalkOriginTarget({ + cfg: {} as never, accountId: "acme", + request: req({ turnSourceChannel: "dingtalk", turnSourceTo: "group:c", turnSourceAccountId: "other" }), + }); + expect(r).toBeNull(); + }); + + it("保留 turnSourceAccountId + turnSourceThreadId(向上游 target 透传)", () => { + const r = resolveDingTalkOriginTarget({ + cfg: {} as never, accountId: "acme", + request: req({ + turnSourceChannel: "dingtalk", + turnSourceTo: "group:c", + turnSourceAccountId: "acme", + turnSourceThreadId: "thread-xyz", + }), + }); + expect(r).toEqual(expect.objectContaining({ + to: "group:c", accountId: "acme", threadId: "thread-xyz", + })); + }); +}); +``` + +> 注:`turnSourceAccountId != accountId → null` 这一行依赖上游 helper 内置 account-binding 校验;如本地核对发现行为不同(如返非空),按上游真实行为调整断言。 + +- [ ] **Step 5.2: 跑测试确认 fail** + +Run: `pnpm vitest run tests/unit/approval-target-resolver.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 5.3: 实现 src/approval/approval-target-resolver.ts** + +```typescript +import { createChannelNativeOriginTargetResolver } from "openclaw/plugin-sdk/approval-native-runtime"; +import type { + ExecApprovalRequest, + PluginApprovalRequest, +} from "openclaw/plugin-sdk/approval-runtime"; + +type ApprovalRequest = ExecApprovalRequest | PluginApprovalRequest; + +// 本地 target 类型(telegram peer 同模式:用本地 alias,不 import) +// 上游 NativeApprovalTarget 实际在 approval-native-helpers.ts,但只有 runtime.ts 是公共子路径 +// 入口;runtime.ts 没 re-export 这个 type 别名(核实 Stage 0.A)。我们参照 telegram 的 +// TelegramOriginTarget 同模式,在 channel 模块内自定义最小形态: +export type DingTalkApprovalTarget = { + to: string; + accountId?: string | null; + // 保留 threadId 即使 v1 不用——上游 NativeApprovalTarget 形状有此字段,留着排查 / 未来扩展更稳 + threadId?: string | number | null; +}; + +export function normalizeApprovalTargetTo(raw: string): string { + if (/^(user|group):/i.test(raw)) return raw; + if (/^cid/i.test(raw)) return `group:${raw}`; + return `user:${raw}`; +} + +function resolveTurnSourceTarget(request: ApprovalRequest): DingTalkApprovalTarget | null { + const payload = request.request; + if (payload.turnSourceChannel !== "dingtalk") return null; + if (!payload.turnSourceTo) return null; + return { + to: normalizeApprovalTargetTo(payload.turnSourceTo), + accountId: payload.turnSourceAccountId ?? null, + threadId: payload.turnSourceThreadId ?? null, + }; +} + +function resolveSessionTarget( + sessionTarget: { to: string; accountId?: string | null; threadId?: string | number | null } | null, + _request: ApprovalRequest, +): DingTalkApprovalTarget | null { + if (!sessionTarget?.to) return null; + return { + to: normalizeApprovalTargetTo(sessionTarget.to), + accountId: sessionTarget.accountId ?? null, + threadId: sessionTarget.threadId ?? null, + }; +} + +export const resolveDingTalkOriginTarget = createChannelNativeOriginTargetResolver({ + channel: "dingtalk", + resolveTurnSourceTarget, + resolveSessionTarget, + normalizeTarget: (target) => ({ + ...target, + to: normalizeApprovalTargetTo(target.to), + }), +}); +``` + +> **NativeApprovalTarget 导入说明**:上游 `approval-native-runtime.ts` 公共子路径**只 re-export 函数**(`createChannelNativeOriginTargetResolver` 等),没 re-export `NativeApprovalTarget` 类型别名(核实 Stage 0.A)。 +> +> 为避免从 internal `approval-native-helpers.ts` 直接 import 类型(破坏封装),本仓库采用 telegram peer 同模式——在 channel 内自定义本地 `DingTalkApprovalTarget` 类型,通过 `createChannelNativeOriginTargetResolver` 泛型参数告诉上游 helper 这是 channel-specific shape。 +> +> 如果将来 OpenClaw SDK 在 `approval-native-runtime.ts` 加上 `export type { NativeApprovalTarget }` re-export,可以直接 import 并 `DingTalkApprovalTarget extends NativeApprovalTarget`。 + +- [ ] **Step 5.4: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/approval-target-resolver.test.ts` +Expected: 全部 PASS(上游 helper 行为 + DingTalk 专属 normalize)。 + +- [ ] **Step 5.5: Commit** + +```bash +git add src/approval/approval-target-resolver.ts tests/unit/approval-target-resolver.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 approval-target-resolver(复用上游 helper) + +调用上游 createChannelNativeOriginTargetResolver 装配 origin target 解析; +channel 端只保留 DingTalk 专属的 normalizeApprovalTargetTo(user:/group: +前缀补全)。turnSource / session / accountId 一致性校验由上游 helper 内置。 +模式对齐 openclaw/extensions/telegram/src/approval-native.ts:64。 + +resolveApproverDmTargets 推迟 v2(v1 不实现 DM 投递)。 +EOF +)" +``` + +--- + +## Task 6 · approval-resolver.ts(D20 单点 + 5 类错误分类) + +**Files:** +- Create: `src/approval/approval-resolver.ts` +- Test: `tests/unit/approval-resolver.test.ts` + +**关键参考:** +- 上游 `resolveApprovalOverGateway`:`openclaw/src/plugin-sdk/approval-gateway-runtime.ts` +- exec invalid-decision 错误:`openclaw/src/gateway/server-methods/exec-approval.ts:45-46,449-470` +- plugin invalid-decision 错误:`openclaw/src/gateway/server-methods/plugin-approval.ts:184-204` +- `isApprovalNotFoundError`:`openclaw/src/infra/approval-errors.ts` + +- [ ] **Step 6.1: 写失败测试** + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { + resolveApproval, + isInvalidApprovalDecisionError, +} from "../../src/approval/approval-resolver"; + +// mock 必须对齐 impl 的真实 import subpath(Stage 0.A 导入子路径表) +// approval-resolver.ts impl 从 openclaw/plugin-sdk/approval-gateway-runtime import,不是根 entry +vi.mock("openclaw/plugin-sdk/approval-gateway-runtime", () => ({ + resolveApprovalOverGateway: vi.fn(), +})); +vi.mock("../../src/approval/approval-config", () => ({ + isExecAuthorizedSender: vi.fn(() => true), + isPluginAuthorizedSender: vi.fn(() => true), +})); + +const { resolveApprovalOverGateway } = await import("openclaw/plugin-sdk/approval-gateway-runtime"); +const { isExecAuthorizedSender, isPluginAuthorizedSender } = await import("../../src/approval/approval-config"); + +const base = { cfg: {} as never, accountId: "default", senderId: "staffA", log: undefined as never }; +const mockGw = resolveApprovalOverGateway as unknown as ReturnType; + +describe("approval-resolver · kind 推导(D21)", () => { + beforeEach(() => mockGw.mockReset()); + + it("approvalId 带 plugin: 前缀 → resolveMethod=plugin", async () => { + mockGw.mockResolvedValue({}); + await resolveApproval({ ...base, approvalId: "plugin:xyz", decision: "allow-once" }); + expect(mockGw).toHaveBeenCalledWith(expect.objectContaining({ resolveMethod: "plugin" })); + }); + + it("无前缀 + 两边都授权 → 默认 exec + allowPluginFallback=true", async () => { + mockGw.mockResolvedValue({}); + (isExecAuthorizedSender as ReturnType).mockReturnValueOnce(true); + (isPluginAuthorizedSender as ReturnType).mockReturnValueOnce(true); + await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + expect(mockGw).toHaveBeenCalledWith(expect.objectContaining({ + allowPluginFallback: true, + })); + expect(mockGw.mock.calls[0][0]).not.toHaveProperty("resolveMethod"); + }); + + it("无前缀 + 仅 plugin 授权 → resolveMethod=plugin", async () => { + mockGw.mockResolvedValue({}); + (isExecAuthorizedSender as ReturnType).mockReturnValueOnce(false); + (isPluginAuthorizedSender as ReturnType).mockReturnValueOnce(true); + await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + expect(mockGw).toHaveBeenCalledWith(expect.objectContaining({ resolveMethod: "plugin" })); + }); + + it("无前缀 + 仅 exec 授权 → 默认 exec(无 plugin fallback)", async () => { + mockGw.mockResolvedValue({}); + (isExecAuthorizedSender as ReturnType).mockReturnValueOnce(true); + (isPluginAuthorizedSender as ReturnType).mockReturnValueOnce(false); + await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + expect(mockGw).toHaveBeenCalledWith(expect.objectContaining({ + allowPluginFallback: false, + })); + expect(mockGw.mock.calls[0][0]).not.toHaveProperty("resolveMethod"); + }); + + it("两边都未授权 → 返 unauthorized 且不调 gateway", async () => { + (isExecAuthorizedSender as ReturnType).mockReturnValueOnce(false); + (isPluginAuthorizedSender as ReturnType).mockReturnValueOnce(false); + const r = await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + expect(r).toEqual({ ok: false, reason: "unauthorized" }); + expect(mockGw).not.toHaveBeenCalled(); + }); +}); + +describe("approval-resolver · 错误分类(5 类)", () => { + beforeEach(() => { + mockGw.mockReset(); + (isExecAuthorizedSender as ReturnType).mockReturnValue(true); + (isPluginAuthorizedSender as ReturnType).mockReturnValue(true); + }); + + it("gateway 抛 APPROVAL_NOT_FOUND → not-found", async () => { + mockGw.mockRejectedValue(Object.assign(new Error("not found"), { gatewayCode: "APPROVAL_NOT_FOUND" })); + const r = await resolveApproval({ ...base, approvalId: "abc", decision: "deny" }); + expect(r).toEqual(expect.objectContaining({ ok: false, reason: "not-found" })); + }); + + it("gateway 抛 APPROVAL_ALREADY_RESOLVED → already-resolved", async () => { + mockGw.mockRejectedValue(Object.assign(new Error("already"), { gatewayCode: "APPROVAL_ALREADY_RESOLVED" })); + const r = await resolveApproval({ ...base, approvalId: "abc", decision: "deny" }); + expect(r).toEqual(expect.objectContaining({ ok: false, reason: "already-resolved" })); + }); + + it("exec invalid-decision(reason=APPROVAL_ALLOW_ALWAYS_UNAVAILABLE) → invalid-decision", async () => { + mockGw.mockRejectedValue(Object.assign(new Error("invalid"), { + gatewayCode: "INVALID_REQUEST", + details: { reason: "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" }, + })); + const r = await resolveApproval({ ...base, approvalId: "abc", decision: "allow-always" }); + expect(r.ok).toBe(false); + expect(r.reason).toBe("invalid-decision"); + }); + + it("plugin invalid-decision(allowedDecisions 数组) → invalid-decision 且透传 allowedDecisions", async () => { + mockGw.mockRejectedValue(Object.assign(new Error("invalid"), { + gatewayCode: "INVALID_REQUEST", + details: { allowedDecisions: ["allow-once", "deny"] }, + })); + const r = await resolveApproval({ ...base, approvalId: "plugin:p", decision: "allow-always" }); + expect(r).toEqual(expect.objectContaining({ + ok: false, reason: "invalid-decision", allowedDecisions: ["allow-once", "deny"], + })); + }); + + it("非 invalid-decision 的 INVALID_REQUEST 归 gateway-error", async () => { + mockGw.mockRejectedValue(Object.assign(new Error("misc"), { + gatewayCode: "INVALID_REQUEST", details: { other: true }, + })); + const r = await resolveApproval({ ...base, approvalId: "abc", decision: "deny" }); + expect(r.reason).toBe("gateway-error"); + }); + + it("其它任意错误 → gateway-error", async () => { + mockGw.mockRejectedValue(new Error("network down")); + const r = await resolveApproval({ ...base, approvalId: "abc", decision: "deny" }); + expect(r.reason).toBe("gateway-error"); + }); + + it("成功 → ok=true", async () => { + mockGw.mockResolvedValue({}); + const r = await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + expect(r).toEqual({ ok: true }); + }); +}); + +describe("isInvalidApprovalDecisionError helper", () => { + it("识别 exec 形态", () => { + expect(isInvalidApprovalDecisionError({ + gatewayCode: "INVALID_REQUEST", + details: { reason: "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" }, + })).toBe(true); + }); + it("识别 plugin 形态", () => { + expect(isInvalidApprovalDecisionError({ + gatewayCode: "INVALID_REQUEST", + details: { allowedDecisions: ["allow-once"] }, + })).toBe(true); + }); + it("不识别 details 为空的 INVALID_REQUEST", () => { + expect(isInvalidApprovalDecisionError({ gatewayCode: "INVALID_REQUEST" })).toBe(false); + }); + it("不识别非 INVALID_REQUEST", () => { + expect(isInvalidApprovalDecisionError({ gatewayCode: "NETWORK_ERROR" })).toBe(false); + }); +}); +``` + +- [ ] **Step 6.2: 跑测试确认 fail** + +Run: `pnpm vitest run tests/unit/approval-resolver.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 6.3: 实现 src/approval/approval-resolver.ts** + +```typescript +import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import type { ApprovalDecision, Logger } from "../types"; +import { + isExecAuthorizedSender, + isPluginAuthorizedSender, +} from "./approval-config"; + +export type ResolverReason = + | "unauthorized" + | "already-resolved" + | "not-found" + | "invalid-decision" + | "gateway-error"; + +export type ResolverResult = + | { ok: true } + | { + ok: false; + reason: ResolverReason; + error?: unknown; + allowedDecisions?: string[]; + }; + +export interface ResolveApprovalInput { + cfg: OpenClawConfig; + accountId: string; + approvalId: string; + decision: ApprovalDecision; + senderId: string; + log?: Logger; +} + +export function isInvalidApprovalDecisionError(err: unknown): boolean { + if (!err || typeof err !== "object") return false; + const e = err as { gatewayCode?: unknown; details?: { reason?: unknown; allowedDecisions?: unknown } }; + if (e.gatewayCode !== "INVALID_REQUEST") return false; + const d = e.details; + if (!d || typeof d !== "object") return false; + if (d.reason === "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE") return true; + if (Array.isArray(d.allowedDecisions)) return true; + return false; +} + +function extractAllowedDecisions(err: unknown): string[] | undefined { + const d = (err as { details?: { allowedDecisions?: unknown } } | null)?.details; + return Array.isArray(d?.allowedDecisions) ? (d!.allowedDecisions as string[]) : undefined; +} + +function deriveResolveMethod( + approvalId: string, + execAuth: boolean, + pluginAuth: boolean, +): { resolveMethod?: "plugin"; allowPluginFallback?: boolean } | null { + if (!execAuth && !pluginAuth) return null; + if (approvalId.startsWith("plugin:")) return { resolveMethod: "plugin" }; + if (execAuth && pluginAuth) return { allowPluginFallback: true }; + if (pluginAuth) return { resolveMethod: "plugin" }; + return { allowPluginFallback: false }; +} + +export async function resolveApproval(input: ResolveApprovalInput): Promise { + const { cfg, accountId, approvalId, decision, senderId, log } = input; + const execAuth = isExecAuthorizedSender({ cfg, accountId, senderId }); + const pluginAuth = isPluginAuthorizedSender({ cfg, accountId, senderId }); + const method = deriveResolveMethod(approvalId, execAuth, pluginAuth); + if (!method) { + log?.info?.(`[DingTalk][Approval] unauthorized sender=${senderId} approvalId=${approvalId}`); + return { ok: false, reason: "unauthorized" }; + } + try { + await resolveApprovalOverGateway({ + cfg, approvalId, decision, + senderId, clientDisplayName: "DingTalk", + ...method, + }); + return { ok: true }; + } catch (err) { + const code = (err as { gatewayCode?: string } | null)?.gatewayCode; + if (code === "APPROVAL_NOT_FOUND") return { ok: false, reason: "not-found", error: err }; + if (code === "APPROVAL_ALREADY_RESOLVED") return { ok: false, reason: "already-resolved", error: err }; + if (isInvalidApprovalDecisionError(err)) { + return { + ok: false, reason: "invalid-decision", error: err, + allowedDecisions: extractAllowedDecisions(err), + }; + } + log?.warn?.(`[DingTalk][Approval] gateway-error approvalId=${approvalId} err=${(err as Error)?.message}`); + return { ok: false, reason: "gateway-error", error: err }; + } +} +``` + +- [ ] **Step 6.4: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/approval-resolver.test.ts` +Expected: 22 PASS。 + +- [ ] **Step 6.5: type-check + lint** + +Run: `pnpm run type-check && pnpm run lint` +Expected: 0 错误。 + +- [ ] **Step 6.6: Commit** + +```bash +git add src/approval/approval-resolver.ts tests/unit/approval-resolver.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 approval-resolver 单点收敛(D20) + +按 D21 推导 resolveMethod + allowPluginFallback;调上游 SDK +resolveApprovalOverGateway 公开 API;catch 5 类错误:unauthorized / +already-resolved / not-found / invalid-decision / gateway-error。 +isInvalidApprovalDecisionError helper 识别上游 exec/plugin 两种 invalid 形态 +(APPROVAL_ALLOW_ALWAYS_UNAVAILABLE / allowedDecisions[])。 + +EOF +)" +``` + +--- + +## Task 7 · card-run-registry 加 isActiveCardRun + bySession 查询(locator 前置) + +**Files:** +- Modify: `src/card/card-run-registry.ts` +- Test: `tests/unit/card-run-registry-approval.test.ts`(新建,避免污染既有大测试) + +> 注:PR-1 只加 `isActiveCardRun` + `resolveActiveCardRunBySession`(locator 依赖);`pendingApprovalId` 字段与 mark/clear API 推迟到 PR-2 的 Task 14 一起加(避免 PR-1 引入未使用的 setter)。 + +> ⚠️ 实施前必读 Stage 0.A 中的 `registerCardRun` 与 `CardRunRecord` 真实签名: +> - 注册签名:`registerCardRun(outTrackId: string, params: { accountId, sessionKey, agentId, ownerUserId?, card?, registeredAt? })` +> - 字段名:`registeredAt`(**不是** `createdAt`) +> - 既有清理 API:`removeCardRun(outTrackId)` 单个清;`clearCardRunRegistryForTest()` 全清(test-only export,参 `src/card/card-run-registry.ts:165`) + +- [ ] **Step 7.1: 写失败测试** + +```typescript +import { describe, it, expect, beforeEach } from "vitest"; +import { + registerCardRun, + resolveActiveCardRunBySession, + isActiveCardRun, + clearCardRunRegistryForTest, + type CardRunRecord, +} from "../../src/card/card-run-registry"; + +const STATE = (s: string) => ({ state: s } as unknown as CardRunRecord["card"]); + +const register = ( + outTrackId: string, + opts: { + accountId?: string; + sessionKey: string; + agentId?: string; + state?: string; + registeredAt?: number; + }, +) => { + registerCardRun(outTrackId, { + accountId: opts.accountId ?? "default", + sessionKey: opts.sessionKey, + agentId: opts.agentId ?? "agent-default", + card: opts.state ? STATE(opts.state) : undefined, + registeredAt: opts.registeredAt, + }); +}; + +describe("card-run-registry · approval helpers", () => { + beforeEach(() => clearCardRunRegistryForTest()); + + it("isActiveCardRun: PROCESSING / INPUTING 返 true", () => { + const make = (state: string): CardRunRecord => + ({ + outTrackId: "o", accountId: "default", sessionKey: "s", + agentId: "agent", card: STATE(state), registeredAt: Date.now(), + } as CardRunRecord); + expect(isActiveCardRun(make("PROCESSING"))).toBe(true); + expect(isActiveCardRun(make("INPUTING"))).toBe(true); + }); + it("isActiveCardRun: FINISHED / STOPPED / FAILED 返 false", () => { + const make = (state: string): CardRunRecord => + ({ + outTrackId: "o", accountId: "default", sessionKey: "s", + agentId: "agent", card: STATE(state), registeredAt: Date.now(), + } as CardRunRecord); + for (const s of ["FINISHED", "STOPPED", "FAILED"]) { + expect(isActiveCardRun(make(s))).toBe(false); + } + }); + it("isActiveCardRun: card 为 undefined 返 false", () => { + expect(isActiveCardRun({ + outTrackId: "o", accountId: "default", sessionKey: "s", + agentId: "agent", card: undefined, registeredAt: Date.now(), + } as CardRunRecord)).toBe(false); + }); + + it("resolveActiveCardRunBySession: 匹配 accountId + sessionKey 且 active", () => { + register("ot1", { sessionKey: "sess-A", state: "INPUTING" }); + expect(resolveActiveCardRunBySession("default", "sess-A")?.outTrackId).toBe("ot1"); + }); + it("resolveActiveCardRunBySession: accountId 不匹配返 null", () => { + register("ot1", { accountId: "other", sessionKey: "sess-A", state: "INPUTING" }); + expect(resolveActiveCardRunBySession("default", "sess-A")).toBeNull(); + }); + it("resolveActiveCardRunBySession: state 已终止返 null", () => { + register("ot1", { sessionKey: "sess-A", state: "FINISHED" }); + expect(resolveActiveCardRunBySession("default", "sess-A")).toBeNull(); + }); + it("resolveActiveCardRunBySession: sessionKey 不存在返 null", () => { + expect(resolveActiveCardRunBySession("default", "no-such")).toBeNull(); + }); + it("resolveActiveCardRunBySession: 多 record 同 sessionKey 返最新 registeredAt", () => { + register("ot-old", { sessionKey: "sess-A", state: "INPUTING", registeredAt: 1000 }); + register("ot-new", { sessionKey: "sess-A", state: "INPUTING", registeredAt: 2000 }); + expect(resolveActiveCardRunBySession("default", "sess-A")?.outTrackId).toBe("ot-new"); + }); +}); +``` + +- [ ] **Step 7.2: 跑测试确认 fail** + +Run: `pnpm vitest run tests/unit/card-run-registry-approval.test.ts` +Expected: FAIL(API 未实现)。 + +- [ ] **Step 7.3: 在 src/card/card-run-registry.ts 加 helper** + +在文件末尾追加(或紧贴现有 `resolveCardRun` 之后): + +```typescript +export function isActiveCardRun(record: CardRunRecord): boolean { + const state = record.card?.state; + return state === "PROCESSING" || state === "INPUTING"; +} + +export function resolveActiveCardRunBySession( + accountId: string, + sessionKey: string, +): CardRunRecord | null { + let latest: CardRunRecord | null = null; + for (const record of records.values()) { + if (record.accountId !== accountId) continue; + if (record.sessionKey !== sessionKey) continue; + if (!isActiveCardRun(record)) continue; + if (!latest || record.registeredAt > latest.registeredAt) latest = record; + } + return latest; +} +``` + +> `records` 是模块内 Map(参 `src/card/card-run-registry.ts:30`)。`CardRunRecord` 已有 `sessionKey`、`accountId`、`registeredAt`(参 `src/card/card-run-registry.ts:13-25`)。 + +- [ ] **Step 7.4: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/card-run-registry-approval.test.ts` +Expected: 8 PASS。 + +- [ ] **Step 7.5: 跑全部 card-run-registry 相关测试确认无回归** + +Run: `pnpm vitest run tests/unit/card-run-registry` +Expected: 全部 PASS(既有 outTrackId 查询、TTL sweep 等不受影响)。 + +- [ ] **Step 7.6: Commit** + +```bash +git add src/card/card-run-registry.ts tests/unit/card-run-registry-approval.test.ts +git commit -m "$(cat <<'EOF' +feat(card-run-registry): 添加 isActiveCardRun + resolveActiveCardRunBySession + +为 Gap #01 approval-card-locator 提供按 sessionKey + 活跃 state 查询能力。 +不动既有 outTrackId 查询、TTL sweep 行为。pendingApprovalId 字段与 +mark/clear API 推迟到 PR-2(D24 主链路落地时一起加)。 + +EOF +)" +``` + +--- + +## Task 8 · approval-card-locator.ts(D22 路由决策) + +**Files:** +- Create: `src/approval/approval-card-locator.ts` +- Test: `tests/unit/approval-card-locator.test.ts` + +- [ ] **Step 8.1: 写失败测试** + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { findActiveAgentCard } from "../../src/approval/approval-card-locator"; + +vi.mock("../../src/card/card-run-registry", () => ({ + resolveActiveCardRunBySession: vi.fn(), +})); +const { resolveActiveCardRunBySession } = await import("../../src/card/card-run-registry"); +const mockResolver = resolveActiveCardRunBySession as ReturnType; + +describe("approval-card-locator", () => { + beforeEach(() => mockResolver.mockReset()); + + it("registry 命中 active record → 返 { outTrackId, sessionKey }", () => { + mockResolver.mockReturnValue({ outTrackId: "ai_card_xxx", sessionKey: "sess-A" }); + expect(findActiveAgentCard({ cfg: {} as never, accountId: "default", sessionKey: "sess-A" })) + .toEqual({ outTrackId: "ai_card_xxx", sessionKey: "sess-A" }); + }); + + it("registry 返 null → 返 null(caller 走 markdown 路径)", () => { + mockResolver.mockReturnValue(null); + expect(findActiveAgentCard({ cfg: {} as never, accountId: "default", sessionKey: "sess-A" })).toBeNull(); + }); + + it("sessionKey 为空时返 null(不查 registry)", () => { + expect(findActiveAgentCard({ cfg: {} as never, accountId: "default", sessionKey: "" })).toBeNull(); + expect(mockResolver).not.toHaveBeenCalled(); + }); + + it("accountId 透传到 registry 查询", () => { + mockResolver.mockReturnValue(null); + findActiveAgentCard({ cfg: {} as never, accountId: "acme", sessionKey: "s" }); + expect(mockResolver).toHaveBeenCalledWith("acme", "s"); + }); + + it("同一卡片已有不同 pendingApprovalId → 返 null(并发审批降级 markdown)", () => { + mockResolver.mockReturnValue({ + outTrackId: "ai_card_xxx", + sessionKey: "sess-A", + pendingApprovalId: "approval-old", + }); + expect(findActiveAgentCard({ + cfg: {} as never, + accountId: "default", + sessionKey: "sess-A", + approvalId: "approval-new", + })).toBeNull(); + }); + + it("同一卡片 pendingApprovalId 相同 → 仍返 card 路径(同一审批重试幂等)", () => { + mockResolver.mockReturnValue({ + outTrackId: "ai_card_xxx", + sessionKey: "sess-A", + pendingApprovalId: "approval-old", + }); + expect(findActiveAgentCard({ + cfg: {} as never, + accountId: "default", + sessionKey: "sess-A", + approvalId: "approval-old", + })).toEqual({ outTrackId: "ai_card_xxx", sessionKey: "sess-A" }); + }); +}); +``` + +- [ ] **Step 8.2: 跑测试确认 fail** + +Run: `pnpm vitest run tests/unit/approval-card-locator.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 8.3: 实现 src/approval/approval-card-locator.ts** + +```typescript +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import { resolveActiveCardRunBySession } from "../card/card-run-registry"; + +export interface FindActiveAgentCardInput { + cfg: OpenClawConfig; + accountId: string; + sessionKey: string; + approvalId?: string; +} + +export interface ActiveAgentCardLocation { + outTrackId: string; + sessionKey: string; +} + +export function findActiveAgentCard(input: FindActiveAgentCardInput): ActiveAgentCardLocation | null { + if (!input.sessionKey) return null; + const record = resolveActiveCardRunBySession(input.accountId, input.sessionKey); + if (!record) return null; + // v1 同一卡片同一时刻只承载一个 pending approval;后续并发审批走 markdown。 + if (record.pendingApprovalId && record.pendingApprovalId !== input.approvalId) return null; + return { outTrackId: record.outTrackId, sessionKey: record.sessionKey }; +} +``` + +- [ ] **Step 8.4: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/approval-card-locator.test.ts` +Expected: 6 PASS。 + +- [ ] **Step 8.5: Commit** + +```bash +git add src/approval/approval-card-locator.ts tests/unit/approval-card-locator.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 approval-card-locator(D22 路由决策) + +按 sessionKey 查 card-run-registry,仅 active record 返 location; +未命中返 null(caller 走 markdown 路径)。 + +EOF +)" +``` + + +--- + +## Task 9 · approval-command-intercept.ts(薄壳:parser + resolver + 5 reason 分支) + +**Files:** +- Create: `src/approval/approval-command-intercept.ts` +- Test: `tests/unit/approval-command-intercept.test.ts` + +- [ ] **Step 9.1: 写失败测试** + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; +import { tryInterceptApproveCommand } from "../../src/approval/approval-command-intercept"; + +vi.mock("../../src/approval/approval-resolver", () => ({ + resolveApproval: vi.fn(), +})); +vi.mock("../../src/send-service", () => ({ + sendProactiveTextOrMarkdown: vi.fn().mockResolvedValue({ ok: true }), +})); +vi.mock("../../src/config", () => ({ + getConfig: vi.fn(() => ({ clientId: "x" })), +})); + +const { resolveApproval } = await import("../../src/approval/approval-resolver"); +const { sendProactiveTextOrMarkdown } = await import("../../src/send-service"); +const mockResolve = resolveApproval as ReturnType; +const mockSend = sendProactiveTextOrMarkdown as ReturnType; + +const base = { cfg: {} as never, accountId: "default", senderId: "staffA", log: undefined as never }; + +describe("tryInterceptApproveCommand", () => { + beforeEach(() => { mockResolve.mockReset(); mockSend.mockReset(); mockSend.mockResolvedValue({ ok: true }); }); + + it("非 /approve 命令返 false", async () => { + expect(await tryInterceptApproveCommand({ ...base, text: "hello world" })).toBe(false); + expect(mockResolve).not.toHaveBeenCalled(); + }); + + it("malformed /approve 返 true 并私聊提示(forceMarkdown)", async () => { + expect(await tryInterceptApproveCommand({ ...base, text: "/approve abc xyz" })).toBe(true); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), `user:${base.senderId}`, + expect.stringContaining("格式错误"), + expect.objectContaining({ forceMarkdown: true }), + ); + expect(mockResolve).not.toHaveBeenCalled(); + }); + + it("正常命令 → 调 resolver → ok=true 不私聊", async () => { + mockResolve.mockResolvedValue({ ok: true }); + expect(await tryInterceptApproveCommand({ ...base, text: "/approve abc allow-once" })).toBe(true); + expect(mockResolve).toHaveBeenCalledWith(expect.objectContaining({ + approvalId: "abc", decision: "allow-once", senderId: "staffA", + })); + expect(mockSend).not.toHaveBeenCalled(); + }); + + it("unauthorized → 私聊拒绝(含 approval id + forceMarkdown)", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "unauthorized" }); + await tryInterceptApproveCommand({ ...base, text: "/approve abc deny" }); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:staffA", + expect.stringMatching(/无权.*abc/), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("invalid-decision 含 allowedDecisions → 私聊带 allowed 列表", async () => { + mockResolve.mockResolvedValue({ + ok: false, reason: "invalid-decision", + allowedDecisions: ["allow-once", "deny"], + }); + await tryInterceptApproveCommand({ ...base, text: "/approve abc allow-always" }); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:staffA", + expect.stringMatching(/不支持.*allow-once.*deny/), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("invalid-decision 无 allowedDecisions → 私聊默认提示", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "invalid-decision" }); + await tryInterceptApproveCommand({ ...base, text: "/approve abc allow-always" }); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:staffA", + expect.stringContaining("允许一次或拒绝"), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("not-found 私聊轻提示", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "not-found" }); + await tryInterceptApproveCommand({ ...base, text: "/approve abc deny" }); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:staffA", + expect.stringContaining("已处理或已过期"), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("already-resolved 私聊轻提示", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "already-resolved" }); + await tryInterceptApproveCommand({ ...base, text: "/approve abc deny" }); + expect(mockSend).toHaveBeenCalledWith(expect.anything(), "user:staffA", + expect.stringContaining("已处理或已过期"), + expect.objectContaining({ forceMarkdown: true })); + }); + + it("gateway-error → 私聊提示稍后重试", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "gateway-error" }); + await tryInterceptApproveCommand({ ...base, text: "/approve abc deny" }); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:staffA", + expect.stringMatching(/暂时处理失败.*稍后重试/), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("send 失败不抛", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "unauthorized" }); + mockSend.mockRejectedValueOnce(new Error("net")); + await expect(tryInterceptApproveCommand({ ...base, text: "/approve abc deny" })).resolves.toBe(true); + }); +}); +``` + +- [ ] **Step 9.2: 跑测试确认 fail** + +Run: `pnpm vitest run tests/unit/approval-command-intercept.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 9.3: 实现 src/approval/approval-command-intercept.ts** + +```typescript +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import type { Logger } from "../types"; +import { getConfig } from "../config"; +import { sendProactiveTextOrMarkdown } from "../send-service"; +import { parseApproveCommand } from "./approval-command-parser"; +import { resolveApproval } from "./approval-resolver"; + +export interface InterceptInput { + cfg: OpenClawConfig; + accountId: string; + text: string; + senderId: string; + log?: Logger; +} + +const sendDm = async ( + cfg: OpenClawConfig, accountId: string, senderId: string, + text: string, log?: Logger, +): Promise => { + await sendProactiveTextOrMarkdown( + getConfig(cfg, accountId), + `user:${senderId}`, + text, + { forceMarkdown: true, accountId, log }, + ).catch(() => undefined); +}; + +export async function tryInterceptApproveCommand(input: InterceptInput): Promise { + const trimmed = input.text.trim(); + if (!/^\/?approve(?:\s|$)/i.test(trimmed)) return false; + + const parsed = parseApproveCommand(trimmed); + if (!parsed) { + await sendDm( + input.cfg, input.accountId, input.senderId, + "⚠️ /approve 命令格式错误。用法:`/approve `", + input.log, + ); + input.log?.warn?.("[DingTalk][Approval] /approve malformed"); + return true; + } + + const result = await resolveApproval({ + cfg: input.cfg, accountId: input.accountId, + approvalId: parsed.approvalId, decision: parsed.decision, + senderId: input.senderId, log: input.log, + }); + + if (result.ok) { + input.log?.info?.(`[DingTalk][Approval] /approve resolved approvalId=${parsed.approvalId} decision=${parsed.decision}`); + return true; + } + + switch (result.reason) { + case "unauthorized": + await sendDm(input.cfg, input.accountId, input.senderId, + `⛔ 你不在 approver 名单,无权批准此请求(${parsed.approvalId})`, input.log); + break; + case "invalid-decision": { + const hint = result.allowedDecisions?.length + ? `请选择:${result.allowedDecisions.join(" / ")}` + : "请选择允许一次或拒绝"; + await sendDm(input.cfg, input.accountId, input.senderId, + `ℹ️ 该审批不支持 ${parsed.decision}。${hint}(${parsed.approvalId})`, input.log); + break; + } + case "not-found": + case "already-resolved": + await sendDm(input.cfg, input.accountId, input.senderId, + `ℹ️ 审批 ${parsed.approvalId} 已处理或已过期,无需再次操作。`, input.log); + break; + case "gateway-error": + await sendDm(input.cfg, input.accountId, input.senderId, + `ℹ️ 审批 ${parsed.approvalId} 暂时处理失败,请稍后重试。`, input.log); + break; + } + input.log?.info?.(`[DingTalk][Approval] /approve resolver returned ${result.reason}`); + return true; +} +``` + +- [ ] **Step 9.4: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/approval-command-intercept.test.ts` +Expected: 10 PASS。 + +- [ ] **Step 9.5: Commit** + +```bash +git add src/approval/approval-command-intercept.ts tests/unit/approval-command-intercept.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 /approve 命令 early intercept 入口 + +调 parser + approval-resolver;按 5 reason 分支私聊提示。所有 +sendProactiveTextOrMarkdown 调用强制 forceMarkdown:true 避免 messageType=card +配置下被发成卡片(src/send-service.ts:371-393)。gateway-error 也私聊提示稍后重试,避免用户手敲命令后无反馈。 + +EOF +)" +``` + +--- + +## Task 10 · approval-capability.ts(PR-1:装配工厂,不挂 nativeRuntime) + +**Files:** +- Create: `src/approval/approval-capability.ts` +- Test: `tests/unit/approval-capability.test.ts` + +> PR-2 的 Task 19 会把 `nativeRuntime` 挂上。PR-1 先把工厂装配 + describeExecApprovalSetup 文案做好,channel 就能挂上 capability 触发 authorize 路径。 + +> **关于 `resolveApproveCommandBehavior` capability hook(有意不实现):** 上游 `openclaw/src/auto-reply/reply/commands-approve.ts:148-158` 允许 channel capability 通过 `resolveApproveCommandBehavior` 接管命令处理。DingTalk 端**不**实现此 hook —— `/approve` 命令走 channel 自有的 inbound early intercept(Task 11 / D2 / §6.8),目的是绕过 OpenClaw 命令派发链路上的 session lock 死锁。Task 10 故意不传 `resolveApproveCommandBehavior` 字段;如 PR review 提及 telegram peer 有这个回调,回复"DingTalk 走 D2 early intercept 路径,不复用上游 commands-approve 注册系统"。 + +- [ ] **Step 10.1: 写失败测试** + +```typescript +import { describe, it, expect, vi } from "vitest"; + +// mock 必须对齐 impl 的 import subpath(approval-capability.ts 从 +// openclaw/plugin-sdk/approval-delivery-runtime import 工厂;不是根 entry) +vi.mock("openclaw/plugin-sdk/approval-delivery-runtime", () => ({ + createApproverRestrictedNativeApprovalCapability: vi.fn(() => ({ mock: "capability" })), +})); + +const { createDingTalkApprovalCapability } = await import("../../src/approval/approval-capability"); +const sdk = await import("openclaw/plugin-sdk/approval-delivery-runtime"); +const factory = sdk.createApproverRestrictedNativeApprovalCapability as ReturnType; + +describe("createDingTalkApprovalCapability", () => { + it("装配工厂参数 channel='dingtalk' channelLabel='DingTalk' eventKinds=[exec,plugin]", () => { + createDingTalkApprovalCapability(); + expect(factory).toHaveBeenCalledWith(expect.objectContaining({ + channel: "dingtalk", + channelLabel: "DingTalk", + eventKinds: ["exec", "plugin"], + })); + }); + + it("notifyOriginWhenDmOnly=false(v1 无 DM 路径)", () => { + createDingTalkApprovalCapability(); + expect(factory).toHaveBeenCalledWith(expect.objectContaining({ notifyOriginWhenDmOnly: false })); + }); + + it("requireMatchingTurnSourceChannel=true(v1 origin-only)", () => { + createDingTalkApprovalCapability(); + expect(factory).toHaveBeenCalledWith(expect.objectContaining({ requireMatchingTurnSourceChannel: true })); + }); + + it("resolveApproverDmTargets 未传(v1 不实现)", () => { + createDingTalkApprovalCapability(); + const args = factory.mock.calls[0][0]; + expect(args.resolveApproverDmTargets).toBeUndefined(); + }); + + it("nativeRuntime 在 PR-1 未传(PR-2 补)", () => { + createDingTalkApprovalCapability(); + const args = factory.mock.calls[0][0]; + expect(args.nativeRuntime).toBeUndefined(); + }); + + it("describeExecApprovalSetup 返回中文配置指南字符串(含 approvers + commands.ownerAllowFrom + enabled)", () => { + createDingTalkApprovalCapability(); + const args = factory.mock.calls[0][0]; + const text = args.describeExecApprovalSetup({ cfg: {}, accountId: "default" }); + expect(text).toMatch(/channels\.dingtalk\.execApprovals\.approvers/); + expect(text).toMatch(/commands\.ownerAllowFrom/); + expect(text).toMatch(/enabled/); + }); +}); +``` + +- [ ] **Step 10.2: 跑测试确认 fail** + +Run: `pnpm vitest run tests/unit/approval-capability.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 10.3: 实现 src/approval/approval-capability.ts** + +```typescript +import { createApproverRestrictedNativeApprovalCapability } from "openclaw/plugin-sdk/approval-delivery-runtime"; +import type { ChannelApprovalCapability } from "openclaw/plugin-sdk/channel-contract"; +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import { + getExecApprovalsConfig, + listExecApprovers, + isExecAuthorizedSender, + isPluginAuthorizedSender, + resolveNativeDeliveryMode, +} from "./approval-config"; +import { resolveDingTalkOriginTarget } from "./approval-target-resolver"; + +const DESCRIBE_TEMPLATE = + "Configure channels.dingtalk.execApprovals.approvers or commands.ownerAllowFrom; " + + "leave channels.dingtalk.execApprovals.enabled unset/auto or set it to true."; + +export function createDingTalkApprovalCapability(): ChannelApprovalCapability { + return createApproverRestrictedNativeApprovalCapability({ + channel: "dingtalk", + channelLabel: "DingTalk", + listAccountIds: (cfg) => { + const accounts = cfg.channels?.dingtalk?.accounts ?? {}; + const ids = Object.keys(accounts); + return ids.length > 0 ? ids : ["default"]; + }, + hasApprovers: ({ cfg, accountId }) => + listExecApprovers({ cfg, accountId }).length > 0, + isExecAuthorizedSender, + isPluginAuthorizedSender, + isNativeDeliveryEnabled: (q) => getExecApprovalsConfig(q).isNativeDeliveryEnabled, + resolveNativeDeliveryMode, + requireMatchingTurnSourceChannel: true, + // 直接把 Task 5 上游 helper 装好的 resolver 传进去;接受 ApprovalResolverParams + // ({ cfg, accountId, request }) → target | null;内部走 input.request.request.* payload + resolveOriginTarget: resolveDingTalkOriginTarget, + // resolveApproverDmTargets: v1 不实现 + notifyOriginWhenDmOnly: false, + // nativeRuntime: PR-2 接上 + describeExecApprovalSetup: (_q: { cfg: OpenClawConfig; accountId: string }) => DESCRIBE_TEMPLATE, + eventKinds: ["exec", "plugin"], + }); +} +``` + +- [ ] **Step 10.4: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/approval-capability.test.ts` +Expected: 6 PASS。 + +- [ ] **Step 10.5: Commit** + +```bash +git add src/approval/approval-capability.ts tests/unit/approval-capability.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 装配 ChannelApprovalCapability(PR-1:无 nativeRuntime) + +createApproverRestrictedNativeApprovalCapability 工厂调用,16 参数中 v1 +需要的全部到位(resolveApproverDmTargets / nativeRuntime / 等 v1 不需要的 +留空,PR-2 再补 nativeRuntime)。describeExecApprovalSetup 文案与上游 +Slack/Telegram/Discord 三家完全对齐。 + +EOF +)" +``` + +--- + +## Task 11 · channel.ts 挂 approvalCapability + inbound-handler /approve early intercept + +**Files:** +- Modify: `src/channel.ts:22-127` +- Modify: `src/inbound-handler.ts:~770` +- Test: `tests/unit/inbound-handler-approve-intercept.test.ts` + +- [ ] **Step 11.1: 在 src/channel.ts 挂 approvalCapability** + +文件顶部 import: + +```typescript +import { createDingTalkApprovalCapability } from "./approval/approval-capability"; +``` + +在 `dingtalkPlugin` 对象字面量内(与现有 `messaging` / `directory` / `gateway` 等字段同级)加: + +```typescript +approvalCapability: createDingTalkApprovalCapability(), +``` + +> 工厂在 module load 时调一次返回单例 capability,与 telegram/slack peer 一致;后续 PR-2 把 `nativeRuntime` 挂上后无需再改 channel.ts。 + +- [ ] **Step 11.2: 写 inbound-handler intercept 失败测试** + +新建 `tests/unit/inbound-handler-approve-intercept.test.ts`: + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("../../src/approval/approval-command-intercept", () => ({ + tryInterceptApproveCommand: vi.fn(), +})); +const { tryInterceptApproveCommand } = await import("../../src/approval/approval-command-intercept"); +const mockIntercept = tryInterceptApproveCommand as ReturnType; + +// 提示:本测试的设计是「在 inbound-handler 真实路径里 mock intercept,断言它被调」。 +// 实现 step 11.4 前需要先看 src/inbound-handler.ts 现有测试套件的 mock 套路; +// 推荐复用同名 setup helper(如已有 `tests/unit/fixtures/inbound-handler-fixtures.ts`)。 + +import { handleDingTalkMessage } from "../../src/inbound-handler"; +// 以下伪代码:实施时按现有测试 fixture 重写 setupMessage +declare function setupMessage(opts: { isGroup: boolean; text: string; senderStaffId: string }): { + invoke: () => Promise; + expectReplyNotDispatched: () => void; + expectSessionLockNotAcquired: () => void; +}; + +describe("inbound-handler · /approve early intercept", () => { + beforeEach(() => { mockIntercept.mockReset(); mockIntercept.mockResolvedValue(false); }); + + it("非 /approve 消息走正常路径,不调 intercept", async () => { + const m = setupMessage({ isGroup: false, text: "hello bot", senderStaffId: "staffA" }); + await m.invoke(); + expect(mockIntercept).not.toHaveBeenCalled(); + }); + + it("私聊 /approve 命令调 intercept", async () => { + mockIntercept.mockResolvedValue(true); + const m = setupMessage({ isGroup: false, text: "/approve abc deny", senderStaffId: "staffA" }); + await m.invoke(); + expect(mockIntercept).toHaveBeenCalledWith(expect.objectContaining({ + text: "/approve abc deny", senderId: "staffA", + })); + m.expectReplyNotDispatched(); + m.expectSessionLockNotAcquired(); + }); + + it("群里 @bot /approve 命令:剥前导 @mention 后传给 intercept", async () => { + mockIntercept.mockResolvedValue(true); + const m = setupMessage({ isGroup: true, text: "@OpenClaw /approve abc once", senderStaffId: "staffA" }); + await m.invoke(); + expect(mockIntercept).toHaveBeenCalledWith(expect.objectContaining({ + text: expect.stringMatching(/^\/approve(?:\s|$)/), + })); + }); + + it("intercept 返 true → 不进 reply 派发", async () => { + mockIntercept.mockResolvedValue(true); + const m = setupMessage({ isGroup: false, text: "/approve abc once", senderStaffId: "staffA" }); + await m.invoke(); + m.expectReplyNotDispatched(); + }); + + it("intercept 返 false → 走正常 inbound pipeline", async () => { + mockIntercept.mockResolvedValue(false); + const m = setupMessage({ isGroup: false, text: "approve maybe wrong format", senderStaffId: "staffA" }); + await m.invoke(); + // 正常 pipeline 继续,不在此断言;具体行为由其它已有测试覆盖 + }); + + it("接受裸 'approve abc once'(无前导 /)", async () => { + mockIntercept.mockResolvedValue(true); + const m = setupMessage({ isGroup: false, text: "approve abc once", senderStaffId: "staffA" }); + await m.invoke(); + expect(mockIntercept).toHaveBeenCalled(); + }); +}); +``` + +> ⚠️ 实施时第一步先 grep `tests/unit/inbound-handler*` 找现有 fixture/setup 模式(参 `tests/unit/fixtures/` 或既有大测试套件),按那个模式重写 `setupMessage`。本伪代码仅描述断言意图。 + +- [ ] **Step 11.3: 跑测试确认 fail** + +Run: `pnpm vitest run tests/unit/inbound-handler-approve-intercept.test.ts` +Expected: FAIL(intercept 没插入到 handler)。 + +- [ ] **Step 11.4: 在 src/inbound-handler.ts 插入 intercept 块** + +精确插入位置:`L770` sessionPeer 解析之后、`L780` routing 解析之前(参 spec §6.8 + §3.3 接触面表)。 + +必须满足 4 个前后约束: +- ✓ 晚于 `L575`(extractedContent ready) +- ✓ 晚于 `L671/729`(DM/Group 授权通过) +- ✓ **早于 `L817`**(sub-agent routing 分支,否则 `@agent /approve` 被吞) +- ✓ 早于 `L874`(handleInboundCommandDispatch) +- ✓ 早于 `L2053`(acquireSessionLock,否则 plugin waitDecision 死锁) + +**先在文件顶部 import** 区域加静态 import(与既有 approval/* 同级 module,无循环依赖风险): + +```typescript +import { tryInterceptApproveCommand } from "./approval/approval-command-intercept"; +``` + +然后插入 block: + +```typescript +// ---- Early /approve bypass:early intercept 绕过 session lock 死锁(D2,§6.8) +{ + const rawApproveText = !isDirect + ? extractedContent.text.replace(/^(?:@\S+\s+)*/u, "").trim() + : extractedContent.text.trim(); + if (/^\/?approve(?:\s|$)/i.test(rawApproveText)) { + const intercepted = await tryInterceptApproveCommand({ + cfg, accountId: account.accountId, + text: rawApproveText, senderId, log, + }); + if (intercepted) return; // 不进 reply 派发 + } +} +``` + +> 变量名 `isDirect` / `extractedContent` / `cfg` / `account` / `senderId` / `log` 都来自既有 handler scope;插入时请按当前 main 实际命名调整(用 `Read` 现场核对 `src/inbound-handler.ts:L760-L820` 周边)。 +> +> **不要用 `await import(...)` 动态加载** —— `src/approval/approval-command-intercept.ts` 不会反向依赖 `src/inbound-handler.ts`(intercept 只引 parser / resolver / send-service / config,不引 inbound-handler),无循环依赖。静态 import 更利于 tree-shaking 与 lint 检查。 + +- [ ] **Step 11.5: 跑测试确认 pass** + +Run: `pnpm vitest run tests/unit/inbound-handler-approve-intercept.test.ts` +Expected: 6 PASS。 + +- [ ] **Step 11.6: 跑全部 inbound 相关测试无回归** + +Run: `pnpm vitest run tests/unit/inbound-handler` +Expected: 全部 PASS(既有 dedup / self-filter / routing / dispatch 行为不变)。 + +- [ ] **Step 11.7: type-check + lint + 全套测试** + +Run: `pnpm run type-check && pnpm run lint && pnpm test` +Expected: 0 错误,全部 PASS。 + +- [ ] **Step 11.8: Commit** + +```bash +git add src/channel.ts src/inbound-handler.ts tests/unit/inbound-handler-approve-intercept.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): channel 挂 approvalCapability + inbound /approve early intercept + +(1) src/channel.ts plugin 对象加 approvalCapability 字段(PR-2 再接 nativeRuntime)。 +(2) src/inbound-handler.ts 在 sessionPeer 解析之后、routing 决策之前插入 + /approve early intercept:剥群里前导 @mention,regex /^\\/?approve(?:\\s|$)/i + 对齐上游 commands-approve.ts:16;命中后调 tryInterceptApproveCommand 并 + return,避免进 reply 派发触发 session lock 死锁(D2 / §6.8)。 + +EOF +)" +``` + +--- + +## PR-1 收尾 + +- [ ] **Step PR1.1: 跑完整测试套件 + coverage** + +```bash +pnpm test +pnpm test:coverage +``` + +Expected: 所有 unit test PASS;`src/approval/*` line coverage ≥ 90%,branch ≥ 85%。 + +- [ ] **Step PR1.2: 真机抽检(PR-1 范围)** + +仅验证 `/approve` 命令通道生效:在 OpenClaw WebUI/CLI 跑一个需要 approval 的 task,从日志拿 approval id,到钉钉里发 `/approve allow-once` —— 验证: +- approver 名单内用户:上游 store 收到 resolve(OpenClaw 端可见 resolved) +- 非 approver:收到 `⛔ 你不在 approver 名单` 私聊 +- 不会触发 session lock 死锁(120s 等待) + +可选:在 `skills/dingtalk-real-device-testing/SKILL.md` 列出的 checklist 内勾选 PR-1 范围条目。 + +- [ ] **Step PR1.3: 开 PR** + +```bash +git push -u origin docs/gap-01-approval-native-design +gh pr create --title "feat(approval): PR-1 ChannelApprovalCapability + /approve early intercept" --body "$(cat <<'EOF' +## Summary +- 装配 ChannelApprovalCapability 工厂(D7 approver schema + D20 单点 resolver + D21 kind 推导 + invalid-decision 分类) +- /approve 命令早期 intercept(D2)绕过 session lock 死锁;对齐上游 10 alias × 2 顺序 = 20 合法形式 +- approval-card-locator(D22)就位,PR-2 加按钮路径时 0 新增 routing 逻辑 + +## PR boundaries(参 docs/plans/2026-05-19-gap-01-approval-native.md §PR-1) +- 不含 native runtime 4 子 adapter 实现(PR-2) +- 不含模板 ID 替换(PR-2) +- 不含按钮回调路径(PR-2) + +## Test plan +- [x] `pnpm test` 全部 PASS(新增 ~100 case,src/approval 覆盖 ≥ 90%) +- [x] 真机:/approve 命令通道 + 非 approver 拒绝 + session lock 不触发 + +BREAKING CHANGE: peerDependencies.openclaw bump >= 2026.4.7 + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +提示用户做 PR review;PR review 通过 + merge 后再启动 PR-2。 + + +--- + +# PR-2 · 完整 Native Runtime + 模板替换 + 真机回归 + +**交付目标:** 完整 v3.3 双路由 UX(card 路径在原 agent reply card 上挂按钮;markdown 路径发独立消息含 `/approve` 模板);按钮回调通路接通;真机回归 PASS。 + +**PR-2 任务清单:** Task 12 ~ Task 22。 + +--- + +## Task 11b · 抽 `approval-card-state.ts`(cardParamMap 字段集 single source) + +**Files:** +- Create: `src/approval/approval-card-state.ts` +- Test: `tests/unit/approval-card-state.test.ts` + +**动机:** v3 模板的 3 个 cardParamMap 变量(`show_approve_btns` / `approveId` / `hasAction`)以及 pending / cleared 字段值会被多处引用:`card-service.ts` 的 createAICard 默认值、`approval-card-patcher.ts` 三 patcher、`approval-callback-handler.ts` clearing path、`tests/unit/*` 断言。如果分散写就会出现"模板加字段时漏改"或"字段名拼错没人发现"。集中到一个模块 + 一组常量后,模板字段名改动只在一处发生,所有 caller 自动跟随。 + +> 这是 PR-1 ↔ PR-2 之间的桥梁 Task:让 PR-2 的 patcher / card-service 修订都基于这一组工具,而不是各自字面量写。同 spec §1.X 单一事实表 1:1 对齐。 + +- [ ] **Step 11b.1: 写失败测试** + +```typescript +import { describe, it, expect } from "vitest"; +import { + APPROVAL_CARD_KEYS, + buildApprovalPendingCardParams, + buildApprovalClearedCardParams, + type ApprovalCardParams, +} from "../../src/approval/approval-card-state"; + +describe("approval-card-state · APPROVAL_CARD_KEYS 常量", () => { + it("固化三个 key 名(模板字段名 single source)", () => { + expect(APPROVAL_CARD_KEYS).toEqual({ + showApproveBtns: "show_approve_btns", + approveId: "approveId", + hasAction: "hasAction", + }); + }); +}); + +describe("buildApprovalPendingCardParams", () => { + it("PUT pending:show_approve_btns='true' + hasAction='false' + approveId=", () => { + expect(buildApprovalPendingCardParams("abc123")).toEqual({ + show_approve_btns: "true", + hasAction: "false", + approveId: "abc123", + }); + }); +}); + +describe("buildApprovalClearedCardParams", () => { + it("cardStillActive=true → hasAction='true'(恢复 stop)", () => { + expect(buildApprovalClearedCardParams(true)).toEqual({ + show_approve_btns: "false", + approveId: "", + hasAction: "true", + }); + }); + it("cardStillActive=false → hasAction='false'", () => { + expect(buildApprovalClearedCardParams(false)).toEqual({ + show_approve_btns: "false", + approveId: "", + hasAction: "false", + }); + }); + it("不写终态文字(v1 schema 无字段位,§7.1)", () => { + const params = buildApprovalClearedCardParams(true); + expect(params).not.toHaveProperty("status"); + expect(params).not.toHaveProperty("statusFooter"); + expect(params).not.toHaveProperty("approval_status"); + }); +}); + +describe("createAICard 默认值的常量 export", () => { + it("APPROVAL_CARD_INITIAL 提供 createAICard cardParamMap 用的初始值(show_approve_btns:false + approveId:'')", async () => { + const { APPROVAL_CARD_INITIAL } = await import("../../src/approval/approval-card-state"); + expect(APPROVAL_CARD_INITIAL).toEqual({ + show_approve_btns: "false", + approveId: "", + }); + }); +}); +``` + +- [ ] **Step 11b.2: 跑确认 fail** + +Run: `pnpm vitest run tests/unit/approval-card-state.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 11b.3: 实现 src/approval/approval-card-state.ts** + +```typescript +/** + * v3 卡片模板 approval 相关 cardParamMap 字段集 + 状态转换 helper。 + * 所有 approval 业务模块(patcher / card-service 默认值 / callback handler 清理) + * 都通过本模块拿字段名与值,避免字面量散落。spec §1.X 单一事实表的代码化身。 + */ + +export const APPROVAL_CARD_KEYS = { + /** 控制 approve_btns 按钮组可见性 */ + showApproveBtns: "show_approve_btns", + /** approval id 主链路载体(绑定到三按钮 params) */ + approveId: "approveId", + /** 控制 btn_stop(既有 AI Card v2 字段,与 D23 共存策略) */ + hasAction: "hasAction", +} as const; + +export type ApprovalCardParams = { + [APPROVAL_CARD_KEYS.showApproveBtns]: "true" | "false"; + [APPROVAL_CARD_KEYS.approveId]: string; + [APPROVAL_CARD_KEYS.hasAction]: "true" | "false"; +}; + +/** card-service.createAICard / finalize / stop 路径的初始默认值(仅 2 字段,hasAction 由既有逻辑驱动) */ +export const APPROVAL_CARD_INITIAL: { show_approve_btns: "false"; approveId: "" } = { + show_approve_btns: "false", + approveId: "", +}; + +/** pending 状态 → 显示三按钮 + 隐藏 stop + 注入 approvalId */ +export function buildApprovalPendingCardParams(approvalId: string): ApprovalCardParams { + return { + show_approve_btns: "true", + hasAction: "false", + approveId: approvalId, + }; +} + +/** resolved / expired 状态 → 隐藏三按钮 + 清 approvalId + hasAction 按 cardStillActive 恢复 stop */ +export function buildApprovalClearedCardParams(cardStillActive: boolean): ApprovalCardParams { + return { + show_approve_btns: "false", + approveId: "", + hasAction: cardStillActive ? "true" : "false", + }; +} +``` + +- [ ] **Step 11b.4: 跑确认 pass** + +Run: `pnpm vitest run tests/unit/approval-card-state.test.ts` +Expected: 6 PASS。 + +- [ ] **Step 11b.5: Commit** + +```bash +git add src/approval/approval-card-state.ts tests/unit/approval-card-state.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 approval-card-state(cardParamMap 字段集 single source) + +集中 APPROVAL_CARD_KEYS 常量 + APPROVAL_CARD_INITIAL(createAICard 默认值) ++ buildApprovalPendingCardParams + buildApprovalClearedCardParams 两个 builder, +作为 spec §1.X 单一事实表的代码化身。后续 patcher / card-service / callback +handler 全部从本模块拿字段名与值,避免字面量散落 / 模板字段改名漏改。 +EOF +)" +``` + +> **下文 Task 12 / 15 / 17 都改用本模块的常量 + builder**——不再写字面量 `"show_approve_btns": "true"` 等。 + +--- + +## Task 12 · v3 模板 ID 替换 + createAICard cardParamMap 默认值修正 + +**Files:** +- Modify: `src/card/card-template.ts:6` +- Modify: `src/card-service.ts`(createAICard、finalize、stop 三处 cardParamMap) +- Test: `tests/unit/card-service.test.ts`(扩既有) + +- [ ] **Step 12.1: 写 createAICard cardParamMap 默认值失败测试** + +在 `tests/unit/card-service.test.ts` 加: + +```typescript +import { describe, it, expect, vi } from "vitest"; +// 沿用既有 test 文件的 mock 套路(http-client / auth / card-callback-service) + +describe("createAICard · approval cardParamMap defaults(D24 v3.6 / Task 12)", () => { + it("createAICard cardParamMap 默认包含 show_approve_btns:'false' + approveId:''", async () => { + // 沿用既有 createAICard test 的 setup,断言 cardParamMap 包含这两个 KV + // mock createAndDeliver 拿到调用参数 + const params = await captureCreateAndDeliverParams(/* 既有 helper */); + expect(params.cardParamMap).toEqual(expect.objectContaining({ + show_approve_btns: "false", + approveId: "", + })); + }); + + it("finalize 路径 PUT cardParamMap 包含 show_approve_btns:'false' + approveId:''", async () => { + // 沿用既有 finalize test setup + const params = await captureFinalizePutParams(); + expect(params).toEqual(expect.objectContaining({ + show_approve_btns: "false", + approveId: "", + })); + }); +}); +``` + +> 伪代码:`captureCreateAndDeliverParams` / `captureFinalizePutParams` 按既有 `tests/unit/card-service*.test.ts` 内的 mock 模式写。 + +- [ ] **Step 12.2: 跑确认 fail** + +Run: `pnpm vitest run tests/unit/card-service.test.ts -t "approval cardParamMap defaults"` +Expected: FAIL(字段不存在)。 + +- [ ] **Step 12.3: 替换 v3 模板 ID** + +修改 `src/card/card-template.ts:6-7`: + +```typescript +export const BUILTIN_DINGTALK_CARD_TEMPLATE_ID = + process.env.DINGTALK_CARD_TEMPLATE_ID || "58f73932-fc3b-46ae-8e90-93313e405061.schema"; +``` + +- [ ] **Step 12.4: 在 src/card-service.ts createAICard 加 cardParamMap 默认值** + +文件头加 import: + +```typescript +import { APPROVAL_CARD_INITIAL } from "./approval/approval-card-state"; +``` + +定位 `src/card-service.ts:802` 附近 `createAICard` 内 cardParamMap 字面量。用 spread 注入: + +```typescript +cardParamMap: { + // ... 既有字段如 hasAction:"true" + ...APPROVAL_CARD_INITIAL, // show_approve_btns:"false" + approveId:"" — 见 approval-card-state.ts +}, +``` + +finalize(约 `src/card-service.ts:711-785`)与 stop / 错误兜底路径:在 PUT updateCardVariables 的 params 对象同样 `...APPROVAL_CARD_INITIAL` spread。 + +> 用常量 import 而不是写字面量——字段名要改时只改 `approval-card-state.ts` 一处(issue 3 设计目标)。 + +- [ ] **Step 12.5: 跑确认 pass** + +Run: `pnpm vitest run tests/unit/card-service.test.ts -t "approval cardParamMap defaults"` +Expected: PASS。 + +- [ ] **Step 12.6: 跑全部 card-service 测试无回归** + +Run: `pnpm vitest run tests/unit/card-service` +Expected: 全部 PASS(既有 streaming / finalize / stop 行为不变)。 + +- [ ] **Step 12.7: 构建 runtime + 真机抽检** + +```bash +pnpm run build:runtime +openclaw gateway restart # 用户手动跑 +``` + +在钉钉里发一条非 approval 消息,验证 agent reply card 渲染正常 + **不显示 approval 按钮**(show_approve_btns=false 生效)+ btn_stop 正常显示。 + +> ⚠️ 这一步不可跳过 —— 如果模板 ID 错或者 cardParamMap 默认值缺失,每条 agent reply 都会显示 3 个未绑 approval 的按钮。 + +- [ ] **Step 12.8: Commit** + +```bash +git add src/card/card-template.ts src/card-service.ts tests/unit/card-service.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 替换 AI Card 模板为 v3 + 补 cardParamMap 默认值 + +(1) src/card/card-template.ts BUILTIN_DINGTALK_CARD_TEMPLATE_ID + 675cde2f-...8b77 (v2) → 58f73932-...05061 (v3, 含 approve_btns/ + show_approve_btns/approveId 三变量)。env DINGTALK_CARD_TEMPLATE_ID + 覆盖能力保留用于开发期测试。 +(2) src/card-service.ts createAICard / finalize / stop / 错误兜底路径 + cardParamMap 显式补 show_approve_btns:"false" + approveId:"",避免 + v3 模板默认值导致 agent reply 一上线就显示未绑 approval 的按钮。 + +真机抽检确认:(a) v3 模板下既有 AI Card 流式行为不变;(b) 非 approval +消息卡片底部不出现 approval 按钮组。 + +EOF +)" +``` + +--- + +## Task 13 · card-callback-service 扩 cardPrivateData 字段(D16 BLOCKER) + +**Files:** +- Modify: `src/card-callback-service.ts:6` 起(接口)+ `src/card-callback-service.ts:94-168`(analyzeCardCallback) +- Test: `tests/unit/card-callback-service.test.ts`(扩既有) + +> 改动量 ~5 行:interface 加字段 + 函数末尾把已解析的 cardPrivateData 拷到返回值。analyzeCardCallback 内部 L100-110 已在解析三层 embedded JSON,只是没附到返回值。 + +- [ ] **Step 13.1: 写失败测试** + +在 `tests/unit/card-callback-service.test.ts` 加: + +```typescript +describe("analyzeCardCallback · cardPrivateData 提取(D16)", () => { + it("payload 嵌套 cardPrivateData 含 actionIds + params → analysis.cardPrivateData", () => { + const payload = { + // 沿用既有 test fixture 的 embedded JSON 嵌套形态 + content: JSON.stringify({ + cardPrivateData: { + actionIds: ["allow-once"], + params: { action: "allow-once", approveId: "abc123" }, + }, + }), + userId: "staffA", + outTrackId: "ai_card_xxx", + }; + const result = analyzeCardCallback(payload); + expect(result.cardPrivateData).toEqual({ + actionIds: ["allow-once"], + params: { action: "allow-once", approveId: "abc123" }, + }); + }); + + it("payload 无 cardPrivateData → analysis.cardPrivateData 为 undefined", () => { + const result = analyzeCardCallback({ content: "{}", userId: "u", outTrackId: "o" }); + expect(result.cardPrivateData).toBeUndefined(); + }); + + it("既有 actionId 抽取行为不变(兼容回归)", () => { + const payload = { + content: JSON.stringify({ cardPrivateData: { actionIds: ["feedback_up"] } }), + userId: "u", outTrackId: "o", + }; + const result = analyzeCardCallback(payload); + expect(result.actionId).toBe("feedback_up"); + }); +}); +``` + +- [ ] **Step 13.2: 跑确认 fail** + +Run: `pnpm vitest run tests/unit/card-callback-service.test.ts -t cardPrivateData` +Expected: FAIL(字段未暴露)。 + +- [ ] **Step 13.3: 修改 CardCallbackAnalysis 接口** + +`src/card-callback-service.ts:6` 起的 interface 加: + +```typescript +export interface CardCallbackAnalysis { + // ... 既有字段 + cardPrivateData?: { + actionIds?: string[]; + params?: Record; + }; +} +``` + +- [ ] **Step 13.4: 在 analyzeCardCallback 内附加 cardPrivateData** + +定位 `src/card-callback-service.ts:94-168` 中已经把 `cardPrivateData` 解析出的局部变量(参 spec:L100-110 已在解析三层 embedded JSON)。函数 return 之前把该对象附到返回值: + +```typescript +return { + // ... 既有字段 + cardPrivateData: extractedCardPrivateData ?? undefined, +}; +``` + +> 现场用 Read 核对变量名 —— spec 内 §3.3 描述的是「内部已经解析」,实现细节看实际代码。 + +- [ ] **Step 13.5: 跑确认 pass** + +Run: `pnpm vitest run tests/unit/card-callback-service.test.ts` +Expected: 全部 PASS(既有 case + 3 新 case)。 + +- [ ] **Step 13.6: Commit** + +```bash +git add src/card-callback-service.ts tests/unit/card-callback-service.test.ts +git commit -m "$(cat <<'EOF' +feat(card-callback): CardCallbackAnalysis 暴露 cardPrivateData 字段(D16) + +analyzeCardCallback 内部已解析 cardPrivateData(actionIds + params), +仅需附到返回 analysis 上。为 Gap #01 approval callback handler 提供 +params.action / params.approveId 解码主链路。 + +既有 actionId 抽取行为完全不变。 + +EOF +)" +``` + +--- + +## Task 14 · card-run-registry 加 pendingApprovalId 字段 + mark/clear API(D24 fallback) + +**Files:** +- Modify: `src/card/card-run-registry.ts` +- Test: `tests/unit/card-run-registry-approval.test.ts`(扩 PR-1 已建文件) + +> **fallback 语义边界(v3.6 D24 + v4 review issue 4):** +> - **主事实源永远是 callback payload 的 `cardPrivateData.params.approveId`**(v3 模板将 approveId 绑定到三按钮 params)。正常运行链路从不读 registry。 +> - `pendingApprovalId` registry 字段 **只是异常兜底**——应对老卡片(v3 前发的、模板没 approveId 变量)、平台 callback payload 字段丢失等罕见路径。 +> - **不是历史审批状态恢复入口**——重启后 registry 是空的,这是**可接受**的行为:用户点旧卡片会走"approveId 反查失败 → applyExpiredPatch → 按钮消失"降级路径(参 spec §6.6 "Channel 重启后用户点旧卡片")。 +> - 这一点必须明确在 commit message + 用户文档里,避免后续维护者把它当成"应该持久化的状态"过度优化。 + +- [ ] **Step 14.1: 写失败测试** + +在 `tests/unit/card-run-registry-approval.test.ts` 加(沿用 Task 7 引入的 `register()` 帮手 + `clearCardRunRegistryForTest()`): + +```typescript +import { + markCardRunPendingApproval, + clearCardRunPendingApproval, + resolveCardRun, + removeCardRun, +} from "../../src/card/card-run-registry"; + +describe("card-run-registry · pendingApprovalId(D24 fallback)", () => { + beforeEach(() => clearCardRunRegistryForTest()); + + it("markCardRunPendingApproval 写入 pendingApprovalId", () => { + register("ot1", { sessionKey: "s1", state: "INPUTING" }); + markCardRunPendingApproval("ot1", "abc123"); + expect(resolveCardRun("ot1")?.pendingApprovalId).toBe("abc123"); + }); + + it("clearCardRunPendingApproval 清除 pendingApprovalId", () => { + register("ot1", { sessionKey: "s1", state: "INPUTING" }); + markCardRunPendingApproval("ot1", "abc123"); + clearCardRunPendingApproval("ot1"); + expect(resolveCardRun("ot1")?.pendingApprovalId).toBeUndefined(); + }); + + it("mark on non-existent outTrackId 无副作用(不抛)", () => { + expect(() => markCardRunPendingApproval("no-such", "abc")).not.toThrow(); + }); + + it("clear on non-existent outTrackId 无副作用(不抛)", () => { + expect(() => clearCardRunPendingApproval("no-such")).not.toThrow(); + }); + + it("record 被 removeCardRun(或 TTL sweep)后整条 record 不存在,反查走 null fallback", () => { + register("ot1", { sessionKey: "s1", state: "INPUTING" }); + markCardRunPendingApproval("ot1", "abc123"); + removeCardRun("ot1"); + expect(resolveCardRun("ot1")).toBeNull(); + }); +}); +``` + +- [ ] **Step 14.2: 跑确认 fail** + +Run: `pnpm vitest run tests/unit/card-run-registry-approval.test.ts -t pendingApprovalId` +Expected: FAIL(API 未实现)。 + +- [ ] **Step 14.3: 在 src/card/card-run-registry.ts 实现** + +`CardRunRecord` 接口加: + +```typescript +export interface CardRunRecord { + // ... 既有字段 + pendingApprovalId?: string; +} +``` + +模块末尾追加: + +```typescript +export function markCardRunPendingApproval(outTrackId: string, approvalId: string): void { + const r = records.get(outTrackId); + if (r) r.pendingApprovalId = approvalId; +} + +export function clearCardRunPendingApproval(outTrackId: string): void { + const r = records.get(outTrackId); + if (r) r.pendingApprovalId = undefined; +} +``` + +- [ ] **Step 14.4: 跑确认 pass** + +Run: `pnpm vitest run tests/unit/card-run-registry-approval.test.ts` +Expected: 全部 PASS(PR-1 8 case + 5 新 case ≈ 13)。 + +- [ ] **Step 14.5: 跑既有 card-run-registry 测试无回归** + +Run: `pnpm vitest run tests/unit/card-run-registry` +Expected: 全部 PASS。 + +- [ ] **Step 14.6: Commit** + +```bash +git add src/card/card-run-registry.ts tests/unit/card-run-registry-approval.test.ts +git commit -m "$(cat <<'EOF' +feat(card-run-registry): 添加 pendingApprovalId 字段 + mark/clear API + +D24 v3.6 fallback:approveId 主链路通过卡片自带 callback 拿,但 callback +没带(老卡片 / 平台异常)时反查 registry。setter/clearer 对既有 outTrackId +路径无影响;record 被 sweep 时 pendingApprovalId 自然丢失(callback +反查失败时降级为 applyExpiredPatch)。 + +EOF +)" +``` + +--- + +## Task 15 · approval-card-patcher.ts(三 patcher,§1.X 单一事实表) + +**Files:** +- Create: `src/approval/approval-card-patcher.ts` +- Test: `tests/unit/approval-card-patcher.test.ts` + +- [ ] **Step 15.1: 写失败测试** + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// 真实 updateCardVariables 返回 Promise(HTTP status code),失败靠 axios throw +vi.mock("../../src/card-callback-service", () => ({ + updateCardVariables: vi.fn().mockResolvedValue(200), +})); +vi.mock("../../src/card/card-run-registry", () => ({ + markCardRunPendingApproval: vi.fn(), + clearCardRunPendingApproval: vi.fn(), +})); +vi.mock("../../src/config", () => ({ + getConfig: vi.fn(() => ({ clientId: "x", bypassProxyForSend: false })), +})); + +const { applyPendingPatch, applyResolvedPatch, applyExpiredPatch } = await import("../../src/approval/approval-card-patcher"); +const { updateCardVariables } = await import("../../src/card-callback-service"); +const { markCardRunPendingApproval, clearCardRunPendingApproval } = await import("../../src/card/card-run-registry"); +const mockPut = updateCardVariables as ReturnType; +const mockMark = markCardRunPendingApproval as ReturnType; +const mockClear = clearCardRunPendingApproval as ReturnType; +const TOKEN = "tok-xxx"; +const CONFIG = { clientId: "x", bypassProxyForSend: false } as never; + +describe("approval-card-patcher · applyPendingPatch", () => { + beforeEach(() => { mockPut.mockReset().mockResolvedValue(200); mockMark.mockReset(); mockClear.mockReset(); }); + + it("PUT 三变量:show_approve_btns='true' + hasAction='false' + approveId=", async () => { + await applyPendingPatch("ot1", "abc123", TOKEN, CONFIG); + expect(mockPut).toHaveBeenCalledWith("ot1", expect.objectContaining({ + show_approve_btns: "true", hasAction: "false", approveId: "abc123", + }), TOKEN, CONFIG); + }); + + it("不 PUT btns/status/statusFooter(v1 字段集严格)", async () => { + await applyPendingPatch("ot1", "abc123", TOKEN, CONFIG); + const vars = mockPut.mock.calls[0][1]; + expect(vars).not.toHaveProperty("btns"); + expect(vars).not.toHaveProperty("status"); + expect(vars).not.toHaveProperty("statusFooter"); + }); + + it("调 markCardRunPendingApproval(outTrackId, approvalId) 写 fallback", async () => { + await applyPendingPatch("ot1", "abc123", TOKEN, CONFIG); + expect(mockMark).toHaveBeenCalledWith("ot1", "abc123"); + }); + + it("PUT 失败(axios throw)会向上传播(不静默吞)", async () => { + const httpErr = Object.assign(new Error("Request failed with status code 500"), { response: { status: 500 } }); + mockPut.mockRejectedValueOnce(httpErr); + await expect(applyPendingPatch("ot1", "abc123", TOKEN, CONFIG)).rejects.toThrow(); + }); + + it("调用透传 config(用于 bypassProxyForSend)", async () => { + await applyPendingPatch("ot1", "abc123", TOKEN, CONFIG); + expect(mockPut).toHaveBeenCalledWith("ot1", expect.any(Object), TOKEN, CONFIG); + }); +}); + +describe("approval-card-patcher · applyResolvedPatch", () => { + beforeEach(() => { mockPut.mockReset().mockResolvedValue(200); mockClear.mockReset(); }); + + it("cardStillActive=true → hasAction='true'(恢复 stop)", async () => { + await applyResolvedPatch("ot1", "allow-once", TOKEN, true, CONFIG); + expect(mockPut).toHaveBeenCalledWith("ot1", expect.objectContaining({ + show_approve_btns: "false", approveId: "", hasAction: "true", + }), TOKEN, CONFIG); + }); + + it("cardStillActive=false → hasAction='false'", async () => { + await applyResolvedPatch("ot1", "deny", TOKEN, false, CONFIG); + expect(mockPut).toHaveBeenCalledWith("ot1", expect.objectContaining({ + show_approve_btns: "false", approveId: "", hasAction: "false", + }), TOKEN, CONFIG); + }); + + it("不写终态文字(v1 schema 无字段位,§7.1)", async () => { + await applyResolvedPatch("ot1", "allow-once", TOKEN, true, CONFIG); + const vars = mockPut.mock.calls[0][1]; + expect(vars).not.toHaveProperty("status"); + expect(vars).not.toHaveProperty("statusFooter"); + expect(vars).not.toHaveProperty("approval_status"); + }); + + it("调 clearCardRunPendingApproval(outTrackId)", async () => { + await applyResolvedPatch("ot1", "allow-once", TOKEN, true, CONFIG); + expect(mockClear).toHaveBeenCalledWith("ot1"); + }); +}); + +describe("approval-card-patcher · applyExpiredPatch", () => { + beforeEach(() => { mockPut.mockReset().mockResolvedValue(200); mockClear.mockReset(); }); + + it("字段集与 resolved 完全相同(show_approve_btns='false' + approveId='' + hasAction 按 cardStillActive)", async () => { + await applyExpiredPatch("ot1", TOKEN, true, CONFIG); + expect(mockPut).toHaveBeenCalledWith("ot1", expect.objectContaining({ + show_approve_btns: "false", approveId: "", hasAction: "true", + }), TOKEN, CONFIG); + await applyExpiredPatch("ot2", TOKEN, false, CONFIG); + expect(mockPut).toHaveBeenLastCalledWith("ot2", expect.objectContaining({ + show_approve_btns: "false", approveId: "", hasAction: "false", + }), TOKEN, CONFIG); + }); + + it("不写终态文字", async () => { + await applyExpiredPatch("ot1", TOKEN, true, CONFIG); + const vars = mockPut.mock.calls[0][1]; + expect(vars).not.toHaveProperty("status"); + }); + + it("调 clearCardRunPendingApproval(outTrackId)", async () => { + await applyExpiredPatch("ot1", TOKEN, true, CONFIG); + expect(mockClear).toHaveBeenCalledWith("ot1"); + }); +}); +``` + +- [ ] **Step 15.2: 跑确认 fail** + +Run: `pnpm vitest run tests/unit/approval-card-patcher.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 15.3: 实现 src/approval/approval-card-patcher.ts** + +```typescript +import { updateCardVariables } from "../card-callback-service"; +import { + markCardRunPendingApproval, + clearCardRunPendingApproval, +} from "../card/card-run-registry"; +import type { ApprovalDecision, DingTalkConfig } from "../types"; +import { + buildApprovalPendingCardParams, + buildApprovalClearedCardParams, +} from "./approval-card-state"; + +// updateCardVariables 返回 Promise(HTTP status),失败靠 axios throw。 +// patcher 不再检查 result.ok —— await 成功即成功,error 直接向上传播让 caller 决定(catch 内降级到 markdown)。 +// 字段集来源:approval-card-state.ts(spec §1.X 单一事实表的代码化身) + +export async function applyPendingPatch( + outTrackId: string, + approvalId: string, + token: string, + config: DingTalkConfig, +): Promise { + await updateCardVariables( + outTrackId, + buildApprovalPendingCardParams(approvalId), + token, + config, + ); + markCardRunPendingApproval(outTrackId, approvalId); +} + +export async function applyResolvedPatch( + outTrackId: string, + _decision: ApprovalDecision, + token: string, + cardStillActive: boolean, + config: DingTalkConfig, +): Promise { + await updateCardVariables( + outTrackId, + buildApprovalClearedCardParams(cardStillActive), + token, + config, + ); + clearCardRunPendingApproval(outTrackId); +} + +export async function applyExpiredPatch( + outTrackId: string, + token: string, + cardStillActive: boolean, + config: DingTalkConfig, +): Promise { + await updateCardVariables( + outTrackId, + buildApprovalClearedCardParams(cardStillActive), + token, + config, + ); + clearCardRunPendingApproval(outTrackId); +} +``` + +- [ ] **Step 15.4: 跑确认 pass** + +Run: `pnpm vitest run tests/unit/approval-card-patcher.test.ts` +Expected: 14 PASS。 + +- [ ] **Step 15.5: Commit** + +```bash +git add src/approval/approval-card-patcher.ts tests/unit/approval-card-patcher.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 approval-card-patcher(pending/resolved/expired) + +三个 patcher 函数 PUT cardParamMap 三变量集,与 spec §1.X 单一事实表 1:1 +对齐:pending = show_approve_btns:true + hasAction:false + approveId:; +resolved/expired = show_approve_btns:false + approveId:"" + hasAction 按 +cardStillActive 决定。v1 不写终态文字(schema 无字段位,§7.1)。 +pending 同时调 markCardRunPendingApproval 写 fallback;resolved/expired +调 clearCardRunPendingApproval 清。 + +EOF +)" +``` + + +--- + +## Task 16 · approval-markdown-render.ts(markdown 路径主路径) + +**Files:** +- Create: `src/approval/approval-markdown-render.ts` +- Test: `tests/unit/approval-markdown-render.test.ts` + +- [ ] **Step 16.1: 写失败测试** + +```typescript +import { describe, it, expect } from "vitest"; +import { + buildExecApprovalMarkdown, + buildPluginApprovalMarkdown, +} from "../../src/approval/approval-markdown-render"; + +const NOW = Date.parse("2026-05-19T10:00:00Z"); + +// 真实形态:ExecApprovalRequest = { id, request: payload, createdAtMs, expiresAtMs } +// payload 含 command / cwd / agentId / sessionKey / turnSourceXxx 等(Stage 0.A) +const execRequest = (payload: Record = {}, overrides: Record = {}) => + ({ + id: "abc123", + createdAtMs: NOW - 1000, + expiresAtMs: NOW + 10 * 60_000, + request: { + command: 'docker image prune -a -f --filter "until=720h"', + cwd: "/Users/zhumin/projects/openclaw", + ...payload, + }, + ...overrides, + }) as never; + +const pluginRequest = (payload: Record = {}, overrides: Record = {}) => + ({ + id: "plugin:xyz789", + createdAtMs: NOW - 1000, + expiresAtMs: NOW + 10 * 60_000, + request: { + toolName: "query_database", + description: "对 production.orders 表查询近 7 天订单", + ...payload, + }, + ...overrides, + }) as never; + +describe("buildExecApprovalMarkdown", () => { + it("含 approval id", () => { + expect(buildExecApprovalMarkdown(execRequest(), NOW)).toContain("abc123"); + }); + it("含 command preview(代码块)", () => { + expect(buildExecApprovalMarkdown(execRequest(), NOW)).toMatch(/```[\s\S]*docker image prune/); + }); + it("默认(无 allowedDecisions 限制) → 三种 decision 全显示", () => { + const md = buildExecApprovalMarkdown(execRequest(), NOW); + expect(md).toContain("/approve abc123 allow-once"); + expect(md).toContain("/approve abc123 allow-always"); + expect(md).toContain("/approve abc123 deny"); + }); + it("ask='always' → resolveExecApprovalRequestAllowedDecisions 返 [allow-once, deny] → 不渲染 allow-always", () => { + // ask=always 时上游 resolveExecApprovalRequestAllowedDecisions 返 ["allow-once", "deny"],不含 allow-always + const md = buildExecApprovalMarkdown(execRequest({ ask: "always" }), NOW); + expect(md).toContain("/approve abc123 allow-once"); + expect(md).toContain("/approve abc123 deny"); + expect(md).not.toContain("/approve abc123 allow-always"); + }); + it("显式 allowedDecisions=['deny'] → 仅渲染 deny 命令", () => { + const md = buildExecApprovalMarkdown(execRequest({ allowedDecisions: ["deny"] }), NOW); + expect(md).toContain("/approve abc123 deny"); + expect(md).not.toContain("/approve abc123 allow-once"); + expect(md).not.toContain("/approve abc123 allow-always"); + }); + it("含过期 hint(分钟)", () => { + expect(buildExecApprovalMarkdown(execRequest(), NOW)).toMatch(/10\s*分钟/); + }); +}); + +describe("buildPluginApprovalMarkdown", () => { + it("含 approval id(plugin: 前缀保留)", () => { + expect(buildPluginApprovalMarkdown(pluginRequest(), NOW)).toContain("plugin:xyz789"); + }); + it("含 toolName 与 description", () => { + const md = buildPluginApprovalMarkdown(pluginRequest(), NOW); + expect(md).toContain("query_database"); + expect(md).toContain("production.orders"); + }); + it("默认(无 allowedDecisions) → 三种 decision 全显示", () => { + const md = buildPluginApprovalMarkdown(pluginRequest(), NOW); + expect(md).toContain("/approve plugin:xyz789 allow-once"); + expect(md).toContain("/approve plugin:xyz789 allow-always"); + expect(md).toContain("/approve plugin:xyz789 deny"); + }); + it("显式 allowedDecisions=['allow-once'] → 仅渲染 allow-once", () => { + const md = buildPluginApprovalMarkdown(pluginRequest({ allowedDecisions: ["allow-once"] }), NOW); + expect(md).toContain("/approve plugin:xyz789 allow-once"); + expect(md).not.toContain("/approve plugin:xyz789 allow-always"); + expect(md).not.toContain("/approve plugin:xyz789 deny"); + }); + it("过期时间为 0 / 负数时不显示分钟数(边界)", () => { + const md = buildPluginApprovalMarkdown(pluginRequest({}, { expiresAtMs: NOW - 1000 }), NOW); + expect(md).not.toMatch(/-?\d+\s*分钟/); + }); +}); +``` + +- [ ] **Step 16.2: 跑确认 fail** + +Run: `pnpm vitest run tests/unit/approval-markdown-render.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 16.3: 实现 src/approval/approval-markdown-render.ts** + +```typescript +import { + resolveExecApprovalRequestAllowedDecisions, + type ExecApprovalRequest, + type PluginApprovalRequest, +} from "openclaw/plugin-sdk/approval-runtime"; +import type { ApprovalDecision } from "../types"; + +const ALL_DECISIONS: readonly ApprovalDecision[] = ["allow-once", "allow-always", "deny"]; + +const DECISION_LABEL: Record = { + "allow-once": "批准(仅一次)", + "allow-always": "批准(总是)", + "deny": "拒绝", +}; + +function formatExpireHint(expiresAtMs: number | undefined, nowMs: number): string { + if (!expiresAtMs || expiresAtMs <= nowMs) return ""; + const minutes = Math.round((expiresAtMs - nowMs) / 60_000); + return minutes > 0 ? `\n**过期时间**: ${minutes} 分钟` : ""; +} + +// 本地 plugin allowedDecisions 归一化 —— 上游 resolvePluginApprovalRequestAllowedDecisions +// 定义在 openclaw/src/infra/plugin-approvals.ts:54-69,但**未**从 openclaw/plugin-sdk/approval-runtime +// re-export(核实:approval-runtime.ts 只 re-export exec 版本,没有 plugin 版本)。 +// 跨进程引用 infra/* internal 文件不可取,所以 channel 内自实现轻量 helper,与上游 :54-69 的语义对齐: +// - 有显式 allowedDecisions 且非空 → 过滤合法值返回 +// - 否则返三种全允 +// 若将来上游公开 plugin 版本,直接替换为 SDK import + 删本 helper。 +function normalizePluginAllowedDecisions( + allowedDecisions?: readonly (ApprovalDecision | string)[] | null, +): readonly ApprovalDecision[] { + if (!Array.isArray(allowedDecisions)) return ALL_DECISIONS; + const filtered = allowedDecisions.filter( + (d): d is ApprovalDecision => (ALL_DECISIONS as readonly string[]).includes(d as string), + ); + return filtered.length > 0 ? filtered : ALL_DECISIONS; +} + +// 用上游 resolve 出的 allowed 列表生成命令模板——不渲染上游会拒的 decision +function decisionBlock(id: string, allowed: readonly ApprovalDecision[]): string { + return allowed + .map((d) => `${DECISION_LABEL[d]}:\`/approve ${id} ${d}\``) + .join("\n"); +} + +export function buildExecApprovalMarkdown(request: ExecApprovalRequest, nowMs: number): string { + const id = request.id; + const payload = request.request; + const cmd = payload?.command ?? "(no command)"; + const cwd = payload?.cwd; + const cwdLine = cwd ? `\n**cwd**: \`${cwd}\`` : ""; + // 上游公开 helper:同时考虑 ask + 显式 allowedDecisions(显式优先) + // 参 openclaw/src/infra/exec-approvals.ts:1251-1262 + const allowed = resolveExecApprovalRequestAllowedDecisions({ + ask: payload?.ask ?? null, + allowedDecisions: payload?.allowedDecisions, + }); + return [ + "### ⚠️ 需要审批:命令执行", + `**ID**: \`${id}\`${cwdLine}${formatExpireHint(request.expiresAtMs, nowMs)}`, + "", + "```", + cmd, + "```", + "", + decisionBlock(id, allowed), + ].join("\n"); +} + +export function buildPluginApprovalMarkdown(request: PluginApprovalRequest, nowMs: number): string { + const id = request.id; + const payload = request.request; + const tool = payload?.toolName ?? "(unknown tool)"; + // PluginApprovalRequestPayload 字段是 description(不是 toolDescription,参 Stage 0.A) + const desc = payload?.description ?? ""; + // 本地 helper(上游 resolvePluginApprovalRequestAllowedDecisions 定义在 + // openclaw/src/infra/plugin-approvals.ts:54-69 但未从 plugin-sdk 公开) + const allowed = normalizePluginAllowedDecisions(payload?.allowedDecisions); + return [ + "### ⚠️ 需要审批:插件调用", + `**ID**: \`${id}\`\n**Tool**: \`${tool}\`${formatExpireHint(request.expiresAtMs, nowMs)}`, + desc ? `\n${desc}` : "", + "", + decisionBlock(id, allowed), + ].join("\n"); +} +``` + +- [ ] **Step 16.4: 跑确认 pass** + +Run: `pnpm vitest run tests/unit/approval-markdown-render.test.ts` +Expected: 11 PASS(含 ask=always + 显式 allowedDecisions 两个新 case)。 + +- [ ] **Step 16.5: Commit** + +```bash +git add src/approval/approval-markdown-render.ts tests/unit/approval-markdown-render.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 approval-markdown-render(markdown 路径主路径) + +buildExec/PluginApprovalMarkdown 构造含 approval id、命令/工具 preview、 +过期 hint、三种 decision 的 /approve 复制即用模板的 markdown 文本。 +markdown 路径是主路径(D10 修订),非 fallback。 + +EOF +)" +``` + +--- + +## Task 17 · approval-callback-handler.ts(TOPIC_CARD 入口 → resolver → patcher) + +**Files:** +- Create: `src/approval/approval-callback-handler.ts` +- Test: `tests/unit/approval-callback-handler.test.ts` + +- [ ] **Step 17.1: 写失败测试** + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("../../src/approval/approval-resolver", () => ({ resolveApproval: vi.fn() })); +vi.mock("../../src/approval/approval-card-patcher", () => ({ + applyResolvedPatch: vi.fn().mockResolvedValue(undefined), + applyExpiredPatch: vi.fn().mockResolvedValue(undefined), +})); +vi.mock("../../src/card/card-run-registry", () => ({ + resolveCardRun: vi.fn(), + isActiveCardRun: vi.fn(() => true), +})); +vi.mock("../../src/send-service", () => ({ + sendProactiveTextOrMarkdown: vi.fn().mockResolvedValue({ ok: true }), +})); +vi.mock("../../src/auth", () => ({ + getAccessToken: vi.fn().mockResolvedValue("tok-xxx"), +})); +vi.mock("../../src/config", () => ({ + getConfig: vi.fn(() => ({ clientId: "x", bypassProxyForSend: false })), +})); + +const { tryHandleApprovalCallback } = await import("../../src/approval/approval-callback-handler"); +const { resolveApproval } = await import("../../src/approval/approval-resolver"); +const { applyResolvedPatch, applyExpiredPatch } = await import("../../src/approval/approval-card-patcher"); +const { resolveCardRun } = await import("../../src/card/card-run-registry"); +const { sendProactiveTextOrMarkdown } = await import("../../src/send-service"); + +const mockResolve = resolveApproval as ReturnType; +const mockApplyResolved = applyResolvedPatch as ReturnType; +const mockApplyExpired = applyExpiredPatch as ReturnType; +const mockResolveCard = resolveCardRun as ReturnType; +const mockSend = sendProactiveTextOrMarkdown as ReturnType; + +const analysis = (overrides: Record = {}) => ({ + actionId: "allow-once", + userId: "staffA", + outTrackId: "ai_card_xxx", + cardPrivateData: { + actionIds: ["allow-once"], + params: { action: "allow-once", approveId: "abc123" }, + }, + ...overrides, +}) as never; + +const base = { cfg: {} as never, accountId: "default", log: undefined as never }; + +describe("tryHandleApprovalCallback · 主链路解码", () => { + beforeEach(() => { + mockResolve.mockReset(); mockApplyResolved.mockReset(); mockApplyExpired.mockReset(); + mockResolveCard.mockReset(); mockSend.mockReset(); + }); + + it("非 approval actionId 返 { handled: false }", async () => { + const r = await tryHandleApprovalCallback({ ...base, analysis: analysis({ cardPrivateData: undefined, actionId: "feedback_up" }) }); + expect(r.handled).toBe(false); + expect(mockResolve).not.toHaveBeenCalled(); + }); + + it("主链路从 params.action 取 decision + params.approveId 取 approvalId", async () => { + mockResolve.mockResolvedValue({ ok: true }); + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + expect(mockResolve).toHaveBeenCalledWith(expect.objectContaining({ + approvalId: "abc123", decision: "allow-once", senderId: "staffA", + })); + }); + + it("fallback:params.approveId 缺失时反查 resolveCardRun(outTrackId).pendingApprovalId", async () => { + mockResolveCard.mockReturnValue({ pendingApprovalId: "from-registry" }); + mockResolve.mockResolvedValue({ ok: true }); + const a = analysis({ cardPrivateData: { actionIds: ["allow-once"], params: { action: "allow-once" } } }); + await tryHandleApprovalCallback({ ...base, analysis: a }); + expect(mockResolve).toHaveBeenCalledWith(expect.objectContaining({ approvalId: "from-registry" })); + }); + + it("fallback:主链路缺失且 registry 也无 → 调 applyExpiredPatch 且 return", async () => { + mockResolveCard.mockReturnValue(null); + const a = analysis({ cardPrivateData: { actionIds: ["allow-once"], params: { action: "allow-once" } } }); + const r = await tryHandleApprovalCallback({ ...base, analysis: a }); + expect(mockApplyExpired).toHaveBeenCalledWith( + "ai_card_xxx", "tok-xxx", expect.any(Boolean), + expect.objectContaining({ clientId: "x" }), + ); + expect(mockResolve).not.toHaveBeenCalled(); + expect(r.handled).toBe(true); + }); + + it("泛用 actionId fallback 但无 approvalId → 不接管,避免吞其它卡片按钮", async () => { + mockResolveCard.mockReturnValue(null); + const a = analysis({ cardPrivateData: { actionIds: ["deny"], params: {} } }); + const r = await tryHandleApprovalCallback({ ...base, analysis: a }); + expect(r).toEqual({ handled: false }); + expect(mockResolve).not.toHaveBeenCalled(); + expect(mockApplyExpired).not.toHaveBeenCalled(); + }); + + it("decision fallback:params.action 缺失但 actionIds[0]∈ALLOWED → 用 actionId 推 decision", async () => { + mockResolve.mockResolvedValue({ ok: true }); + const a = analysis({ cardPrivateData: { actionIds: ["deny"], params: { approveId: "abc" } } }); + await tryHandleApprovalCallback({ ...base, analysis: a }); + expect(mockResolve).toHaveBeenCalledWith(expect.objectContaining({ decision: "deny" })); + }); + + it("非 ALLOWED 的 actionId → 返 { handled: false }(让位 feedback / btn_stop)", async () => { + const a = analysis({ actionId: "btn_stop", cardPrivateData: { actionIds: ["btn_stop"], params: {} } }); + expect((await tryHandleApprovalCallback({ ...base, analysis: a })).handled).toBe(false); + }); +}); + +describe("tryHandleApprovalCallback · 5 reason 分支", () => { + beforeEach(() => { + mockResolve.mockReset(); mockApplyResolved.mockReset(); mockApplyExpired.mockReset(); + mockSend.mockReset().mockResolvedValue({ ok: true }); + }); + + it("ok=true → 调 applyResolvedPatch(三变量),不私聊", async () => { + mockResolve.mockResolvedValue({ ok: true }); + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + expect(mockApplyResolved).toHaveBeenCalledWith( + "ai_card_xxx", "allow-once", "tok-xxx", expect.any(Boolean), + expect.objectContaining({ clientId: "x" }), + ); + expect(mockApplyExpired).not.toHaveBeenCalled(); + expect(mockSend).not.toHaveBeenCalled(); + }); + + it("unauthorized → 私聊 forceMarkdown + 卡片不变", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "unauthorized" }); + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:staffA", + expect.stringContaining("无权"), + expect.objectContaining({ forceMarkdown: true }), + ); + expect(mockApplyResolved).not.toHaveBeenCalled(); + expect(mockApplyExpired).not.toHaveBeenCalled(); + }); + + it("invalid-decision → 不调 patcher,私聊重选提示(含 allowedDecisions)", async () => { + mockResolve.mockResolvedValue({ + ok: false, reason: "invalid-decision", + allowedDecisions: ["allow-once", "deny"], + }); + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + expect(mockApplyExpired).not.toHaveBeenCalled(); + expect(mockApplyResolved).not.toHaveBeenCalled(); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:staffA", + expect.stringMatching(/不支持.*allow-once.*deny/), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("already-resolved → applyExpiredPatch(兜底)", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "already-resolved" }); + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + expect(mockApplyExpired).toHaveBeenCalled(); + }); + + it("not-found → applyExpiredPatch", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "not-found" }); + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + expect(mockApplyExpired).toHaveBeenCalled(); + }); + + it("gateway-error → 私聊提示重试 + 卡片保持 pending", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "gateway-error" }); + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:staffA", + expect.stringContaining("稍后重试"), + expect.objectContaining({ forceMarkdown: true }), + ); + expect(mockApplyExpired).not.toHaveBeenCalled(); + expect(mockApplyResolved).not.toHaveBeenCalled(); + }); + + it("patcher 抛错被 catch(callback 已 ack)", async () => { + mockResolve.mockResolvedValue({ ok: true }); + mockApplyResolved.mockRejectedValueOnce(new Error("PUT failed")); + await expect(tryHandleApprovalCallback({ ...base, analysis: analysis() })).resolves.toEqual( + expect.objectContaining({ handled: true }), + ); + }); + + it("DingTalk token 获取失败不阻塞上游审批 resolve", async () => { + mockResolve.mockResolvedValue({ ok: true }); + mockGetAccessToken.mockRejectedValueOnce(new Error("token unavailable")); + await expect(tryHandleApprovalCallback({ ...base, analysis: analysis() })).resolves.toEqual( + { handled: true, reason: "resolved" }, + ); + expect(mockResolve).toHaveBeenCalledWith( + expect.objectContaining({ approvalId: "abc123", decision: "allow-once" }), + ); + expect(mockApplyResolved).not.toHaveBeenCalled(); + }); +}); +``` + +- [ ] **Step 17.2: 跑确认 fail** + +Run: `pnpm vitest run tests/unit/approval-callback-handler.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 17.3: 实现 src/approval/approval-callback-handler.ts** + +```typescript +import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import type { Logger } from "../types"; +import type { CardCallbackAnalysis } from "../card-callback-service"; +import type { ApprovalDecision } from "../types"; +import { getConfig } from "../config"; +import { getAccessToken } from "../auth"; +import { sendProactiveTextOrMarkdown } from "../send-service"; +import { resolveApproval } from "./approval-resolver"; +import { + applyResolvedPatch, + applyExpiredPatch, +} from "./approval-card-patcher"; +import { + resolveCardRun, + isActiveCardRun, +} from "../card/card-run-registry"; + +const ALLOWED_DECISIONS: ReadonlyArray = ["allow-once", "allow-always", "deny"]; + +export interface HandleCallbackInput { + cfg: OpenClawConfig; + accountId: string; + analysis: CardCallbackAnalysis; + log?: Logger; +} + +export interface HandleCallbackResult { + handled: boolean; + reason?: string; +} + +function parseDecision(analysis: CardCallbackAnalysis): ApprovalDecision | null { + const cpd = analysis.cardPrivateData; + const fromParams = typeof cpd?.params?.action === "string" ? cpd.params.action : null; + if (fromParams && (ALLOWED_DECISIONS as readonly string[]).includes(fromParams)) { + return fromParams as ApprovalDecision; + } + const fromActionId = + (Array.isArray(cpd?.actionIds) && typeof cpd.actionIds[0] === "string") + ? cpd.actionIds[0] + : (typeof analysis.actionId === "string" ? analysis.actionId : null); + if (fromActionId && (ALLOWED_DECISIONS as readonly string[]).includes(fromActionId)) { + return fromActionId as ApprovalDecision; + } + return null; +} + +/** + * 解出 approvalId。 + * **主事实源**:callback payload 的 `cardPrivateData.params.approveId`(v3 模板将 approveId 绑定到三按钮 params)。 + * **fallback**:registry `pendingApprovalId` 只在主事实源缺失时反查——属于异常兜底(老卡片 / 平台 payload 字段丢失等), + * 不是历史审批状态恢复入口;registry 是进程内 Map,重启 / 多 worker / TTL sweep 都会让 fallback miss,这是可接受行为 + * (miss 时 caller 会走 applyExpiredPatch 让按钮消失,参 spec §6.6)。 + */ +function resolveApprovalId(analysis: CardCallbackAnalysis): string | null { + // 主链路 —— 正常运行从这里返回 + const fromParams = analysis.cardPrivateData?.params?.approveId; + if (typeof fromParams === "string" && fromParams.length > 0) return fromParams; + // Fallback —— 异常情况才走(老卡片 / 平台异常) + const run = analysis.outTrackId ? resolveCardRun(analysis.outTrackId) : null; + return run?.pendingApprovalId ?? null; +} + +async function privateDmReject( + cfg: OpenClawConfig, accountId: string, userId: string, + text: string, log?: Logger, +): Promise { + await sendProactiveTextOrMarkdown( + getConfig(cfg, accountId), `user:${userId}`, text, + { forceMarkdown: true, accountId, log }, + ).catch(() => undefined); +} + +async function patchCardBestEffort( + dtConfig: ReturnType, + log: Logger | undefined, + patch: (token: string) => Promise, +): Promise { + try { + const token = await getAccessToken(dtConfig, log); + await patch(token); + } catch (err) { + log?.warn?.(`[DingTalk][Approval] card patch skipped: ${String(err)}`); + } +} + +export async function tryHandleApprovalCallback( + input: HandleCallbackInput, +): Promise { + const { cfg, accountId, analysis, log } = input; + const decision = parseDecision(analysis); + if (!decision) return { handled: false }; + if (!analysis.outTrackId) return { handled: false }; + + const dtConfig = getConfig(cfg, accountId); + const cardRun = resolveCardRun(analysis.outTrackId); + const cardStillActive = cardRun ? isActiveCardRun(cardRun) : false; + + const approvalId = resolveApprovalId(analysis); + if (!approvalId) { + if (!ALLOWED_DECISIONS.includes(analysis.cardPrivateData?.params?.action as ApprovalDecision)) { + // 没有 params.action 主链路、没有 approveId/fallback 时,可能只是其它卡片用了 deny 这类泛用 actionId。 + return { handled: false }; + } + await patchCardBestEffort(dtConfig, log, (token) => + applyExpiredPatch(analysis.outTrackId, token, cardStillActive, dtConfig)); + return { handled: true, reason: "no-pending-approval" }; + } + + const result = await resolveApproval({ + cfg, accountId, approvalId, decision, + senderId: analysis.userId ?? "", + log, + }); + + if (result.ok) { + await patchCardBestEffort(dtConfig, log, (token) => + applyResolvedPatch(analysis.outTrackId, decision, token, cardStillActive, dtConfig)); + return { handled: true, reason: "resolved" }; + } + + switch (result.reason) { + case "unauthorized": + await privateDmReject(cfg, accountId, analysis.userId ?? "", + `⛔ 你不在 approver 名单,无权批准此请求(${approvalId})`, log); + return { handled: true, reason: "unauthorized" }; + case "invalid-decision": { + const hint = result.allowedDecisions?.length + ? `请选择:${result.allowedDecisions.join(" / ")}` + : "请选择允许一次或拒绝"; + await privateDmReject(cfg, accountId, analysis.userId ?? "", + `ℹ️ 该审批不支持 ${decision}。${hint}(${approvalId})`, log); + return { handled: true, reason: "invalid-decision" }; + } + case "gateway-error": + await privateDmReject(cfg, accountId, analysis.userId ?? "", + `ℹ️ 审批暂时处理失败,请稍后重试(${approvalId})`, log); + return { handled: true, reason: "gateway-error" }; + case "already-resolved": + case "not-found": + await patchCardBestEffort(dtConfig, log, (token) => + applyExpiredPatch(analysis.outTrackId, token, cardStillActive, dtConfig)); + return { handled: true, reason: result.reason }; + } +} +``` + +- [ ] **Step 17.4: 跑确认 pass** + +Run: `pnpm vitest run tests/unit/approval-callback-handler.test.ts` +Expected: 13 PASS。 + +- [ ] **Step 17.5: Commit** + +```bash +git add src/approval/approval-callback-handler.ts tests/unit/approval-callback-handler.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 添加 TOPIC_CARD callback handler + +主链路 params.action / params.approveId + fallback actionIds[0] / registry +pendingApprovalId;调 approval-resolver 单点;按 5 reason 分支: +- ok → applyResolvedPatch +- unauthorized → 私聊 + 卡片保留 +- invalid-decision → 私聊重选 + 卡片保 pending(不调 patcher) + - already-resolved/not-found → applyExpiredPatch + - gateway-error → 私聊提示稍后重试 + 卡片保 pending(不调 patcher) +所有私聊强制 forceMarkdown:true。patcher 失败被 catch 不影响 ack。 + +EOF +)" +``` + +--- + +## Task 18 · approval-native-runtime.ts(4 子 adapter) + +**Files:** +- Create: `src/approval/approval-native-runtime.ts` +- Test: `tests/unit/approval-native-runtime.test.ts` + +> 上游契约:`openclaw/src/infra/approval-handler-runtime-types.ts:216-235` ChannelApprovalNativeRuntimeAdapter(3 必需 + 2 可选)。v1 实现 availability/presentation/transport/observe 4 个;interactions 推迟 v2。 +> **不用 `createLazyChannelApprovalNativeRuntimeAdapter`**——该 lazy 包装只接受 `{ load, isConfigured, shouldHandle, eventKinds?, resolveApprovalKind? }`,不能把 availability/presentation/transport 塞进同一对象(参 Stage 0.A)。直接 `return { eventKinds, availability, presentation, transport, observe }` 即满足 `ChannelApprovalNativeRuntimeAdapter` 类型。 + +- [ ] **Step 18.1: 写失败测试** + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("../../src/approval/approval-config", () => ({ + getExecApprovalsConfig: vi.fn(), + listExecApprovers: vi.fn(), + resolveNativeDeliveryMode: vi.fn(() => "channel"), +})); +vi.mock("../../src/approval/approval-card-locator", () => ({ + findActiveAgentCard: vi.fn(), +})); +vi.mock("../../src/approval/approval-card-patcher", () => ({ + applyPendingPatch: vi.fn().mockResolvedValue(undefined), + applyResolvedPatch: vi.fn().mockResolvedValue(undefined), + applyExpiredPatch: vi.fn().mockResolvedValue(undefined), +})); +vi.mock("../../src/approval/approval-markdown-render", () => ({ + buildExecApprovalMarkdown: vi.fn(() => "exec-md"), + buildPluginApprovalMarkdown: vi.fn(() => "plugin-md"), +})); +vi.mock("../../src/approval/approval-target-resolver", () => ({ + normalizeApprovalTargetTo: vi.fn((s: string) => s), +})); +vi.mock("../../src/card/card-run-registry", () => ({ + resolveCardRun: vi.fn(), + isActiveCardRun: vi.fn(() => false), +})); +vi.mock("../../src/send-service", () => ({ + sendProactiveTextOrMarkdown: vi.fn().mockResolvedValue({ ok: true }), +})); +// getAccessToken 真实签名 (config: DingTalkConfig, log?: Logger) → Promise +vi.mock("../../src/auth", () => ({ getAccessToken: vi.fn().mockResolvedValue("tok") })); +vi.mock("../../src/config", () => ({ + getConfig: vi.fn(() => ({ clientId: "x", bypassProxyForSend: false })), +})); +vi.mock("../../src/logger-context", () => ({ + getLogger: vi.fn(() => undefined), +})); + +const { createDingTalkApprovalNativeRuntime } = await import("../../src/approval/approval-native-runtime"); +const { getExecApprovalsConfig, listExecApprovers } = await import("../../src/approval/approval-config"); +const { findActiveAgentCard } = await import("../../src/approval/approval-card-locator"); +const { applyPendingPatch } = await import("../../src/approval/approval-card-patcher"); +const { sendProactiveTextOrMarkdown } = await import("../../src/send-service"); + +const mockGetCfg = getExecApprovalsConfig as ReturnType; +const mockListApprovers = listExecApprovers as ReturnType; +const mockFind = findActiveAgentCard as ReturnType; +const mockPending = applyPendingPatch as ReturnType; +const mockSend = sendProactiveTextOrMarkdown as ReturnType; + +const runtime = createDingTalkApprovalNativeRuntime(); + +// 真实 ApprovalRequest 形态:turnSourceXxx / sessionKey 嵌在 request.request.* payload +const baseRequest = (payload: Record = {}) => ({ + id: "abc123", + createdAtMs: Date.now() - 1000, + expiresAtMs: Date.now() + 600_000, + request: { + sessionKey: "sess-A", + turnSourceChannel: "dingtalk", + turnSourceTo: "group:cid_xxx", + turnSourceAccountId: "default", + turnSourceThreadId: null, + ...payload, + }, +}) as never; + +describe("availability", () => { + beforeEach(() => { mockGetCfg.mockReset(); mockListApprovers.mockReset(); }); + + it("isConfigured 透传 getExecApprovalsConfig().isNativeDeliveryEnabled", () => { + mockGetCfg.mockReturnValue({ isNativeDeliveryEnabled: true }); + expect(runtime.availability.isConfigured({ cfg: {} as never, accountId: "default" })).toBe(true); + }); + + it("shouldHandle 四连判:dingtalk turn source + 可解析 to + 非空 approvers + isConfigured", () => { + mockGetCfg.mockReturnValue({ isNativeDeliveryEnabled: true }); + mockListApprovers.mockReturnValue(["staffA"]); + expect(runtime.availability.shouldHandle({ + cfg: {} as never, accountId: "default", request: baseRequest(), + })).toBe(true); + }); + + it("shouldHandle: turnSourceChannel != dingtalk 返 false", () => { + mockGetCfg.mockReturnValue({ isNativeDeliveryEnabled: true }); + mockListApprovers.mockReturnValue(["staffA"]); + expect(runtime.availability.shouldHandle({ + cfg: {} as never, accountId: "default", + request: baseRequest({ turnSourceChannel: "discord" }), + })).toBe(false); + }); + + it("shouldHandle: turnSourceTo 为空返 false", () => { + mockGetCfg.mockReturnValue({ isNativeDeliveryEnabled: true }); + mockListApprovers.mockReturnValue(["staffA"]); + expect(runtime.availability.shouldHandle({ + cfg: {} as never, accountId: "default", + request: baseRequest({ turnSourceTo: null }), + })).toBe(false); + }); + + it("shouldHandle: 无 approvers 返 false", () => { + mockGetCfg.mockReturnValue({ isNativeDeliveryEnabled: true }); + mockListApprovers.mockReturnValue([]); + expect(runtime.availability.shouldHandle({ + cfg: {} as never, accountId: "default", request: baseRequest(), + })).toBe(false); + }); + + it("shouldHandle: isConfigured=false 返 false", () => { + mockGetCfg.mockReturnValue({ isNativeDeliveryEnabled: false }); + mockListApprovers.mockReturnValue(["staffA"]); + expect(runtime.availability.shouldHandle({ + cfg: {} as never, accountId: "default", request: baseRequest(), + })).toBe(false); + }); +}); + +describe("transport.prepareTarget", () => { + beforeEach(() => mockFind.mockReset()); + + // prepareTarget 真实参数:{ cfg, accountId, plannedTarget, request, approvalKind, view, pendingPayload } + // plannedTarget shape = { surface, target, reason }(不含 cfg) + + it("找到 active card → route=card + activeCardOutTrackId", () => { + mockFind.mockReturnValue({ outTrackId: "ai_card_xxx", sessionKey: "sess-A" }); + const t = runtime.transport.prepareTarget({ + cfg: {} as never, accountId: "default", + plannedTarget: { surface: "channel", target: { to: "group:cid_xxx" }, reason: "preferred" }, + request: baseRequest(), + approvalKind: "exec", + } as never); + expect(t).toEqual(expect.objectContaining({ + route: "card", activeCardOutTrackId: "ai_card_xxx", + target: expect.objectContaining({ to: "group:cid_xxx" }), + })); + }); + + it("未找到 active card → route=markdown", () => { + mockFind.mockReturnValue(null); + const t = runtime.transport.prepareTarget({ + cfg: {} as never, accountId: "default", + plannedTarget: { surface: "channel", target: { to: "group:cid_xxx" }, reason: "preferred" }, + request: baseRequest(), + approvalKind: "exec", + } as never); + expect(t.route).toBe("markdown"); + expect(t).not.toHaveProperty("activeCardOutTrackId"); + }); + + it("dedupeKey 含 accountId + to + outTrackId(card 路径,target.accountId 优先于 params.accountId)", () => { + mockFind.mockReturnValue({ outTrackId: "ot1", sessionKey: "s1" }); + const t = runtime.transport.prepareTarget({ + cfg: {} as never, accountId: "default", + plannedTarget: { surface: "channel", target: { to: "group:c", accountId: "acme" }, reason: "preferred" }, + request: baseRequest(), + approvalKind: "exec", + } as never); + expect(t.dedupeKey).toContain("acme"); + expect(t.dedupeKey).toContain("ot1"); + }); + + it("target.accountId 缺失时 fallback 到 params.accountId", () => { + mockFind.mockReturnValue(null); + const t = runtime.transport.prepareTarget({ + cfg: {} as never, accountId: "acme", + plannedTarget: { surface: "channel", target: { to: "group:c" }, reason: "preferred" }, + request: baseRequest(), + approvalKind: "exec", + } as never); + expect(t.dedupeKey).toContain("acme"); + }); +}); + +describe("transport.deliverPending", () => { + beforeEach(() => { mockPending.mockReset().mockResolvedValue(undefined); mockSend.mockReset().mockResolvedValue({ ok: true }); }); + + it("route=card 成功 → entry.mode=card + outTrackId", async () => { + const entry = await runtime.transport.deliverPending({ + cfg: {} as never, accountId: "default", + preparedTarget: { route: "card", activeCardOutTrackId: "ot1", target: { to: "group:c" } }, + request: baseRequest(), + pendingPayload: { approvalId: "abc", markdownText: "md" }, + } as never); + expect(mockPending).toHaveBeenCalledWith("ot1", "abc", "tok", expect.objectContaining({ clientId: "x" })); + expect(entry).toEqual(expect.objectContaining({ mode: "card", outTrackId: "ot1", approvalId: "abc" })); + }); + + it("route=card 明确失败 → 降级 markdown,entry.mode=markdown", async () => { + mockPending.mockRejectedValueOnce(Object.assign(new Error("400"), { status: 400 })); + const entry = await runtime.transport.deliverPending({ + cfg: {} as never, accountId: "default", + preparedTarget: { route: "card", activeCardOutTrackId: "ot1", target: { to: "group:c" } }, + request: baseRequest(), + pendingPayload: { approvalId: "abc", markdownText: "md" }, + } as never); + expect(mockSend).toHaveBeenCalledWith(expect.anything(), "group:c", "md", expect.objectContaining({ forceMarkdown: true })); + expect(entry?.mode).toBe("markdown"); + }); + + it("route=card 模糊失败(超时)→ 返 null(不重发)", async () => { + mockPending.mockRejectedValueOnce(Object.assign(new Error("timeout"), { code: "ETIMEDOUT" })); + const entry = await runtime.transport.deliverPending({ + cfg: {} as never, accountId: "default", + preparedTarget: { route: "card", activeCardOutTrackId: "ot1", target: { to: "group:c" } }, + request: baseRequest(), + pendingPayload: { approvalId: "abc", markdownText: "md" }, + } as never); + expect(entry).toBeNull(); + expect(mockSend).not.toHaveBeenCalled(); + }); + + it("route=markdown 走 sendProactiveTextOrMarkdown(forceMarkdown:true)", async () => { + const entry = await runtime.transport.deliverPending({ + cfg: {} as never, accountId: "default", + preparedTarget: { route: "markdown", target: { to: "group:c" } }, + request: baseRequest(), + pendingPayload: { approvalId: "abc", markdownText: "md" }, + } as never); + expect(mockSend).toHaveBeenCalledWith(expect.anything(), "group:c", "md", expect.objectContaining({ forceMarkdown: true })); + expect(entry?.mode).toBe("markdown"); + }); + + it("route=markdown 失败 → return null", async () => { + mockSend.mockResolvedValueOnce({ ok: false, error: "5xx" }); + const entry = await runtime.transport.deliverPending({ + cfg: {} as never, accountId: "default", + preparedTarget: { route: "markdown", target: { to: "group:c" } }, + request: baseRequest(), + pendingPayload: { approvalId: "abc", markdownText: "md" }, + } as never); + expect(entry).toBeNull(); + }); +}); + +describe("transport.updateEntry · 按 entry.mode 分支 + cardStillActive 真实查询", () => { + let mockApplyResolved: ReturnType; + let mockApplyExpired: ReturnType; + let mockResolveCard: ReturnType; + let mockIsActive: ReturnType; + + beforeEach(async () => { + const patcher = await import("../../src/approval/approval-card-patcher"); + const registry = await import("../../src/card/card-run-registry"); + mockApplyResolved = patcher.applyResolvedPatch as ReturnType; + mockApplyExpired = patcher.applyExpiredPatch as ReturnType; + mockResolveCard = registry.resolveCardRun as ReturnType; + mockIsActive = registry.isActiveCardRun as ReturnType; + mockApplyResolved.mockReset(); mockApplyExpired.mockReset(); + mockResolveCard.mockReset(); mockIsActive.mockReset(); + }); + + it("mode=card · resolved · card 仍 active → applyResolvedPatch(..., true)", async () => { + mockResolveCard.mockReturnValue({ outTrackId: "ot1", card: { state: "INPUTING" } }); + mockIsActive.mockReturnValue(true); + await runtime.transport.updateEntry({ + cfg: {} as never, accountId: "default", + entry: { mode: "card", outTrackId: "ot1", approvalId: "abc" } as never, + payload: { decision: "allow-once" } as never, + phase: "resolved" as never, + } as never); + expect(mockApplyResolved).toHaveBeenCalledWith("ot1", "allow-once", "tok", true, expect.objectContaining({ clientId: "x" })); + }); + + it("mode=card · resolved · card 已 FINISHED → applyResolvedPatch(..., false)", async () => { + mockResolveCard.mockReturnValue({ outTrackId: "ot1", card: { state: "FINISHED" } }); + mockIsActive.mockReturnValue(false); + await runtime.transport.updateEntry({ + cfg: {} as never, accountId: "default", + entry: { mode: "card", outTrackId: "ot1", approvalId: "abc" } as never, + payload: { decision: "allow-once" } as never, + phase: "resolved" as never, + } as never); + expect(mockApplyResolved).toHaveBeenCalledWith("ot1", "allow-once", "tok", false, expect.objectContaining({ clientId: "x" })); + }); + + it("mode=card · phase=expired → applyExpiredPatch(cardStillActive 同样按 registry)", async () => { + mockResolveCard.mockReturnValue(null); + mockIsActive.mockReturnValue(false); + await runtime.transport.updateEntry({ + cfg: {} as never, accountId: "default", + entry: { mode: "card", outTrackId: "ot1", approvalId: "abc" } as never, + payload: {} as never, + phase: "expired" as never, + } as never); + expect(mockApplyExpired).toHaveBeenCalledWith("ot1", "tok", false, expect.objectContaining({ clientId: "x" })); + }); + + it("mode=markdown · phase=resolved → no-op", async () => { + await runtime.transport.updateEntry({ + cfg: {} as never, accountId: "default", + entry: { mode: "markdown", approvalId: "abc" } as never, + payload: { decision: "allow-once" } as never, + phase: "resolved" as never, + } as never); + expect(mockApplyResolved).not.toHaveBeenCalled(); + expect(mockApplyExpired).not.toHaveBeenCalled(); + }); +}); +``` + +- [ ] **Step 18.2: 跑确认 fail** + +Run: `pnpm vitest run tests/unit/approval-native-runtime.test.ts` +Expected: FAIL(模块未实现)。 + +- [ ] **Step 18.3: 实现 src/approval/approval-native-runtime.ts** + +```typescript +import type { ChannelApprovalNativeRuntimeAdapter } from "openclaw/plugin-sdk/approval-handler-runtime"; +import { + getExecApprovalsConfig, + listExecApprovers, +} from "./approval-config"; +import { findActiveAgentCard } from "./approval-card-locator"; +import { + applyPendingPatch, applyResolvedPatch, applyExpiredPatch, +} from "./approval-card-patcher"; +import { + buildExecApprovalMarkdown, + buildPluginApprovalMarkdown, +} from "./approval-markdown-render"; +import { normalizeApprovalTargetTo } from "./approval-target-resolver"; +import { + resolveCardRun, + isActiveCardRun, +} from "../card/card-run-registry"; +import { getAccessToken } from "../auth"; +import { getConfig } from "../config"; +import { getLogger } from "../logger-context"; +import { sendProactiveTextOrMarkdown } from "../send-service"; + +// HTTP status 已知错误码集合 — 明确失败(降级 markdown) +// +// 设计决策:4xx + 5xx 都归"明确失败 → 降级 markdown",而不是把 5xx 当"模糊失败 → null"。 +// 理由: +// 1. payload 就在手上(pendingPayload.markdownText),降级到 markdown 不需要额外请求即可送达; +// 2. 我们**不**做 retry-with-backoff —— deliverPending 设计为单次尝试,重试逻辑由上游 approval- +// handler-runtime 决定(如果上游觉得需要重试它会再调一次 deliverPending,到时再走完整 flow); +// 3. 5xx 当 null 会让用户在群里**完全看不到 pending 提示**,UX 比"上线的是 markdown 而非按钮"更差。 +// 只有 timeout / ECONNRESET / ECONNABORTED 这类**结果未定**的失败才 return null(卡片可能已经 PUT +// 成功但响应丢了;再发 markdown 会变重复提示)。 +function isExplicitHttpFailure(err: unknown): boolean { + const e = err as { status?: number; response?: { status?: number }; code?: string } | null; + if (!e) return false; + const status = typeof e.status === "number" ? e.status : e.response?.status; + if (typeof status === "number" && status >= 400) return true; + if (e.code === "EBADREQ") return true; + return false; +} + +function isTimeoutFailure(err: unknown): boolean { + const e = err as { code?: string } | null; + return e?.code === "ETIMEDOUT" || e?.code === "ECONNRESET" || e?.code === "ECONNABORTED"; +} + +// 不用 createLazyChannelApprovalNativeRuntimeAdapter(参 Stage 0.A)—— 直接返字面量 adapter +export function createDingTalkApprovalNativeRuntime(): ChannelApprovalNativeRuntimeAdapter { + return { + eventKinds: ["exec", "plugin"], + + availability: { + isConfigured: ({ cfg, accountId }) => + getExecApprovalsConfig({ cfg, accountId }).isNativeDeliveryEnabled, + shouldHandle: ({ cfg, accountId, request }) => { + if (!getExecApprovalsConfig({ cfg, accountId }).isNativeDeliveryEnabled) return false; + const payload = request.request; + if (payload?.turnSourceChannel !== "dingtalk") return false; + if (!payload.turnSourceTo) return false; + if (listExecApprovers({ cfg, accountId }).length === 0) return false; + return true; + }, + }, + + presentation: { + buildPendingPayload: ({ request, approvalKind, nowMs }) => { + // 用上游传入的 approvalKind ("exec" | "plugin"),不靠 id 前缀猜 + // —— 上游 id 格式将来变化(如 plugin id 不带前缀)也不会错位 + const markdownText = approvalKind === "plugin" + ? buildPluginApprovalMarkdown(request as never, nowMs) + : buildExecApprovalMarkdown(request as never, nowMs); + return { approvalId: request.id, markdownText }; + }, + buildResolvedResult: ({ resolved }) => ({ + kind: "update", + payload: { phase: "resolved", decision: resolved.decision }, + }), + buildExpiredResult: () => ({ + kind: "update", + payload: { phase: "expired" }, + }), + }, + + transport: { + // 注意:prepareTarget params 含 cfg / accountId(来自 ChannelApprovalCapabilityHandlerContext, + // 参 Stage 0.A);plannedTarget 只是 { surface, target, reason },不含 cfg + prepareTarget: ({ cfg, accountId, plannedTarget, request }) => { + const rawTo = (plannedTarget.target as { to: string }).to; + const normalizedTo = normalizeApprovalTargetTo(rawTo); + const resolvedAccountId = + (plannedTarget.target as { accountId?: string }).accountId ?? accountId ?? "default"; + const found = findActiveAgentCard({ + cfg, + accountId: resolvedAccountId, + sessionKey: request.request?.sessionKey ?? "", + }); + if (found) { + return { + target: { ...plannedTarget.target, to: normalizedTo, accountId: resolvedAccountId }, + threadId: null, + route: "card" as const, + activeCardOutTrackId: found.outTrackId, + dedupeKey: `dingtalk:${resolvedAccountId}:${normalizedTo}:${found.outTrackId}`, + }; + } + return { + target: { ...plannedTarget.target, to: normalizedTo, accountId: resolvedAccountId }, + threadId: null, + route: "markdown" as const, + dedupeKey: `dingtalk:${resolvedAccountId}:${normalizedTo}:markdown:${request.id}`, + }; + }, + + deliverPending: async ({ cfg, accountId, preparedTarget, request: _request, pendingPayload }) => { + const tgt = preparedTarget as { + route: "card" | "markdown"; + activeCardOutTrackId?: string; + target: { to: string }; + }; + const dtConfig = getConfig(cfg, accountId); + const log = getLogger(accountId); + if (tgt.route === "card" && tgt.activeCardOutTrackId) { + const token = await getAccessToken(dtConfig, log); + try { + await applyPendingPatch(tgt.activeCardOutTrackId, pendingPayload.approvalId, token, dtConfig); + return { + approvalId: pendingPayload.approvalId, + accountId, mode: "card", + outTrackId: tgt.activeCardOutTrackId, + }; + } catch (err) { + if (isTimeoutFailure(err)) return null; + if (isExplicitHttpFailure(err)) { + // 明确失败 → 降级到 markdown + const md = await sendProactiveTextOrMarkdown( + dtConfig, tgt.target.to, pendingPayload.markdownText, + { forceMarkdown: true, accountId, log }, + ); + if (md?.ok) { + return { approvalId: pendingPayload.approvalId, accountId, mode: "markdown" }; + } + return null; + } + return null; + } + } + // markdown 路径 + const md = await sendProactiveTextOrMarkdown( + dtConfig, tgt.target.to, pendingPayload.markdownText, + { forceMarkdown: true, accountId, log }, + ); + if (!md?.ok) return null; + return { approvalId: pendingPayload.approvalId, accountId, mode: "markdown" }; + }, + + updateEntry: async ({ cfg, accountId, entry, payload, phase }) => { + const e = entry as { mode: "card" | "markdown"; outTrackId?: string }; + if (e.mode !== "card" || !e.outTrackId) return; + const dtConfig = getConfig(cfg, accountId); + const log = getLogger(accountId); + const token = await getAccessToken(dtConfig, log); + // 按 registry 真实状态判 cardStillActive,用于 hasAction 恢复 + const record = resolveCardRun(e.outTrackId); + const cardStillActive = record ? isActiveCardRun(record) : false; + if (phase === "resolved") { + await applyResolvedPatch(e.outTrackId, (payload as { decision: never }).decision, token, cardStillActive, dtConfig); + } else { + await applyExpiredPatch(e.outTrackId, token, cardStillActive, dtConfig); + } + }, + }, + + observe: { + // accountId 直接从上游 ChannelApprovalCapabilityHandlerContext 拿(observe 参数继承自 context) + // —— 比从 entry.accountId / plannedTarget.target.accountId 回推更稳:delivery error 阶段 + // entry 可能为 null,plannedTarget 也可能因 prepareTarget 抛错没成形。 + onDelivered: ({ accountId, entry, request }) => { + getLogger(accountId)?.info(`[DingTalk][Approval] delivered approval=${request.id} mode=${(entry as { mode?: string }).mode}`); + }, + onDeliveryError: ({ accountId, error, request }) => { + getLogger(accountId)?.warn(`[DingTalk][Approval][DeliveryError] approval=${request.id} error=${(error as Error)?.message}`); + }, + }, + }; +} +``` + +> 注:`cardStillActive` 用 `resolveCardRun(outTrackId)` + `isActiveCardRun()` 真实查询,保证审批通过后 agent 仍在 stream 时能恢复 btn_stop(参 spec D23)。registry record 被 sweep / 不存在时退化为 `false`(resolved 时不恢复 stop,符合"卡片已不可控"语义)。 + +- [ ] **Step 18.4: 跑确认 pass** + +Run: `pnpm vitest run tests/unit/approval-native-runtime.test.ts` +Expected: ≥ 14 PASS。 + +- [ ] **Step 18.5: Commit** + +```bash +git add src/approval/approval-native-runtime.ts tests/unit/approval-native-runtime.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): 实现 native runtime 4 子 adapter + +availability(4 连判 origin-only)+ presentation(pending/resolved/expired +payload,按上游传入的 approvalKind ("exec"|"plugin") 选 markdown builder)+ transport(D22 双路由: +prepareTarget 查 card-locator 决定 route;deliverPending card 明确失败降级 +markdown,模糊失败 return null;updateEntry 按 entry.mode 分支调 patcher 或 +no-op)+ observe(投递日志)。 + +interactions 推迟 v2。 + +EOF +)" +``` + + +--- + +## Task 19 · approval-capability.ts 接 nativeRuntime + +**Files:** +- Modify: `src/approval/approval-capability.ts` +- Test: `tests/unit/approval-capability.test.ts`(扩 PR-1 已建文件) + +- [ ] **Step 19.1: 写失败测试** + +在 `tests/unit/approval-capability.test.ts` 加: + +```typescript +import { createDingTalkApprovalNativeRuntime } from "../../src/approval/approval-native-runtime"; +vi.mock("../../src/approval/approval-native-runtime", () => ({ + createDingTalkApprovalNativeRuntime: vi.fn(() => ({ marker: "native-runtime" })), +})); + +describe("PR-2 增量 · nativeRuntime 挂接", () => { + it("createDingTalkApprovalCapability 传 nativeRuntime 给工厂", () => { + createDingTalkApprovalCapability(); + expect(factory).toHaveBeenCalledWith(expect.objectContaining({ + nativeRuntime: expect.objectContaining({ marker: "native-runtime" }), + })); + }); +}); +``` + +- [ ] **Step 19.2: 跑确认 fail** + +Run: `pnpm vitest run tests/unit/approval-capability.test.ts -t "nativeRuntime 挂接"` +Expected: FAIL(capability 未传 nativeRuntime)。 + +- [ ] **Step 19.3: 修改 src/approval/approval-capability.ts** + +加 import: + +```typescript +import { createDingTalkApprovalNativeRuntime } from "./approval-native-runtime"; +``` + +在工厂参数对象内加: + +```typescript +nativeRuntime: createDingTalkApprovalNativeRuntime(), +``` + +- [ ] **Step 19.4: 跑确认 pass** + +Run: `pnpm vitest run tests/unit/approval-capability.test.ts` +Expected: 7 PASS(PR-1 6 + 1 新增)。 + +- [ ] **Step 19.5: Commit** + +```bash +git add src/approval/approval-capability.ts tests/unit/approval-capability.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): capability 接上 nativeRuntime(PR-2 收尾装配) + +createDingTalkApprovalCapability 工厂参数加 nativeRuntime,至此 channel +plugin 完整实现 ChannelApprovalCapability —— 上游 approval-handler-runtime +可以触发 4 子 adapter 调用 DingTalk channel 投递路径。 + +EOF +)" +``` + +--- + +## Task 20 · channel-gateway TOPIC_CARD listener 接入 approval 分支 + +**Files:** +- Modify: `src/gateway/channel-gateway.ts`:在 `channel-gateway.ts:352-382` 的 feedback-learning block **之后**、`channel-gateway.ts:383` `handleCardAction` 调用**之前**插入 approval 分支 +- Test: `tests/unit/channel-gateway-approval.test.ts` + +### TOPIC_CARD listener 三段式优先级(PR-2 落地后的稳定形态) + +按此**固定顺序**处理 card callback,未来加新按钮类型也按这个表插。**违反顺序即被视为回归**。 + +| 顺序 | 阶段 | actionId 范围 | 实现入口 | 命中后行为 | +|---|---|---|---|---| +| 1 | feedback-learning | `feedback_up` / `feedback_down` | 既有 `channel-gateway.ts:352-382` block | 记录学习 + 发**反馈确认消息**(业务层 ack,发给用户的提示,不是平台 callback ack);不 return,让 listener 继续走到阶段 2/3;**平台 callback ack 统一由 listener 顶部 `finally { acknowledge() }` 处理,本阶段不要手动调 `client.socketCallBackResponse`** | +| 2 | **approval (本 task 新增)** | `allow-once` / `allow-always` / `deny`(按 `cardPrivateData.params.action` 主链路 + actionId fallback 精确匹配) | `tryHandleApprovalCallback` | 命中 → `return`(短路;listener 顶部已有 `finally { acknowledge() }` 完成 ack);未命中 → 继续到阶段 3 | +| 3 | handleCardAction | `btn_stop` 等其它按钮 | 既有 `handleCardAction(...)` 调用 | 既有逻辑 | + +**为什么这个顺序**: +- feedback 必须在 approval **之前**——feedback / approval actionId 实际不冲突,但 feedback block 已与上游 OpenClaw 学习链路深度耦合(写 store / 发 ack);放在 approval 后会让 reviewer 怀疑 approval 是否吞了 feedback。 +- approval 必须在 handleCardAction **之前**——approval 按钮命中后必须 `return`,避免 `handleCardAction` 继续处理同一 actionId(虽然 actionId 不重叠,**显式短路**更安全 + 减少不必要的 stop-detection 日志)。 +- 三段都是**单一职责**:阶段 1 不解析 approval,阶段 2 不发 feedback,阶段 3 不识 approval。**actionId 路由集中在各阶段内部**,gateway 不做总分发。 + +**未来扩展原则**:再加新按钮类型(如 v2 future feedback-button-with-comment)时,遵循"先 ack 类(无 return)→ 中 short-circuit 类(return)→ 末尾 fallback 类(既有 handleCardAction)"的顺序;新增 short-circuit 类按钮必须更新本表 + 加单测验证不踩之前阶段。 + +- [ ] **Step 20.1: 写失败测试** + +```typescript +import { describe, it, expect, vi, beforeEach } from "vitest"; + +vi.mock("../../src/approval/approval-callback-handler", () => ({ + tryHandleApprovalCallback: vi.fn(), +})); +vi.mock("../../src/card-callback-service", () => ({ + analyzeCardCallback: vi.fn(), + // 不 mock socketCallBackResponse —— 该方法在 client 实例上,由 gateway fixture 的 client mock 提供 + // (listener 通过 client.socketCallBackResponse 在 finally 块的 acknowledge() helper 里 ack, + // 参 src/gateway/channel-gateway.ts:332,337,404) +})); + +// 沿用现有 channel-gateway tests 的 setup 模式 +describe("channel-gateway · TOPIC_CARD approval 分支", () => { + beforeEach(() => { /* reset mocks */ }); + + it("analysis 含 cardPrivateData.params.action 命中 → 调 tryHandleApprovalCallback", async () => { + // setup analyzeCardCallback 返回带 approval 数据的 analysis + // invoke gateway TOPIC_CARD listener + // 断言 tryHandleApprovalCallback 被调 + }); + + it("tryHandleApprovalCallback handled=true → 跳过 handleCardAction", async () => { + // mock tryHandleApprovalCallback 返 { handled: true } + // 断言 handleCardAction 未调 + }); + + it("tryHandleApprovalCallback handled=false → 继续 handleCardAction(feedback / btn_stop 不受影响)", async () => { + // mock tryHandleApprovalCallback 返 { handled: false } + // 断言 handleCardAction 被调 + }); + + it("无论 approval 分支是否命中、即使 approval handler 抛错,listener 的 finally 块都会通过 client.socketCallBackResponse 完成 ack", async () => { + // 断言 mocked client.socketCallBackResponse 总是被调 + }); +}); +``` + +> 沿用既有 `tests/unit/channel-gateway*.test.ts` 的 fixture 模式——**fixture 用 `client` mock(含 `socketCallBackResponse` 方法)**,**不要** `vi.mock("../../src/card-callback-service")` 去 mock 一个不存在的顶层 `socketCallBackResponse` 函数。真实 ack 路径是 `client.socketCallBackResponse(messageId, { success: true })`(参 `src/gateway/channel-gateway.ts:239` + `:337`)。 + +- [ ] **Step 20.2: 跑确认 fail** + +Run: `pnpm vitest run tests/unit/channel-gateway-approval.test.ts` +Expected: FAIL(分支未插入)。 + +- [ ] **Step 20.3: 修改 src/gateway/channel-gateway.ts** + +定位 feedback-learning block 末尾(约 `src/gateway/channel-gateway.ts:382`,`if (analysis.feedbackTarget && ...) { ... }` 的右花括号)与 `handleCardAction` 调用开始(约 `:383`)之间。 + +**先在文件顶部 import** 区域加静态 import(与 `analyzeCardCallback` / `handleCardAction` 等既有 import 同级): + +```typescript +import { tryHandleApprovalCallback } from "../approval/approval-callback-handler"; +``` + +> `src/approval/approval-callback-handler.ts` 不会反向依赖 `src/gateway/channel-gateway.ts`,无循环依赖;用静态 import 即可,不要写 `await import(...)`。 + +然后插入: + +```typescript + // -- approval 分支:feedback-learning 之后、handleCardAction 之前 -- + const approvalResult = await tryHandleApprovalCallback({ + cfg, + accountId: account.accountId, + analysis, + log: pluginLog, + }); + if (approvalResult.handled) { + // 命中即短路 —— listener 顶部的 finally { acknowledge() } 会发 ack + // 不要在这里手动调 acknowledge / client.socketCallBackResponse + return; + } + // 未命中(非 approval 按钮)继续既有 handleCardAction + const actionResult = await handleCardAction({...}); // ← 既有这一行不动 +``` + +> 实施前必读 `Read src/gateway/channel-gateway.ts:330-407`: +> - 变量名按 listener scope 实际命名(`cfg` / `account` / `analysis` / `pluginLog` / `messageId` 都已在外层 closure 中)。 +> - **listener 已有 `try { ... } finally { acknowledge(); }` 结构**(参 `channel-gateway.ts:332` 的 `const acknowledge = () => {...}` + `:404` 的 `finally { acknowledge(); }`)——approval 分支只需要 `return` 即可,**不要**手动写 `socketCallBackResponse(...)`(这变量不在当前 scope;ack 路径走 `client.socketCallBackResponse` 在 `acknowledge` helper 内)。 +> - feedback path(`channel-gateway.ts:352-382`)的 actionId 仅匹配 `feedback_up/down`,与 approval 三按钮 actionId(`allow-once/allow-always/deny`)永远不冲突;放在 feedback 之后是**显式排序**而非冲突回避(spec v3.9)。 + +- [ ] **Step 20.4: 跑确认 pass** + +Run: `pnpm vitest run tests/unit/channel-gateway-approval.test.ts` +Expected: 4 PASS。 + +- [ ] **Step 20.5: 跑既有 channel-gateway 测试无回归** + +Run: `pnpm vitest run tests/unit/channel-gateway` +Expected: 全部 PASS(feedback / btn_stop 行为不变)。 + +- [ ] **Step 20.6: Commit** + +```bash +git add src/gateway/channel-gateway.ts tests/unit/channel-gateway-approval.test.ts +git commit -m "$(cat <<'EOF' +feat(approval): channel-gateway TOPIC_CARD 接入 approval 分支 + +在 handleCardAction 之前 try approval-callback-handler;命中即 ack + return, +未命中(非 approval 按钮)走既有 feedback / btn_stop 路径。actionId 集合 +{allow-once, allow-always, deny} 与 {feedback_up, feedback_down, btn_stop} +永不冲突,所以放前后均可。 + +EOF +)" +``` + +--- + +## Task 21 · integration test approval-end-to-end(DEFERRED / follow-up) + +**Files:** +- Create: `tests/integration/approval-end-to-end.test.ts`(按 sub-task 21a-21l 分 12 次 commit) + +> spec §9.3 列出 12 个关键 integration 场景(含 v3.11 invalid-decision exec/plugin 两个)。 +> **关键约定:** 这是 integration 级——mock HTTP/auth/registry/上游 SDK,但保持 channel 内部模块(callback-handler + resolver + patcher + native-runtime)真实串联(不要 mock `src/approval/*` 内部)。 +> **TDD 严格:** 每个 sub-task 都是「写 → 跑确认 fail / pass → commit」;不允许提交空 `it(..., async () => {})`。 +> **当前分支状态:DEFERRED。** 尚未创建该 integration 文件;现有覆盖主要是各模块 unit test 加 `gateway-inbound-flow.test.ts` 的 TOPIC_CARD 路由 smoke。若严格按本文验收,Task 21 仍是合并前缺口;若先合并,本 task 应作为独立 follow-up PR 追踪。 + +### 共享 setup(Step 21.0:每个 sub-task 都依赖) + +```typescript +// tests/integration/approval-end-to-end.test.ts +import { describe, it, expect, vi, beforeEach } from "vitest"; + +// integration 测试:mock 对齐 impl 真实 subpath(Stage 0.A 表) +vi.mock("openclaw/plugin-sdk/approval-gateway-runtime", () => ({ + resolveApprovalOverGateway: vi.fn(), +})); +// updateCardVariables 真实返 Promise(不是 {ok}),失败 throw +// 不 mock socketCallBackResponse —— card-callback-service 不导出该函数(真实 ack 在 client 实例上) +vi.mock("../../src/card-callback-service", () => ({ + updateCardVariables: vi.fn().mockResolvedValue(200), + analyzeCardCallback: vi.fn(), +})); +vi.mock("../../src/send-service", () => ({ + sendProactiveTextOrMarkdown: vi.fn().mockResolvedValue({ ok: true }), +})); +vi.mock("../../src/auth", () => ({ getAccessToken: vi.fn().mockResolvedValue("tok") })); +vi.mock("../../src/config", () => ({ + getConfig: vi.fn(() => ({ clientId: "x", bypassProxyForSend: false })), +})); +vi.mock("../../src/logger-context", () => ({ + getLogger: vi.fn(() => undefined), +})); + +// 真实串联 channel 内部 approval 模块(不 mock src/approval/*) +const { tryHandleApprovalCallback } = await import("../../src/approval/approval-callback-handler"); +const { tryInterceptApproveCommand } = await import("../../src/approval/approval-command-intercept"); +const { createDingTalkApprovalNativeRuntime } = await import("../../src/approval/approval-native-runtime"); +const sdk = await import("openclaw/plugin-sdk/approval-gateway-runtime"); +const cardSvc = await import("../../src/card-callback-service"); +const sendSvc = await import("../../src/send-service"); + +const mockGateway = sdk.resolveApprovalOverGateway as ReturnType; +const mockPut = cardSvc.updateCardVariables as ReturnType; +const mockSend = sendSvc.sendProactiveTextOrMarkdown as ReturnType; + +// 通用 fixture +const baseCfg = { + channels: { + dingtalk: { + clientId: "x", clientSecret: "y", + execApprovals: { approvers: ["staffA", "staffB"] }, + }, + }, +} as never; + +const callbackAnalysis = (overrides: Record = {}) => ({ + actionId: "allow-once", + userId: "staffA", + outTrackId: "ai_card_xxx", + cardPrivateData: { + actionIds: ["allow-once"], + params: { action: "allow-once", approveId: "abc123" }, + }, + ...overrides, +}) as never; + +beforeEach(() => { + mockGateway.mockReset(); + mockPut.mockReset().mockResolvedValue(200); + mockSend.mockReset().mockResolvedValue({ ok: true }); +}); +``` + +### Step 21a · (1) Multi-approver 竞争点击 — already-resolved 第二次也刷成终态 + +- [ ] 写测试并 commit: + +```typescript +it("(1) multi-approver: 1st wins → 2nd already-resolved → applyExpiredPatch 仍调(卡片再刷一次)", async () => { + // 1st 调用:gateway 成功 + mockGateway.mockResolvedValueOnce({}); + await tryHandleApprovalCallback({ + cfg: baseCfg, accountId: "default", + analysis: callbackAnalysis({ userId: "staffA" }), + }); + expect(mockPut).toHaveBeenCalledWith( + "ai_card_xxx", + expect.objectContaining({ show_approve_btns: "false", approveId: "" }), + "tok", + expect.objectContaining({ clientId: "x" }), + ); + mockPut.mockClear(); + + // 2nd 调用:gateway 抛 APPROVAL_ALREADY_RESOLVED → applyExpiredPatch + mockGateway.mockRejectedValueOnce(Object.assign(new Error("already"), { + gatewayCode: "APPROVAL_ALREADY_RESOLVED", + })); + await tryHandleApprovalCallback({ + cfg: baseCfg, accountId: "default", + analysis: callbackAnalysis({ userId: "staffB" }), + }); + expect(mockPut).toHaveBeenCalledWith( + "ai_card_xxx", + expect.objectContaining({ show_approve_btns: "false", approveId: "" }), + "tok", + expect.objectContaining({ clientId: "x" }), + ); +}); +``` + +Run: `pnpm vitest run tests/integration/approval-end-to-end.test.ts -t "(1) multi-approver"` → PASS。 +Commit: `test(approval): integration (1) multi-approver 竞争点击`。 + +### Step 21b · (2) Self-approval in DM + +```typescript +it("(2) self-approval in DM: approver 自己点 → ok=true → applyResolvedPatch", async () => { + mockGateway.mockResolvedValue({}); + await tryHandleApprovalCallback({ + cfg: baseCfg, accountId: "default", + analysis: callbackAnalysis({ userId: "staffA" }), + }); + expect(mockGateway).toHaveBeenCalledWith(expect.objectContaining({ + approvalId: "abc123", decision: "allow-once", senderId: "staffA", + })); + expect(mockSend).not.toHaveBeenCalled(); // 不私聊 +}); +``` + +### Step 21c · (3) 非 approver 点击 + +```typescript +it("(3) 非 approver 点击 → 私聊拒绝 + 不调 patcher(卡片不变)", async () => { + await tryHandleApprovalCallback({ + cfg: baseCfg, accountId: "default", + analysis: callbackAnalysis({ userId: "outsider" }), + }); + expect(mockGateway).not.toHaveBeenCalled(); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:outsider", + expect.stringContaining("无权"), + expect.objectContaining({ forceMarkdown: true }), + ); + expect(mockPut).not.toHaveBeenCalled(); +}); +``` + +### Step 21d · (4) 上游过期事件 → updateEntry phase=expired + +```typescript +it("(4) 上游 expired event → transport.updateEntry({phase:'expired'}) → applyExpiredPatch", async () => { + const runtime = createDingTalkApprovalNativeRuntime(); + await runtime.transport.updateEntry({ + cfg: baseCfg, accountId: "default", + entry: { mode: "card", outTrackId: "ot1", approvalId: "abc" } as never, + payload: {} as never, + phase: "expired" as never, + } as never); + expect(mockPut).toHaveBeenCalledWith( + "ot1", + expect.objectContaining({ show_approve_btns: "false", approveId: "" }), + "tok", + expect.objectContaining({ clientId: "x" }), + ); +}); +``` + +### Step 21e · (5) Card patch 明确失败 → 降级 markdown + +```typescript +it("(5) card 路径 HTTP 400 → 降级 markdown,entry.mode='markdown'", async () => { + // 让 first PUT 抛 HTTP 400(明确失败) + mockPut.mockRejectedValueOnce(Object.assign(new Error("400"), { status: 400 })); + const runtime = createDingTalkApprovalNativeRuntime(); + const entry = await runtime.transport.deliverPending({ + cfg: baseCfg, accountId: "default", + preparedTarget: { route: "card", activeCardOutTrackId: "ot1", target: { to: "group:c" } }, + request: { id: "abc", createdAtMs: 0, expiresAtMs: 0, request: { sessionKey: "s1", turnSourceChannel: "dingtalk", turnSourceTo: "group:c" } }, + pendingPayload: { approvalId: "abc", markdownText: "md-payload" }, + } as never); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "group:c", "md-payload", + expect.objectContaining({ forceMarkdown: true }), + ); + expect(entry?.mode).toBe("markdown"); +}); +``` + +### Step 21f · (6) Card patch 模糊失败 → return null + +```typescript +it("(6) card 路径 ETIMEDOUT → return null,不调 sendProactiveTextOrMarkdown", async () => { + mockPut.mockRejectedValueOnce(Object.assign(new Error("timeout"), { code: "ETIMEDOUT" })); + const runtime = createDingTalkApprovalNativeRuntime(); + const entry = await runtime.transport.deliverPending({ + cfg: baseCfg, accountId: "default", + preparedTarget: { route: "card", activeCardOutTrackId: "ot1", target: { to: "group:c" } }, + request: { id: "abc", createdAtMs: 0, expiresAtMs: 0, request: { sessionKey: "s1", turnSourceChannel: "dingtalk", turnSourceTo: "group:c" } }, + pendingPayload: { approvalId: "abc", markdownText: "md" }, + } as never); + expect(entry).toBeNull(); + expect(mockSend).not.toHaveBeenCalled(); +}); +``` + +### Step 21g · (7) /approve 命令路径绕过 session lock + +```typescript +it("(7) /approve 命令 → 调 resolveApprovalOverGateway,不触发 reply 派发", async () => { + mockGateway.mockResolvedValue({}); + const intercepted = await tryInterceptApproveCommand({ + cfg: baseCfg, accountId: "default", + text: "/approve abc once", senderId: "staffA", + }); + expect(intercepted).toBe(true); + expect(mockGateway).toHaveBeenCalledWith(expect.objectContaining({ + approvalId: "abc", decision: "allow-once", senderId: "staffA", + })); +}); +``` + +### Step 21h · (8) 未配置 approvers + +```typescript +it("(8) 未配置 approvers → availability.shouldHandle=false", async () => { + const cfgNoApprovers = { channels: { dingtalk: { clientId: "x", clientSecret: "y" } } } as never; + const runtime = createDingTalkApprovalNativeRuntime(); + expect(runtime.availability.shouldHandle({ + cfg: cfgNoApprovers, accountId: "default", + request: { id: "abc", request: { turnSourceChannel: "dingtalk", turnSourceTo: "group:c" } } as never, + })).toBe(false); +}); +``` + +### Step 21i · (9) CLI 触发 turnSourceChannel != dingtalk + +```typescript +it("(9) turnSourceChannel=CLI → shouldHandle=false", async () => { + const runtime = createDingTalkApprovalNativeRuntime(); + expect(runtime.availability.shouldHandle({ + cfg: baseCfg, accountId: "default", + request: { id: "abc", request: { turnSourceChannel: "codex-cli", turnSourceTo: "group:c" } } as never, + })).toBe(false); +}); +``` + +### Step 21j · (10) Channel 重启后旧按钮 not-found + +```typescript +it("(10) gateway 抛 APPROVAL_NOT_FOUND → applyExpiredPatch(三变量 PUT,无终态文字)", async () => { + mockGateway.mockRejectedValue(Object.assign(new Error("nf"), { gatewayCode: "APPROVAL_NOT_FOUND" })); + await tryHandleApprovalCallback({ + cfg: baseCfg, accountId: "default", + analysis: callbackAnalysis({ userId: "staffA" }), + }); + const vars = mockPut.mock.calls[0][1] as Record; + expect(vars).toEqual(expect.objectContaining({ + show_approve_btns: "false", approveId: "", + })); + expect(vars).not.toHaveProperty("status"); + expect(vars).not.toHaveProperty("statusFooter"); + expect(vars).not.toHaveProperty("approval_status"); +}); +``` + +### Step 21k · (11) Invalid-decision exec (allow-always unavailable) + +```typescript +it("(11) exec invalid-decision (APPROVAL_ALLOW_ALWAYS_UNAVAILABLE) → 不 patch + 私聊重选", async () => { + mockGateway.mockRejectedValue(Object.assign(new Error("ad"), { + gatewayCode: "INVALID_REQUEST", + details: { reason: "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" }, + })); + await tryHandleApprovalCallback({ + cfg: baseCfg, accountId: "default", + analysis: callbackAnalysis({ + userId: "staffA", + actionId: "allow-always", + cardPrivateData: { actionIds: ["allow-always"], params: { action: "allow-always", approveId: "abc123" } }, + }), + }); + expect(mockPut).not.toHaveBeenCalled(); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:staffA", + expect.stringContaining("不支持 allow-always"), + expect.objectContaining({ forceMarkdown: true }), + ); +}); +``` + +### Step 21l · (12) Invalid-decision plugin (allowedDecisions=[...]) + +```typescript +it("(12) plugin invalid-decision (allowedDecisions=['allow-once']) → 私聊含 allowedDecisions 文案", async () => { + mockGateway.mockRejectedValue(Object.assign(new Error("ad"), { + gatewayCode: "INVALID_REQUEST", + details: { allowedDecisions: ["allow-once"] }, + })); + await tryHandleApprovalCallback({ + cfg: baseCfg, accountId: "default", + analysis: callbackAnalysis({ + userId: "staffA", + actionId: "allow-always", + cardPrivateData: { + actionIds: ["allow-always"], + params: { action: "allow-always", approveId: "plugin:xyz789" }, + }, + }), + }); + expect(mockPut).not.toHaveBeenCalled(); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), "user:staffA", + expect.stringContaining("allow-once"), + expect.objectContaining({ forceMarkdown: true }), + ); +}); +``` + +### Step 21.M · 总结 commit + +- [ ] 跑全部 integration 测试 + +Run: `pnpm vitest run tests/integration/approval-end-to-end.test.ts` +Expected: 12 PASS。 + +- [ ] 跑覆盖率确认达标 + +Run: `pnpm test:coverage` +Expected: `src/approval/*` line ≥ 90%, branch ≥ 85%;仓库整体 coverage 不下降。 + +> 如 Step 21a-21l 已逐个 commit 完毕,此步无需再 commit;否则可一次性 commit: + +```bash +git add tests/integration/approval-end-to-end.test.ts +git commit -m "$(cat <<'EOF' +test(approval): integration end-to-end 覆盖 12 关键场景 + +参 spec §9.3 + v3.11/v3.12 invalid-decision 场景:multi-approver 竞争、 +self-approval、非 approver 拒绝、上游过期、card 失败降级 markdown、模糊 +失败不重发、/approve 命令链路、未配 approvers、CLI 触发、channel 重启 +not-found 降级 expired、exec/plugin invalid-decision 私聊重选。 +EOF +)" +``` + +--- + +## Task 22 · 真机回归(PR-2 最终验证) + +**Files:** 无代码改动;产出回归记录 `docs/artifacts/2026-05-19-approval-real-device-regression.md` + +> 参 `skills/dingtalk-real-device-testing/SKILL.md`。**这一步不能跳过 —— PR-2 是真机回归 PR**。 +> **当前分支状态:** 尚未产出该回归记录;若没有真实钉钉环境,本项应在 PR 验证 TODO 中明确标注为未执行,而不是写成已通过。 + +- [ ] **Step 22.1: 准备真机环境** + +```bash +pnpm run build:runtime +openclaw gateway restart # 用户手动跑 +``` + +确认日志显示加载新的 `dist/index.js`。 + +- [ ] **Step 22.2: 真机回归 checklist(参 spec §10 阶段 2)** + +依次在钉钉群里跑: + +- [ ] (a) Agent 触发 exec approval(如 `docker image prune`)→ AI Card 出现 + **底部出现 3 个 approval 按钮** + btn_stop 暂时隐藏(show_approve_btns=true, hasAction=false 生效) +- [ ] (b) approver 点 "允许一次" → approval 按钮消失 + agent 继续 stream + btn_stop 恢复(show_approve_btns=false, approveId="", hasAction=true) +- [ ] (c) approver 点 "拒绝" → 按钮消失 + agent terminated(按钮变化与允许一致;agent 行为由上游决定) +- [ ] (d) 非 approver 点按钮 → 收到 `⛔ 你不在 approver 名单` 私聊 + **卡片不变**(按钮仍在) +- [ ] (e) approver 敲 `/approve allow-once` 命令 → 等同点按钮(含群里 `@bot /approve ...` 形式) +- [ ] (f) callback payload 真机抓包确认含 `cardPrivateData.params.approveId` 字段(与 PUT 时一致) +- [ ] (g) markdown 模式(messageType=markdown)触发 approval → 群里出现独立 markdown 消息含 `/approve ` 三命令模板 +- [ ] (h) Agent FINISHED 后旧按钮点 → 立即变 expired(按钮消失 + 卡片其余内容不变) +- [ ] (i) 同 approval 多 approver 同时点 → 第一个成功 + 第二个看到按钮立刻消失(already-resolved 走 applyExpiredPatch) +- [ ] (j) 用户点了 request 不允许的 decision(如 ask=always 时点 allow-always)→ 收到私聊重选提示 + **按钮保持 pending 可再点** + +- [ ] **Step 22.3: 产出回归记录** + +写入 `docs/artifacts/2026-05-19-approval-real-device-regression.md`,列每个 checklist 的实际行为 + 截图(可选)+ 任何 anomaly。模板参既有 v2 卡片真机回归记录(如 `docs/artifacts/` 内现存文件)。 + +- [ ] **Step 22.4: Commit 回归记录** + +```bash +git add docs/artifacts/2026-05-19-approval-real-device-regression.md +git commit -m "$(cat <<'EOF' +docs(artifacts): 添加 Gap #01 approval PR-2 真机回归记录 + +10 项 checklist 全部 PASS(card 双路由 + 非 approver 拒绝 + /approve +命令 + markdown 路径 + already-resolved + invalid-decision 重选)。 + +EOF +)" +``` + +--- + +## PR-2 收尾 + +- [ ] **Step PR2.1: 跑完整测试套件 + coverage** + +```bash +pnpm run type-check && pnpm run lint && pnpm test && pnpm test:coverage +``` + +Expected: 全部 PASS;`src/approval/*` line ≥ 90%, branch ≥ 85%。 + +- [ ] **Step PR2.2: 开 PR-2(基于 PR-1 已 merge 的 main)** + +```bash +git push -u origin # 若用 feature 分支 +gh pr create --title "feat(approval): PR-2 native runtime + v3 模板替换 + 真机回归" --body "$(cat <<'EOF' +## Summary +- 模板 ID 从 v2 替换为 v3(含 approve_btns/show_approve_btns/approveId);createAICard cardParamMap 默认值修正 +- card-callback-service 暴露 cardPrivateData(D16 BLOCKER) +- card-run-registry 加 pendingApprovalId fallback(D24) +- approval-card-patcher 三个 patcher 与 §1.X 单一事实表 1:1 +- approval-markdown-render markdown 主路径文案 +- approval-callback-handler 主链路 + fallback 解码 + 5 reason 分支 +- approval-native-runtime 4 子 adapter(含 card 失败降级 markdown 策略) +- channel-gateway TOPIC_CARD 接入 approval 分支 +- 真机回归 PASS + +## PR boundaries +- 不含用户文档(PR-3) +- 不含 release notes / README(PR-3) + +## Test plan +- [x] `pnpm test` 全部 PASS +- [x] coverage src/approval ≥ 90% line / 85% branch +- [x] 真机 10 项 checklist 全部 PASS(参 docs/artifacts/...) + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +--- + +# PR-3 · 用户文档与回归收尾 + +**交付目标:** feature 正式 production-ready。 + +**PR-3 任务清单:** Task 23 ~ Task 24。 + +--- + +## Task 23 · 用户文档 docs/user/features/exec-approval.md + +**Files:** +- Create: `docs/user/features/exec-approval.md` + +- [ ] **Step 23.1: 写文档** + +按 spec §4.2 配置 schema + §6 数据流 + §7 mockup + §11.1 limitations 整理用户视角文档。结构: + +```markdown +# DingTalk Exec / Plugin Approval + +## 概述 +(产品价值 1 段) + +## 启用方法 +(最小可用 yaml 配置:channels.dingtalk.execApprovals.approvers 一项即可) + +## 配置 schema +(4.2 schema 完整版 + 字段说明 + 多账号 override) + +## 交互方式 +### 按钮路径(card 模式) +(mockup 截图 + 三按钮含义) +### 命令路径(/approve) +(10 alias × 2 顺序 = 20 形式 + 私聊提示样本) + +## 真机 FAQ +- 为什么我点了按钮但没反应? → 检查 approver 名单 +- 为什么 agent reply 卡片底部一直显示 3 个按钮? → 模板未替换 v3 或 cardParamMap 默认值缺失 +- /approve 命令格式提示 → 列 20 合法形式 +- invalid-decision 提示重选 → 上游 ExecApprovalRequest.allowedDecisions 限制 + +## v1 已知 limitation +(搬 spec §11.1:approver-DM 推迟、终态文字位、allowedDecisions 不动态隐藏、finalize-on-stop 推迟) + +## v2 future(不在 v1 范围) +(搬 spec §10 "v2 future") +``` + +- [ ] **Step 23.2: Commit** + +```bash +git add docs/user/features/exec-approval.md +git commit -m "$(cat <<'EOF' +docs(user): 添加 DingTalk exec / plugin approval 用户配置指南 + +覆盖启用步骤、schema、按钮 / /approve 命令路径、FAQ、v1 limitation。 +对齐 spec v3.12 §4.2 / §6 / §11.1。 + +EOF +)" +``` + +--- + +## Task 24 · README + release notes(BREAKING 标注) + +**Files:** +- Modify: `README.md` +- Create: `docs/releases/v3.6.4.md`(参既有 `docs/releases/v3.5.3.md` 结构) +- Modify: `docs/releases/latest.md`(更新 include target) +- Modify: `docs/releases/index.md`(追加 v3.6.4 入口) + +> ⚠️ 本仓库**没有** `CHANGELOG.md`——release notes 在 `docs/releases/` 目录每版一个 markdown 文件,`latest.md` 用 `` 指向最新版本。 + +- [ ] **Step 24.1: README 加 approval feature 段** + +在 README features section 加: + +```markdown +## DingTalk Native Approval(Gap #01 · v3.6.4+) + +- Exec / Plugin approval 在钉钉群 AI Card 上挂 3 按钮原生体验 +- 或敲 `/approve ` 命令完成审批 +- approver 名单配置见 [docs/user/features/exec-approval.md](docs/user/features/exec-approval.md) +- 详细设计:[docs/features/2026-05-18-gap-01-approval-native-design.html](docs/features/2026-05-18-gap-01-approval-native-design.html)(v3.12) +``` + +- [ ] **Step 24.2: 新建 docs/releases/v3.6.4.md(参 v3.5.3.md 模板)** + +```markdown +# v3.6.4 发布说明 + +本次发布聚焦一件大事:**DingTalk Native Approval (Gap #01)** —— 在钉钉群把 OpenClaw 的 exec / plugin 审批以原生 3 按钮 + `/approve` 命令双轨方式落地。 + +**最新版本入口**:[`latest.md`](./latest.md) + +> [!IMPORTANT] +> **BREAKING:openclaw peerDependency 升级到 `>=2026.4.7`** +> 老版本(2026.3.28)不再支持。升级前请确认 OpenClaw 主程序版本。 + +## ✨ 功能 + +### 1. DingTalk Native Approval + +* exec / plugin approval 在钉钉群 AI Card 底部挂 3 按钮(允许一次 / 总是允许 / 拒绝) +* 也支持文本命令 `/approve `(10 alias × 2 顺序 = 20 合法形式,对齐上游) +* approver 名单配置:`channels.dingtalk.execApprovals.approvers` 或 `commands.ownerAllowFrom` fallback +* 详细配置 + FAQ + 限制:[`docs/user/features/exec-approval.md`](../user/features/exec-approval.md) +* 设计文档:[`docs/features/2026-05-18-gap-01-approval-native-design.html`](../features/2026-05-18-gap-01-approval-native-design.html) (v3.12) + +### 2. 5 类 approval 错误分类 + +`unauthorized` / `not-found` / `already-resolved` / `invalid-decision` / `gateway-error`。`invalid-decision` 与 `gateway-error` 时按钮保留可再点;前者提示重选,后者提示稍后重试,避免用户卡死或瞬时网关失败误关有效审批。 + +## 🛠 内部改动 + +* 新增 `src/approval/` 域目录(10 个模块,~900 行业务 + ~2000 行测试) +* AI Card 模板从 v2 升级到 v3(含 `approve_btns` / `show_approve_btns` / `approveId` 三变量) +* `src/card/card-run-registry.ts` 新增 `resolveActiveCardRunBySession` / `isActiveCardRun` / `pendingApprovalId` +* `src/card-callback-service.ts` `CardCallbackAnalysis` 暴露 `cardPrivateData` + +## ⚠️ 升级步骤 + +1. 升级 OpenClaw 主程序到 `>=2026.4.7` +2. 在 `channels.dingtalk.execApprovals.approvers` 配置 approver staffId 列表(详见上述用户文档) +3. AI Card 模板会自动用 v3(默认 templateId 已替换;可用 env `DINGTALK_CARD_TEMPLATE_ID` 覆盖回 v2) + +## 🤝 贡献者 + +* @soimy(设计 + 实施) +``` + +- [ ] **Step 24.3: 更新 docs/releases/latest.md 的 include target** + +```markdown + + +``` + +- [ ] **Step 24.4: 更新 docs/releases/index.md** + +按既有结构在版本列表顶部追加 v3.6.4 条目(不知道具体格式时先 `Read docs/releases/index.md` 比对)。 + +- [ ] **Step 24.5: Commit** + +```bash +git add README.md docs/releases/v3.6.4.md docs/releases/latest.md docs/releases/index.md +git commit -m "$(cat <<'EOF' +docs(release): v3.6.4 release notes 含 Gap #01 + peer BREAKING + +新增 docs/releases/v3.6.4.md(参 v3.5.3.md 模板); +latest.md include target 切换; +index.md 追加 v3.6.4 入口; +README 加 approval feature 段引导到 docs/user/features/exec-approval.md。 +EOF +)" +``` + +--- + +## PR-3 收尾 + +- [ ] **Step PR3.1: 开 PR** + +```bash +gh pr create --title "docs(approval): PR-3 用户文档 + release notes" --body "$(cat <<'EOF' +## Summary +- docs/user/features/exec-approval.md 启用 + schema + 交互 + FAQ + limitation +- README features 段 + docs/releases/v3.6.4.md release notes(含 peerDep BREAKING 标注) + +## Test plan +- [x] markdown 渲染检查(GitHub preview) +- [x] 文档示例可复制运行 + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +PR-3 merge 后 Gap #01 正式 production-ready。 + +--- + +# 附录 A · 与 Spec 的映射 + +| Spec 锚点 | 实施 Task | +|---|---| +| §1.X 单一事实表 | Task 15(patcher)+ Task 17(callback handler)+ Task 18(runtime) | +| §2 D1 实现范围 4 子 adapter | Task 18 | +| §2 D2 /approve early intercept | Task 9 + Task 11 | +| §2 D4 v1 origin-only | Task 5 + Task 10 + Task 18 | +| §2 D7 approver schema | Task 1 + Task 3 | +| §2 D9 v3 模板 | Task 12 | +| §2 D10 双路由(不读 messageType) | Task 8 + Task 18 | +| §2 D11/D12/D13 TTL/重启/停机 | spec 决策 = v1 不做(无实施 task) | +| §2 D14 终态展示(v1 不写文字) | Task 15(patcher 不 PUT status) | +| §2 D15 按钮 payload 编码 | 模板已发布;Task 12 替换 ID | +| §2 D16 cardPrivateData 扩展 | Task 13 | +| §2 D17 SDK 基线 | Stage 0 | +| §2 D18 无本地 store | 整体设计(无实施 task) | +| §2 D20 单点 resolver | Task 6 | +| §2 D21 kind 推导 | Task 6(deriveResolveMethod) | +| §2 D22 agent-card-coalesce | Task 8(locator)+ Task 18(runtime.prepareTarget) | +| §2 D23 btn_stop 与 approval 共存 | Task 15(hasAction toggle 字段) | +| §2 D24 approveId 主链路 + fallback | Task 14 + Task 15 + Task 17 | +| §3.3 接触面表(含 v3.11 config.ts 修订) | Task 2 | +| §5 Sub-Adapter | Task 18 | +| §6.3 callback 数据流 + 5 reason | Task 17 | +| §6.4 entry.mode 分支 updateEntry | Task 18 | +| §6.6 失败 / 边界(重启 / 过期 / multi-approver) | Task 21(integration) | +| §6.7 失败处理(card 失败降级 markdown) | Task 18 | +| §6.8 /approve early intercept | Task 9 + Task 11 | +| §7.1 v1 终态文字限制 | Task 15(patcher 不 PUT 终态文字) | +| §8 错误矩阵(含 v3.11 invalid-decision) | Task 6 + Task 17 + Task 21 | +| §9 测试矩阵 | 所有 Task 的测试 + Task 21 | +| §10 阶段 0 / 1 / 2 / 3 | Stage 0 + PR-1 + PR-2 + PR-3 | +| §11.1 v1 limitation | Task 23 文档化 | +| §11.2 风险 | Task 23 FAQ + 真机回归 | + +--- + +# 附录 B · 关键不可跳过的真机抽检节点 + +| Step | 原因 | +|---|---| +| Step 12.7 | 模板 ID 替换后必须确认非 approval 消息**不**显示 approval 按钮(默认值生效) | +| Step 22.2 (a-j) | PR-2 真机回归 10 项 checklist —— 唯一能 catch 模板字段错配 / 平台行为偏差 | +| PR1.2 | PR-1 /approve 通道生效抽检 —— 命令路径必须验证不死锁 | + +--- + +# 附录 C · 实施时的 grep 安全清单 + +每完成一个 Task 跑: + +```bash +# 1. 不留 TODO / FIXME / placeholder +git grep -n "TODO\|FIXME\|XXX" src/approval/ tests/ + +# 2. 不留 @ts-ignore +git grep -n "@ts-ignore" src/approval/ tests/ + +# 3. 不留 console.*(用 getLogger()?.info / warn / error) +git grep -nE "console\." src/approval/ + +# 4. 所有 sendProactiveTextOrMarkdown 调用都带 forceMarkdown:true +git grep -n "sendProactiveTextOrMarkdown" src/approval/ + +# 5. v3 模板 ID 全局一致 +git grep -n "templateId\|TEMPLATE_ID" src/ docs/ + +# 6. patcher 调用都通过 approval-card-patcher,不绕过它直接调 updateCardVariables +git grep -n "updateCardVariables" src/approval/ # 应只在 approval-card-patcher.ts 出现 + +# 7. approve regex 没漂移回 \b(必须严格用 (?:\s|$) 与上游 commands-approve.ts:16 对齐) +git grep -nE '\^\\/\?approve\\b' src/ tests/ # 应为空;若有命中说明 regex 写错 +``` + +--- + +**Plan 总结:** 1 个 stage(SDK 基线)+ 11 个 PR-1 task + 11 个 PR-2 task + 2 个 PR-3 task = 25 个 task,约 120 个 step。预计实施工时 8-12 工作日(含真机回归)。所有 task 都遵循 TDD + 频繁 commit;PR 边界处停下来等用户 review。 diff --git a/src/approval/approval-callback-handler.ts b/src/approval/approval-callback-handler.ts index 4d525397..68181ed5 100644 --- a/src/approval/approval-callback-handler.ts +++ b/src/approval/approval-callback-handler.ts @@ -55,6 +55,20 @@ function resolveApprovalId(analysis: CardCallbackAnalysis): string | null { return resolveCardRun(analysis.outTrackId)?.pendingApprovalId ?? null; } +async function patchCardBestEffort(params: { + dtConfig: ReturnType; + log?: Logger; + failureMessage: string; + patch: (token: string) => Promise; +}): Promise { + try { + const token = await getAccessToken(params.dtConfig, params.log); + await params.patch(token); + } catch (error) { + params.log?.warn?.(`${params.failureMessage}: ${String(error)}`); + } +} + async function sendPrivateHint(params: { cfg: OpenClawConfig; accountId: string; @@ -88,14 +102,19 @@ export async function tryHandleApprovalCallback( } const dtConfig = getConfig(input.cfg, input.accountId); - const token = await getAccessToken(dtConfig, input.log); const run = resolveCardRun(input.analysis.outTrackId); const cardStillActive = run ? isActiveCardRun(run) : false; const approvalId = resolveApprovalId(input.analysis); if (!approvalId) { - await applyExpiredPatch(input.analysis.outTrackId, token, cardStillActive, dtConfig).catch((error) => { - input.log?.warn?.(`[DingTalk][Approval] Failed to expire callback without approvalId: ${String(error)}`); + if (!isApprovalDecision(input.analysis.cardPrivateData?.params?.action)) { + return { handled: false }; + } + await patchCardBestEffort({ + dtConfig, + log: input.log, + failureMessage: "[DingTalk][Approval] Failed to expire callback without approvalId", + patch: (token) => applyExpiredPatch(input.analysis.outTrackId!, token, cardStillActive, dtConfig), }); return { handled: true, reason: "missing-approval-id" }; } @@ -110,14 +129,18 @@ export async function tryHandleApprovalCallback( }); if (result.ok) { - await applyResolvedPatch( - input.analysis.outTrackId, - decision, - token, - cardStillActive, + await patchCardBestEffort({ dtConfig, - ).catch((error) => { - input.log?.warn?.(`[DingTalk][Approval] Failed to patch resolved card: ${String(error)}`); + log: input.log, + failureMessage: "[DingTalk][Approval] Failed to patch resolved card", + patch: (token) => + applyResolvedPatch( + input.analysis.outTrackId!, + decision, + token, + cardStillActive, + dtConfig, + ), }); return { handled: true, reason: "resolved" }; } @@ -147,8 +170,22 @@ export async function tryHandleApprovalCallback( return { handled: true, reason: result.reason }; } - await applyExpiredPatch(input.analysis.outTrackId, token, cardStillActive, dtConfig).catch((error) => { - input.log?.warn?.(`[DingTalk][Approval] Failed to patch expired card: ${String(error)}`); + if (result.reason === "gateway-error") { + await sendPrivateHint({ + cfg: input.cfg, + accountId: input.accountId, + userId: input.analysis.userId, + text: `ℹ️ 审批暂时处理失败,请稍后重试(${approvalId})。`, + log: input.log, + }); + return { handled: true, reason: result.reason }; + } + + await patchCardBestEffort({ + dtConfig, + log: input.log, + failureMessage: "[DingTalk][Approval] Failed to patch expired card", + patch: (token) => applyExpiredPatch(input.analysis.outTrackId!, token, cardStillActive, dtConfig), }); return { handled: true, reason: result.reason }; } diff --git a/src/approval/approval-command-intercept.ts b/src/approval/approval-command-intercept.ts index eca46a93..3d4a1ffd 100644 --- a/src/approval/approval-command-intercept.ts +++ b/src/approval/approval-command-intercept.ts @@ -81,6 +81,8 @@ export async function tryInterceptApproveCommand( ); } else if (result.reason === "not-found" || result.reason === "already-resolved") { await sendDirectHint(input, `ℹ️ 审批 ${parsed.approvalId} 已处理或已过期,无需再次操作。`); + } else if (result.reason === "gateway-error") { + await sendDirectHint(input, `ℹ️ 审批 ${parsed.approvalId} 暂时处理失败,请稍后重试。`); } input.log?.info?.(`[DingTalk][Approval] /approve resolver returned ${result.reason}`); diff --git a/src/approval/approval-resolver.ts b/src/approval/approval-resolver.ts index 2c4fa726..107d254b 100644 --- a/src/approval/approval-resolver.ts +++ b/src/approval/approval-resolver.ts @@ -78,6 +78,8 @@ function deriveGatewayParams(params: { if (params.execAuthorized && params.pluginAuthorized) { return { allowPluginFallback: true }; } + // Reserved for v2 if plugin approvers diverge from exec approvers. + // In v1, isPluginAuthorizedSender intentionally aliases isExecAuthorizedSender. if (params.pluginAuthorized) { return { resolveMethod: "plugin" }; } diff --git a/tests/unit/approval-callback-handler.test.ts b/tests/unit/approval-callback-handler.test.ts index a56242b0..3d573d14 100644 --- a/tests/unit/approval-callback-handler.test.ts +++ b/tests/unit/approval-callback-handler.test.ts @@ -123,6 +123,21 @@ describe("approval-callback-handler", () => { expect(mockResolve).not.toHaveBeenCalled(); }); + it("does not claim generic decision actionIds without an approval id", async () => { + mockResolveCard.mockReturnValue(null); + + const result = await tryHandleApprovalCallback({ + ...base, + analysis: analysis({ + cardPrivateData: { actionIds: ["deny"], params: {} }, + }), + }); + + expect(result).toEqual({ handled: false }); + expect(mockResolve).not.toHaveBeenCalled(); + expect(mockApplyExpired).not.toHaveBeenCalled(); + }); + it("uses action id fallback for decision", async () => { await tryHandleApprovalCallback({ ...base, @@ -174,7 +189,7 @@ describe("approval-callback-handler", () => { expect(mockSend).not.toHaveBeenCalled(); }); - it.each(["already-resolved", "not-found", "gateway-error"] as const)( + it.each(["already-resolved", "not-found"] as const)( "expires the card for %s", async (reason) => { mockResolve.mockResolvedValue({ ok: false, reason }); @@ -184,4 +199,32 @@ describe("approval-callback-handler", () => { expect(mockApplyExpired).toHaveBeenCalled(); }, ); + + it("keeps card pending and sends a private retry hint for gateway errors", async () => { + mockResolve.mockResolvedValue({ ok: false, reason: "gateway-error" }); + + await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:staffA", + expect.stringContaining("稍后重试"), + expect.objectContaining({ forceMarkdown: true }), + ); + expect(mockApplyResolved).not.toHaveBeenCalled(); + expect(mockApplyExpired).not.toHaveBeenCalled(); + }); + + it("resolves upstream approval even when DingTalk token lookup fails", async () => { + mockResolve.mockResolvedValue({ ok: true }); + mockGetAccessToken.mockRejectedValueOnce(new Error("token unavailable")); + + const result = await tryHandleApprovalCallback({ ...base, analysis: analysis() }); + + expect(result).toEqual({ handled: true, reason: "resolved" }); + expect(mockResolve).toHaveBeenCalledWith( + expect.objectContaining({ approvalId: "abc123", decision: "allow-once" }), + ); + expect(mockApplyResolved).not.toHaveBeenCalled(); + }); }); diff --git a/tests/unit/approval-command-intercept.test.ts b/tests/unit/approval-command-intercept.test.ts index 28243586..96942626 100644 --- a/tests/unit/approval-command-intercept.test.ts +++ b/tests/unit/approval-command-intercept.test.ts @@ -132,12 +132,17 @@ describe("tryInterceptApproveCommand", () => { ); }); - it("does not DM for gateway errors", async () => { + it("sends a retry DM for gateway errors", async () => { mockResolveApproval.mockResolvedValue({ ok: false, reason: "gateway-error" }); await tryInterceptApproveCommand({ ...base, text: "/approve abc deny" }); - expect(mockSend).not.toHaveBeenCalled(); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:staffA", + expect.stringMatching(/暂时处理失败.*稍后重试/), + expect.objectContaining({ forceMarkdown: true }), + ); }); it("does not throw when sending a DM fails", async () => { From 71bfca48943168dd345312d63625571df83367b7 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 16:54:30 +0800 Subject: [PATCH 31/44] chore(deps): raise openclaw peer baseline to 2026.5.7 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous baseline `>=2026.4.7` was a false floor: this PR's code uses three upstream SDK APIs that landed after 2026.4.7. Aligning the declared peer floor with the actual minimum compatible version, and pinning the locked install via `pnpm.overrides` so a fresh `pnpm install` resolves to that floor instead of drifting to whichever 2026.5.x is latest on npm. Real minimum derived from API availability: - `"presentation"` channel-message capability — added in 2026.4.21 (openclaw `fd0970c077 refactor(channels): decouple presentation rendering`) - `createChannelNativeOriginTargetResolver({ normalizeTarget })` option — added in 2026.4.27 (`e27c32b9b0 refactor(plugin-sdk): publish route helpers`) - `PluginApprovalRequestPayload.allowedDecisions` field — added in 2026.5.7 (`f011d6bc0a Fix repeated Codex native approval prompts after allow-always`) max(4.21, 4.27, 5.7) = **2026.5.7**. Changes: - `package.json`: `peerDependencies.openclaw`, `openclaw.compat.pluginApi`, `openclaw.build.openclawVersion`, `openclaw.install.minHostVersion` all raised to `2026.5.7`; added `pnpm.overrides.openclaw: "2026.5.7"`. - `pnpm-lock.yaml`: installed `openclaw@2026.5.7` (was 2026.5.18 on the earlier commit `44afa9a`, which let the lockfile drift past the declared floor). - `tests/unit/plugin-manifest.test.ts` + `tests/unit/sdk-import-structure.test.ts`: hard-coded `2026.4.7` strings synced to `2026.5.7`. Note: this commit does not yet make CI green. `openclaw@2026.5.x` pulls `undici@8.x`, which declares `engines.node: ">=22.19.0"` and uses `webidl.util.markAsUncloneable`; CI is still on Node 20. Bumping the CI workflow Node version is a follow-up. --- package.json | 13 +- pnpm-lock.yaml | 2330 ++++++++++++++++++----- tests/unit/plugin-manifest.test.ts | 8 +- tests/unit/sdk-import-structure.test.ts | 4 +- 4 files changed, 1822 insertions(+), 533 deletions(-) diff --git a/package.json b/package.json index 32a4b81e..f1cfeb6a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,11 @@ ], "type": "module", "packageManager": "pnpm@10.33.0", + "pnpm": { + "overrides": { + "openclaw": "2026.5.7" + } + }, "main": "./dist/index.js", "types": "./dist/index.d.ts", "exports": { @@ -82,7 +87,7 @@ "vitest": "^3.2.4" }, "peerDependencies": { - "openclaw": ">=2026.4.7" + "openclaw": ">=2026.5.7" }, "peerDependenciesMeta": { "openclaw": { @@ -91,10 +96,10 @@ }, "openclaw": { "compat": { - "pluginApi": ">=2026.4.7" + "pluginApi": ">=2026.5.7" }, "build": { - "openclawVersion": "2026.4.7" + "openclawVersion": "2026.5.7" }, "extensions": [ "./index.ts" @@ -120,7 +125,7 @@ ] }, "install": { - "minHostVersion": ">=2026.4.7", + "minHostVersion": ">=2026.5.7", "npmSpec": "@soimy/dingtalk", "localPath": ".", "defaultChoice": "npm" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5a29a5c..4859f893 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,9 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +overrides: + openclaw: 2026.5.7 + importers: .: @@ -21,8 +24,8 @@ importers: specifier: ^1.12.0 version: 1.12.0 openclaw: - specifier: '>=2026.4.7' - version: 2026.5.18 + specifier: 2026.5.7 + version: 2026.5.7(@types/express@5.0.6) pdf-parse: specifier: ^2.4.5 version: 2.4.5 @@ -60,8 +63,8 @@ importers: packages: - '@agentclientprotocol/sdk@0.21.1': - resolution: {integrity: sha512-ZTLH+o9QxcZDLX/9ww+W7C2iExnXFM+vD/uGFVSlR61Kzj9FaxUqBC6Rv/kwgA7qVWYUEI9c5ZNqCuO9PM4rKg==} + '@agentclientprotocol/sdk@0.21.0': + resolution: {integrity: sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw==} peerDependencies: zod: ^3.25.0 || ^4.0.0 @@ -154,6 +157,18 @@ packages: zod: optional: true + '@anthropic-ai/sdk@0.93.0': + resolution: {integrity: sha512-q9vaSZQVFx6B/gPxetGYfLXSJD5v0sOmh0OpZDq7yCrTSA+Rscvrtyol7JJTW40wEpQB4U1B4JXzxQitbQ3CAA==} + hasBin: true + peerDependencies: + zod: ^3.25.0 || ^4.0.0 + peerDependenciesMeta: + zod: + optional: true + + '@anthropic-ai/vertex-sdk@0.16.1': + resolution: {integrity: sha512-NQSJTmHFqJP32W4I+UyZ42ioUkd8avdye259Cs+P9yhi+XdI4wk7sDVnmVNNTiMtN08WXyELnAQPG2gcLQFXdQ==} + '@aws-crypto/crc32@5.2.0': resolution: {integrity: sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==} engines: {node: '>=16.0.0'} @@ -171,14 +186,30 @@ packages: '@aws-crypto/util@5.2.0': resolution: {integrity: sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==} + '@aws-sdk/client-bedrock-runtime@3.1042.0': + resolution: {integrity: sha512-uYJ/HDSQvorlgYqZSwRFGolEx5wygqyuBRfemXJ3Bla2yiRj9maSVOvWP88i/hDC2BKoH6NQw8GPB9Z4RYAnwQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/client-bedrock-runtime@3.1049.0': resolution: {integrity: sha512-YM8b2baoRY8ul47b4amQW2VlUthLmM8DnqdlGO20LJmmmRpjnT91SaQJai3OMehA6uE0Gig88VyDCT1vEACSww==} engines: {node: '>=20.0.0'} + '@aws-sdk/client-bedrock@3.1042.0': + resolution: {integrity: sha512-oEVjGU8wgW+eTF7ApdRU4jTs/iMVl4OdfpLmiNLuB082UVxxN/fQ5GIX2Ktbyt+x0mPlI3fug36XnOyf7oCo+Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/client-cognito-identity@3.1049.0': + resolution: {integrity: sha512-gfgKqA8If1CVtHgDma2yIpBDbYiB8jshbGe+nagfjBmlOmrBAycpgXxPGg3VONtm+UfgzbkSYbbR9xKW8rRCQg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/core@3.974.12': resolution: {integrity: sha512-qrqgioqYFjwR6LatVNS1L2Vk++EwRIxqSQXPKNv5Ofux2D8UNgqMQ1znnMyEImXquVPTtbf71fc128pvmU6y9A==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-cognito-identity@3.972.35': + resolution: {integrity: sha512-mMQsBJv40oi5QdqRj4Xbc9jTlWMxqWfs5zWu+RhbOuF5F0AxxWXT70hm0abOmLbF2M/Tkuygs01H4eWIQMfoMw==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-env@3.972.38': resolution: {integrity: sha512-m3WjZEgPtioMhPmwqUt+DhlTJ2i9ufR6DhfkyXojb9puEvfR+ur2U5shavu5/Cc9WHHsDCvALi6UFHgcqjhQ5w==} engines: {node: '>=20.0.0'} @@ -195,6 +226,10 @@ packages: resolution: {integrity: sha512-O6WkZga3kf0yqyJYd1dbeJqVhEgJx/x1UaLgtbR+XuL/YP+K5y6QTxQKL7ka9z3jnQASESKGAPnRyt4D5hQrxA==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.39': + resolution: {integrity: sha512-HEswDQyxUtadoZ/bJsPPENHg7R0Lzym5LuMksJeHvqhCOpP+rtkDLKI4/ZChH4w3cf5kG8n6bZuI8PzajoiqMg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/credential-provider-node@3.972.43': resolution: {integrity: sha512-D/DJmbrWRP5BXEO3FH+ar4el+2n6OlGofiud7dQun2jES+AQEJjczenp1jBb4MBN7CpGpS8nsWGQLtuzc9tQbA==} engines: {node: '>=20.0.0'} @@ -211,6 +246,10 @@ packages: resolution: {integrity: sha512-/67fXX0ddllD4u2Nujc5PvT4byHgpMUfz6+RxIKi/0nFIckeorm7JvXgzBuDyVKw0s58EbofmETDWUf9vTEuHQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/credential-providers@3.1049.0': + resolution: {integrity: sha512-2B0ljqENrXKHlPg50kCV12RA35jcAfgZLLB38NW9qv5gUtOHFS8/wW7AkbyVAFYQySl/+2a3x1MAjY2d+Ed71g==} + engines: {node: '>=20.0.0'} + '@aws-sdk/eventstream-handler-node@3.972.16': resolution: {integrity: sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==} engines: {node: '>=20.0.0'} @@ -219,6 +258,22 @@ packages: resolution: {integrity: sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==} engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-host-header@3.972.13': + resolution: {integrity: sha512-EA3+u2LD3kGcfRNmCSjyJuzX4XvG4zYv57i4ZksH+1IEciuSyHQGvzivEz7vZ+jbRPdAAe7WWKy/4M8InCKDcw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-logger@3.972.12': + resolution: {integrity: sha512-NxB2dS4/mV3380hNkC72TkhMaLLjWGGBeTAEucqlJptVVovTbNmQWZLwaMC74ICo9NZHmFiBVVTHzDfAh/3y6Q==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-recursion-detection@3.972.14': + resolution: {integrity: sha512-bqL+upATpOJ/7px4IVfMVxcM6Lyt9uRizmEx3mNg4N6+IQlnOaYXXOJ4TNX6P0mKPPW0lwn9ZW8QEhXwQuCH9A==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/middleware-user-agent@3.972.42': + resolution: {integrity: sha512-U7jjlJKQnuUlI2swC2umFLFzLAxMLudSRFv+Bqk2F8ORmr5bG25qsFxGm4GEFwoZeGaFFnAFmTY0xReVRfyl2A==} + engines: {node: '>=20.0.0'} + '@aws-sdk/middleware-websocket@3.972.20': resolution: {integrity: sha512-LM6P0i+Lu6pi25oNw2nqxjRxiEOtLgPB7xIvHfa+FxHTRLg8wcgqu3qg2COl4QaT7Es2yCxYdeRLVYazKAwL8g==} engines: {node: '>= 14.0.0'} @@ -227,10 +282,18 @@ packages: resolution: {integrity: sha512-FtQ/Bt327peZJuyo4WZSOLVUTw9ujRxntepiC7L65FxA2P82Xlq0g14T22BuqBUeMjDoxa9nvwiMHjLIfP3eUg==} engines: {node: '>=20.0.0'} + '@aws-sdk/region-config-resolver@3.972.16': + resolution: {integrity: sha512-/YaivCvKUkEeMN9VTKBSvBn5w/4osAM1YboM58DKaLF/vqFGf/FdJCLmppqiPPJWZaXcASqByVjc3evE7KHKdA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/signature-v4-multi-region@3.996.27': resolution: {integrity: sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==} engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1042.0': + resolution: {integrity: sha512-rOEGTVOrceb/1CfIWK0zl1v2WS70f/i5bDirLl5xdFAbVQ5znub6Ezf2ugmJEg+rionO0IkwbKX3Dh3T/oZjbA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/token-providers@3.1049.0': resolution: {integrity: sha512-r7+d0lQMTHKypkmaF5jRTBYLYHCUHzt3gaVoN9SidLhQeWhCmHk3AKrboDTpPF5b7Pt7vKu3+oeMjznM2Eu1ow==} engines: {node: '>=20.0.0'} @@ -239,14 +302,33 @@ packages: resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==} engines: {node: '>=20.0.0'} + '@aws-sdk/util-endpoints@3.996.11': + resolution: {integrity: sha512-BUMJ6VoL54r6Udj/wKy8uKRIndL04rGbaS/wTIV0dM1ewxSrR8yARBHdvZKQsK55ZSW2JrmAPk3KP15kBDxJMw==} + engines: {node: '>=20.0.0'} + + '@aws-sdk/util-format-url@3.972.14': + resolution: {integrity: sha512-NQCk6YiJntbx3lTfRZVBo/2JTYJdblHHtQnLJQ8Pky87uotjhH2qhkkRUJa2jzuzf9RicXqKMjr4bfD/QeSBGA==} + engines: {node: '>=20.0.0'} + '@aws-sdk/util-locate-window@3.965.5': resolution: {integrity: sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==} engines: {node: '>=20.0.0'} + '@aws-sdk/util-user-agent-browser@3.972.13': + resolution: {integrity: sha512-wfk9ZdVwh187gdGXB1EyAoprwjSMt/bSfVtva+OaZx+LyNdKD7smlZf611yMd42UpfQ9vaS8NOftjSajgpdd+w==} + + '@aws-sdk/util-user-agent-node@3.973.28': + resolution: {integrity: sha512-A2l/PTRzsOS9L8dmZbXtDyJQgeeX+qjqLJ+fr0UU5Dz0AUQMuxgZCPSLKZgUDlHAmLFuk34owdMEvJxmDTBgRg==} + engines: {node: '>=20.0.0'} + '@aws-sdk/xml-builder@3.972.24': resolution: {integrity: sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==} engines: {node: '>=20.0.0'} + '@aws/bedrock-token-generator@1.1.0': + resolution: {integrity: sha512-i+DkWnfdA4j4sffy9dI4k3OGoOWqN8CTGdtO4IZ3c0kpKYFr6KyqzqLQmoRNrF3ACFcWj6u+J6cbBQ97j9wx5w==} + engines: {node: '>=16.0.0'} + '@aws/lambda-invoke-store@0.2.4': resolution: {integrity: sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==} engines: {node: '>=18.0.0'} @@ -315,27 +397,6 @@ packages: search-insights: optional: true - '@earendil-works/pi-agent-core@0.75.1': - resolution: {integrity: sha512-JVpX/Zle/enBzEM6he9sE0ASMo8Yhm8q7nOuPQjR/BXhkTBUevrNz7wtTV8VFvgjyhsXzbAsNCP5A4LiCcDx/A==} - engines: {node: '>=22.19.0'} - - '@earendil-works/pi-ai@0.75.1': - resolution: {integrity: sha512-/bhCWS2R+qHLBDnN+d1t1QRUxtZk7sZpMcrlexPq3W++3bJ0Df0GjhM2FToTubhoCsjOBdBOuRYcV8FNPfRUVQ==} - engines: {node: '>=22.19.0'} - hasBin: true - - '@earendil-works/pi-coding-agent@0.75.1': - resolution: {integrity: sha512-QMbmv8lFQ8P98kpuMc/z1ATTq7t0lQ+Bo3GLiOKQ/HonO34n4E1+395FCqlmG8zJEhiMp4yqVTzlj7BALQMlqw==} - engines: {node: '>=22.19.0'} - hasBin: true - - '@earendil-works/pi-tui@0.75.1': - resolution: {integrity: sha512-IFDSvCXcXMoIxFKxdhqc7ybX8p86KpdxoTUTYEq3FHilMFkBqlXqZD0jZBitqxStBjjMkAlhjS1bKS0IOXSpsg==} - engines: {node: '>=22.19.0'} - - '@emnapi/runtime@1.9.1': - resolution: {integrity: sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==} - '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -630,17 +691,8 @@ packages: cpu: [x64] os: [win32] - '@google/genai@1.46.0': - resolution: {integrity: sha512-ewPMN5JkKfgU5/kdco9ZhXBHDPhVqZpMQqIFQhwsHLf8kyZfx1cNpw1pHo1eV6PGEW7EhIBFi3aYZraFndAXqg==} - engines: {node: '>=20.0.0'} - peerDependencies: - '@modelcontextprotocol/sdk': ^1.25.2 - peerDependenciesMeta: - '@modelcontextprotocol/sdk': - optional: true - - '@google/genai@2.3.0': - resolution: {integrity: sha512-rXDhXUBj31gZafcwQFbXvt8jMrMxZoK7ECjQpk88UfA/OkZls3PtZDprT9lM3jjqRtwRjQoNLoPoNq6MlV8qLw==} + '@google/genai@1.52.0': + resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==} engines: {node: '>=20.0.0'} peerDependencies: '@modelcontextprotocol/sdk': ^1.25.2 @@ -660,8 +712,8 @@ packages: peerDependencies: grammy: ^1.0.0 - '@grammyjs/types@3.26.0': - resolution: {integrity: sha512-jlnyfxfev/2o68HlvAGRocAXgdPPX5QabG7jZlbqC2r9DZyWBfzTlg+nu3O3Fy4EhgLWu28hZ/8wr7DsNamP9A==} + '@grammyjs/types@3.27.3': + resolution: {integrity: sha512-yUKMLliGsGbnxu96YUJ7km7B0zy4PzeH/Jvti5705R/LeKDMqkDV4DckMSt+OrliWQpTwQljHE0QLol5zgxBkg==} '@homebridge/ciao@1.3.8': resolution: {integrity: sha512-lNhpCsZVbdbjz2trFjQdzQ3cUIMZQMIMksi7wd3ntTIYgdaGLqT1Ms97DfVIJYHzRuduf56ISvgU8RRLTpK/ng==} @@ -679,159 +731,6 @@ packages: '@iconify/types@2.0.0': resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==} - '@img/colour@1.1.0': - resolution: {integrity: sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==} - engines: {node: '>=18'} - - '@img/sharp-darwin-arm64@0.34.5': - resolution: {integrity: sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [darwin] - - '@img/sharp-darwin-x64@0.34.5': - resolution: {integrity: sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-darwin-arm64@1.2.4': - resolution: {integrity: sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==} - cpu: [arm64] - os: [darwin] - - '@img/sharp-libvips-darwin-x64@1.2.4': - resolution: {integrity: sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==} - cpu: [x64] - os: [darwin] - - '@img/sharp-libvips-linux-arm64@1.2.4': - resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-arm@1.2.4': - resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-ppc64@1.2.4': - resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-riscv64@1.2.4': - resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-s390x@1.2.4': - resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linux-x64@1.2.4': - resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-linux-arm64@0.34.5': - resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-arm@0.34.5': - resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-ppc64@0.34.5': - resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ppc64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-riscv64@0.34.5': - resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [riscv64] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-s390x@0.34.5': - resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [s390x] - os: [linux] - libc: [glibc] - - '@img/sharp-linux-x64@0.34.5': - resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [glibc] - - '@img/sharp-linuxmusl-arm64@0.34.5': - resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [linux] - libc: [musl] - - '@img/sharp-linuxmusl-x64@0.34.5': - resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [linux] - libc: [musl] - - '@img/sharp-wasm32@0.34.5': - resolution: {integrity: sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [wasm32] - - '@img/sharp-win32-arm64@0.34.5': - resolution: {integrity: sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [arm64] - os: [win32] - - '@img/sharp-win32-ia32@0.34.5': - resolution: {integrity: sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [ia32] - os: [win32] - - '@img/sharp-win32-x64@0.34.5': - resolution: {integrity: sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - cpu: [x64] - os: [win32] - '@isaacs/cliui@8.0.2': resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} @@ -958,6 +857,32 @@ packages: resolution: {integrity: sha512-MXdtr+6+ntlIVHdrZYuZNQydu6o8yZswFJ2Ln81j2O/Y9B/LDHvEaIm95xWNPkjGTWriSOeLnQJRFs6dYb60bg==} engines: {node: '>= 10'} + '@mariozechner/jiti@2.6.5': + resolution: {integrity: sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==} + hasBin: true + + '@mariozechner/pi-agent-core@0.73.0': + resolution: {integrity: sha512-ugcpvq0X9fr9fTSK29/3S4+KU/eeVMrBb7ZU3HqiF3xD7I1GlgumLj4FYmDrYSEA6+rzgNWlJUKwjKh9o0Z6AA==} + engines: {node: '>=20.0.0'} + deprecated: please use @earendil-works/pi-agent-core instead going forward + + '@mariozechner/pi-ai@0.73.0': + resolution: {integrity: sha512-phKOpcde/ssz6UYszkmaGJ9LF9mgt/AP8LrtSwsfap+kMSeFfSQ2/mCSBT1mLJ2BqVuff9uXs1/+op1aQeaafQ==} + engines: {node: '>=20.0.0'} + deprecated: please use @earendil-works/pi-ai instead going forward + hasBin: true + + '@mariozechner/pi-coding-agent@0.73.0': + resolution: {integrity: sha512-Fs2dRIgtjDT8X5VDGNGzxj251B0FvkRsgX03YJv1FK4wg5Maj+jkf8/5A6tbPnPcXsCgs41xxJRf3tF5vJRccA==} + engines: {node: '>=20.6.0'} + deprecated: please use @earendil-works/pi-coding-agent instead going forward + hasBin: true + + '@mariozechner/pi-tui@0.73.0': + resolution: {integrity: sha512-St1W+tMPKHatfK+lblsKfL+SsFyFVMK2tW6xHpBfCiMuevbOCRo/CMatso7mu1642UO04ncmfCrrpUK5L9aoog==} + engines: {node: '>=20.0.0'} + deprecated: please use @earendil-works/pi-tui instead going forward + '@mistralai/mistralai@2.2.1': resolution: {integrity: sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==} @@ -1122,16 +1047,6 @@ packages: '@nodable/entities@2.1.0': resolution: {integrity: sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==} - '@openclaw/fs-safe@0.2.4': - resolution: {integrity: sha512-Fo3WTQhxu0asD/rZqIKBqhX6fuZfjyHxSW5yTKfcRx+D9BRAcz0AGoVh+3ur/4XRvZkvsh3Ud8XTw006yRYLgg==} - engines: {node: '>=20.11'} - - '@openclaw/proxyline@0.3.3': - resolution: {integrity: sha512-sftHnW69NHQqLjCxBTvQ8f/eQl+peZ5pHCBQtuTWBbeuYRHZ0/GXVTmw/O/YKsShMbqPWhJB0UYtPPdvCUSS8w==} - engines: {node: '>=22.19.0'} - peerDependencies: - undici: '>=8.3.0 <9' - '@oxfmt/binding-android-arm-eabi@0.34.0': resolution: {integrity: sha512-sqkqjh/Z38l+duOb1HtVqJTAj1grt2ttkobCopC/72+a4Xxz4xUgZPFyQ4HxrYMvyqO/YA0tvM1QbfOu70Gk1Q==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1605,6 +1520,36 @@ packages: '@silvia-odwyer/photon-node@0.3.4': resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==} + '@slack/bolt@4.7.2': + resolution: {integrity: sha512-ALHtaS2iaP2WAWgX08yXsoCxEDitC6AqZs26ot6smXJQzBFMM4slVP+w3blLwzUV551xZ/+9RlBmWHsZDJJ5HA==} + engines: {node: '>=18', npm: '>=8.6.0'} + peerDependencies: + '@types/express': ^5.0.0 + + '@slack/logger@4.0.1': + resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@slack/oauth@3.0.5': + resolution: {integrity: sha512-exqFQySKhNDptWYSWhvRUJ4/+ndu2gayIy7vg/JfmJq3wGtGdHk531P96fAZyBm5c1Le3yaPYqv92rL4COlU3A==} + engines: {node: '>=18', npm: '>=8.6.0'} + + '@slack/socket-mode@2.0.7': + resolution: {integrity: sha512-qYy07je71WnEHgRwmw12DlAnZLi5HXmdlI2WUzUK2LH/rYXQpP6uEg462S5CwfE8FoCKUdIigHtYnOOfzZH1lQ==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@slack/types@2.21.1': + resolution: {integrity: sha512-I8vmSjNYWsaxuWPx6dz4yeh0h7vRBWbgAMK14LEmblbZ404BtrPbXs6jDPx4cYgGf8msDGF4A9opLZBu21FViQ==} + engines: {node: '>= 12.13.0', npm: '>= 6.12.0'} + + '@slack/web-api@7.16.0': + resolution: {integrity: sha512-68SAV77uuGKuhyyaRytX8UijVnqSLsTSKslGXw17cjQYXn+jtNl7gbaEjHgC5x2rhCuFdahBrEC2VCLppbzReg==} + engines: {node: '>= 18', npm: '>= 8.6.0'} + + '@smithy/config-resolver@4.5.3': + resolution: {integrity: sha512-TpS6Am5zSEtx3ow7VynThEL7UwRM06zZZcmFaP6Ij9hqKPfsFhTYCLcgU7gjFjw9QAI2kzwXrfS7InH8BivJTA==} + engines: {node: '>=18.0.0'} + '@smithy/core@3.24.3': resolution: {integrity: sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==} engines: {node: '>=18.0.0'} @@ -1613,34 +1558,141 @@ packages: resolution: {integrity: sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==} engines: {node: '>=18.0.0'} + '@smithy/eventstream-serde-browser@4.3.3': + resolution: {integrity: sha512-LXg5yYJPYnVSrpa6LOZ+/wqpI2OlIccy7j5F16EFNYDbXWmnhry/PFRRPyM30H+hJeqfVgckFuvNGnAGCt56cA==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-config-resolver@4.4.3': + resolution: {integrity: sha512-MdQxEX5SFNc3QmpiLXtcZXsWk4imCfGVN7Ikz9I/XvavypvHT4mqxwo5JHdr/LBKCfAv89+8193ZWlUwDp8YXQ==} + engines: {node: '>=18.0.0'} + + '@smithy/eventstream-serde-node@4.3.3': + resolution: {integrity: sha512-54RbRsw9eVaVnqYUXi3F6nMAPgUyKsBvAKBY2lf+81mIgM7N+yS9V5LYk7yUGbrM789b2e1qBuyDSjX1/Axxcw==} + engines: {node: '>=18.0.0'} + '@smithy/fetch-http-handler@5.4.3': resolution: {integrity: sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==} engines: {node: '>=18.0.0'} + '@smithy/hash-node@4.3.3': + resolution: {integrity: sha512-tSUA38sM7kzMoLhqQ2aCGTwLXovjurz3jjG+a0sxqD4qT/4FhQr/wxMdhCumT70giM+axC1pPjimAHLlEQCfzw==} + engines: {node: '>=18.0.0'} + + '@smithy/invalid-dependency@4.3.3': + resolution: {integrity: sha512-wUWowbCm7DGczl6bfLI6wGGtoxwN5Pon8DhF0Q8AA4NvgLwYfLo3h2DWI7sHr33lLcEsyTLQKeUeTHydqSfQ5Q==} + engines: {node: '>=18.0.0'} + '@smithy/is-array-buffer@2.2.0': resolution: {integrity: sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==} engines: {node: '>=14.0.0'} + '@smithy/middleware-content-length@4.3.3': + resolution: {integrity: sha512-Up1XAYnj6oxFBypWpkhNpgX+yReQxkKAV/iLaeP0KVLb2oTkmA9X+UJuGBVvEA9uZIN06y0irDi7sBMuTZMVJg==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-endpoint@4.5.3': + resolution: {integrity: sha512-p60HGFflWsJC6V9GAYeFgbfORn+9ILx8FqgMa/8PzA0rhIUxF57EKoOR4Irs6oe1oy8RLzhjhcGS8CBtPv/t+Q==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-retry@4.6.3': + resolution: {integrity: sha512-MnfYnJs3cBXK3ZBqbPzXRPHIp+QtgpkX5NogcUOWHPU5GbgTAQSIfPLi91lTcEbkFDcH2YbgjLPQjWeyQ689rA==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-serde@4.3.3': + resolution: {integrity: sha512-RUVCZgn92izDAARs5OJSM2+KWSfTRvQWwN9t0MmiybT3pquRgDx9vD9t/YZjd/5lwcFbsNuPojJSddYQEZGeWw==} + engines: {node: '>=18.0.0'} + + '@smithy/middleware-stack@4.3.3': + resolution: {integrity: sha512-+BPabWluqxo3EfMMvOgnAmPtWnCSzj+gf5mJ27wTZUbvS0hpdUIU1g80R01bEGKZx4JCi8P58jAXD9FUGMjhwA==} + engines: {node: '>=18.0.0'} + + '@smithy/node-config-provider@4.4.3': + resolution: {integrity: sha512-vDtz5OuytrjP4o9GtAOz1JloN003p94utJIQeO0WAjorhpafFFjpbDOrP6btPoCN3UxaU/U84OIEt5dM7ZRRLA==} + engines: {node: '>=18.0.0'} + '@smithy/node-http-handler@4.7.3': resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==} engines: {node: '>=18.0.0'} + '@smithy/property-provider@4.3.3': + resolution: {integrity: sha512-nmeVi9Ww/RMyttqj1Dh0PA+iVieKm4dxDlnT6tNP118O/5U/Qqb9b3DV5A3RX+slR/m4/MABSZ2zNfSkpVV8dw==} + engines: {node: '>=18.0.0'} + + '@smithy/protocol-http@5.4.3': + resolution: {integrity: sha512-P16TBD/d8ZcD9MHQ0ubQ9BbOYSd5HZKbHOLsyFWxKk2oBEoghbRFPfGOoqToZX1yrfLITXRylL16EyPP4IzLPg==} + engines: {node: '>=18.0.0'} + + '@smithy/shared-ini-file-loader@4.5.3': + resolution: {integrity: sha512-9fgVSJBB1k79oZkT5eLHaPx289LZg8wDi2xNEDKlD2Wy2GpPQfvUhnzJCXEWQxIJ5hhj+peI/todWUFBXhi86w==} + engines: {node: '>=18.0.0'} + '@smithy/signature-v4@5.4.3': resolution: {integrity: sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==} engines: {node: '>=18.0.0'} + '@smithy/smithy-client@4.13.3': + resolution: {integrity: sha512-Z8mQ+YryjP5krDadV6unnp5035L4S1brafXpTiRmjPweKSaQ6X9CYDYWvmEggXjDIa1oufX/2a/bdwu8EIz/lw==} + engines: {node: '>=18.0.0'} + '@smithy/types@4.14.2': resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==} engines: {node: '>=18.0.0'} + '@smithy/url-parser@4.3.3': + resolution: {integrity: sha512-TsMTAOnjuMOv1zJBw8cfYGWhopyc3og8tZX/KuyCPjg7V3ji3f4YjFOVu843UjBmrfS/+X6kwFv5ZKg7sSm1bQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-base64@4.4.3': + resolution: {integrity: sha512-91lxjhFpAktA9yPBxniqVR/NSH9zyjMjLmoa+jbQHQFR9WiJA+n61T7HBrfh5APdEoAledJwGq8l4cS+ZJFUnQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-browser@4.3.3': + resolution: {integrity: sha512-/M6Ya1Fjq8hg3rYjiwwqTen6s1bAa3U3g/2eicBaBQfaoa4ymLUke/x4T8mwb9dSq/L8TQ4YgndS0MaB9ShgmA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-body-length-node@4.3.3': + resolution: {integrity: sha512-M+zdSrevWj0grtZx2RBULPUyjTq1aB+n+13Hrm9owiGpow6DqY/WqiSj6sHVQy/rKp0j7NzV3TNf2LrwDel8JQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-buffer-from@2.2.0': resolution: {integrity: sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==} engines: {node: '>=14.0.0'} + '@smithy/util-defaults-mode-browser@4.4.3': + resolution: {integrity: sha512-Q60hxKkMEkmBsOEzxlMWEymBWov0dtWGgoJhOUs6mE8k2FDPjK8NlsRdMkmO80n2pwzreHtrYcX5jiRP7ZkP3w==} + engines: {node: '>=18.0.0'} + + '@smithy/util-defaults-mode-node@4.3.3': + resolution: {integrity: sha512-RYj+8gr95WiiBqvVghoRvL12NS9ryvLyufp7FOs7EzKwGX0W5gOVlXdCrFkJScSf8gxdjQMRyIZ3Y82/MvXQ3Q==} + engines: {node: '>=18.0.0'} + + '@smithy/util-endpoints@3.5.3': + resolution: {integrity: sha512-2JqSmzQtKDKqBckLl/9NXTL1fY+zQBU5fNGMpud7AT65vql0tVFhb2UEZNZmLSHayLeD+X/Qzn84oXw5KS+KSQ==} + engines: {node: '>=18.0.0'} + + '@smithy/util-middleware@4.3.3': + resolution: {integrity: sha512-8NZwlQ+nyAIWn9YZxH14FC8ca0i6ZGW1aJyPjD+zMZz3k9jOhXXKhdCSRvjmcSYLW42uhbrxavXqMkrTKHyY3A==} + engines: {node: '>=18.0.0'} + + '@smithy/util-retry@4.4.3': + resolution: {integrity: sha512-8RJXeU5lEhdNfXm4XAuHlf6VtNzd279Z2FJZSR7VaELYCR46ffgjJBSjc+3UAy7V1YqBOLV0G9gWhLB/nA44nA==} + engines: {node: '>=18.0.0'} + + '@smithy/util-stream@4.6.3': + resolution: {integrity: sha512-DSpJpPg0rQwjZk9/CSlOTplD6xSUu+bz8eDJQkq/Fmy9JlSD4ZGhXG/qFl0aRHmouDbBF75tnZ00lPxiL/sgRQ==} + engines: {node: '>=18.0.0'} + '@smithy/util-utf8@2.3.0': resolution: {integrity: sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==} engines: {node: '>=14.0.0'} + '@smithy/util-utf8@4.3.3': + resolution: {integrity: sha512-c1QpRBn3aMsoqE64dd4Imgjy8Pynfw+eR7GkjElquxUFSnezwYVaOFm8JcYa+Bo/5ssbEyPKcT3+4bmrWYh6eQ==} + engines: {node: '>=18.0.0'} + + '@telegraf/types@7.1.0': + resolution: {integrity: sha512-kGevOIbpMcIlCDeorKGpwZmdH7kHbqlk/Yj6dEpJMKEQw5lk0KVQY0OLXaCswy8GqlIVLd5625OB+rAntP9xVw==} + '@tokenizer/inflate@0.4.1': resolution: {integrity: sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==} engines: {node: '>=18'} @@ -1648,18 +1700,39 @@ packages: '@tokenizer/token@0.3.0': resolution: {integrity: sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==} + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@types/body-parser@1.19.6': + resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==} + '@types/chai@5.2.3': resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + '@types/deep-eql@4.0.2': resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + '@types/express-serve-static-core@5.1.1': + resolution: {integrity: sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==} + + '@types/express@5.0.6': + resolution: {integrity: sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/http-errors@2.0.5': + resolution: {integrity: sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==} + + '@types/jsonwebtoken@9.0.10': + resolution: {integrity: sha512-asx5hIG9Qmf/1oStypjanR7iKTv0gXQ1Ov/jfrX6kS/EO0OFni8orbmGCn0672NHR3kXHwpAwR+B368ZGN/2rA==} + '@types/linkify-it@5.0.0': resolution: {integrity: sha512-sVDA58zAw4eWAffKOaQH5/5j3XeayukzDk+ewSsnv3p4yJEZHCCzMDiZM8e0OUrRvmpGZ85jf4yDHkHsgBNr9Q==} @@ -1672,18 +1745,42 @@ packages: '@types/mdurl@2.0.0': resolution: {integrity: sha512-RGdgjQUZba5p6QEFAVx2OGb8rQDL/cPRG7GiedRzMcJ1tYnUANBncjbSB1NRGwbvjcPeikRABz2nshyPk1bhWg==} + '@types/mime-types@2.1.4': + resolution: {integrity: sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + '@types/node@25.2.0': resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==} + '@types/qs@6.15.1': + resolution: {integrity: sha512-GZHUBZR9hckSUhrxmp1nG6NwdpM9fCunJwyThLW1X3AyHgd9IlHb6VANpQQqDr2o/qQp6McZ3y/IA2rVzKzSbw==} + + '@types/range-parser@1.2.7': + resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} + '@types/retry@0.12.0': resolution: {integrity: sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==} + '@types/send@1.2.1': + resolution: {integrity: sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==} + + '@types/serve-static@2.2.0': + resolution: {integrity: sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==} + '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} '@types/web-bluetooth@0.0.21': resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} + '@types/ws@8.18.1': + resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} + + '@types/yauzl@2.10.3': + resolution: {integrity: sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==} + '@ungap/structured-clone@1.3.0': resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==} @@ -1832,10 +1929,18 @@ packages: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} + agent-base@6.0.2: + resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} + engines: {node: '>= 6.0.0'} + agent-base@7.1.4: resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} engines: {node: '>= 14'} + agent-base@9.0.0: + resolution: {integrity: sha512-TQf59BsZnytt8GdJKLPfUZ54g/iaUL2OWDSFCCvMOhsHduDQxO8xC4PNeyIkVcA5KwL2phPSv0douC0fgWzmnA==} + engines: {node: '>= 20'} + ajv-formats@3.0.1: resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} peerDependencies: @@ -1867,6 +1972,9 @@ packages: resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} engines: {node: '>=12'} + any-promise@1.3.0: + resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -1880,6 +1988,10 @@ packages: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + ast-v8-to-istanbul@0.3.11: resolution: {integrity: sha512-Qya9fkoofMjCBNVdWINMjB5KZvkYfaO9/anwkWnjxibpWUxo5iHl2sOdP7/uAqaRuUYuoo8rDwnbaaKVFxoUvw==} @@ -1889,6 +2001,9 @@ packages: axios@1.13.6: resolution: {integrity: sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==} + axios@1.16.1: + resolution: {integrity: sha512-caYkukvroVPO8KrzuJEb50Hm07KwfBZPEC3VeFHTsqWHvKTsy54hjJz9BS/cdaypROE2rH6xvm9mHX4fgWkr3A==} + balanced-match@4.0.3: resolution: {integrity: sha512-1pHv8LX9CpKut1Zp4EXey7Z8OfH11ONNH6Dhi2WDUt31VVZFXZzKwXcysBgqSumFCmR+0dqjMK5v5JiFHzi0+g==} engines: {node: 20 || >=22} @@ -1896,6 +2011,10 @@ packages: base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + basic-ftp@5.3.1: + resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} + engines: {node: '>=10.0.0'} + bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -1925,9 +2044,25 @@ packages: resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==} engines: {node: 20 || >=22} + brace-expansion@5.0.6: + resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} + engines: {node: 18 || 20 || >=22} + + buffer-alloc-unsafe@1.1.0: + resolution: {integrity: sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==} + + buffer-alloc@1.2.0: + resolution: {integrity: sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==} + + buffer-crc32@0.2.13: + resolution: {integrity: sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==} + buffer-equal-constant-time@1.0.1: resolution: {integrity: sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==} + buffer-fill@1.0.0: + resolution: {integrity: sha512-T7zexNBwiiaCOGDg9xNX9PBmjrubblRkENuptryuI64URkXDFum9il/JGL8Lm8wYfAXpredVXXZz7eMHilimiQ==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} @@ -1958,6 +2093,10 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + chalk@5.6.2: resolution: {integrity: sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==} engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} @@ -1980,9 +2119,17 @@ packages: resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} engines: {node: '>=18'} - cliui@6.0.0: + cli-highlight@2.1.11: + resolution: {integrity: sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==} + engines: {node: '>=8.0.0', npm: '>=5.0.0'} + hasBin: true + + cliui@6.0.0: resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==} + cliui@7.0.4: + resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} + cliui@8.0.1: resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} engines: {node: '>=12'} @@ -2057,6 +2204,14 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + data-uri-to-buffer@8.0.0: + resolution: {integrity: sha512-6UHfyCux51b8PTGDgveqtz1tvphBku5DrMKKJbFAZAJOI2zsjDpDoYE1+QGj7FOMS4BdTFNJsJiR3zEB0xH0yQ==} + engines: {node: '>= 20'} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -2074,6 +2229,24 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + degenerator@7.0.1: + resolution: {integrity: sha512-ABErK0IefDSyHjlPH7WUEenIAX2rPPnrDcDM+TS3z3+zu9TfyKKi07BQM+8rmxpdE2y1v5fjjdoAS/x4D2U60w==} + engines: {node: '>= 20'} + peerDependencies: + quickjs-wasi: ^2.2.0 + delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -2086,10 +2259,6 @@ packages: resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} engines: {node: '>=6'} - detect-libc@2.1.2: - resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} - engines: {node: '>=8'} - devlop@1.1.0: resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} @@ -2119,6 +2288,10 @@ packages: domutils@3.2.2: resolution: {integrity: sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==} + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + dotenv@17.4.2: resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} engines: {node: '>=12'} @@ -2152,6 +2325,9 @@ packages: resolution: {integrity: sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==} engines: {node: '>= 0.8'} + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} @@ -2196,12 +2372,34 @@ packages: escape-html@1.0.3: resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} estree-walker@3.0.3: resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + etag@1.8.1: resolution: {integrity: sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==} engines: {node: '>= 0.6'} @@ -2210,6 +2408,12 @@ packages: resolution: {integrity: sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==} engines: {node: '>=6'} + eventemitter3@4.0.7: + resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + eventsource-parser@3.0.6: resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} engines: {node: '>=18.0.0'} @@ -2235,6 +2439,11 @@ packages: extend@3.0.2: resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + extract-zip@2.0.1: + resolution: {integrity: sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==} + engines: {node: '>= 10.17.0'} + hasBin: true + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -2257,6 +2466,9 @@ packages: resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==} hasBin: true + fd-slicer@1.1.0: + resolution: {integrity: sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==} + fdir@6.5.0: resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} engines: {node: '>=12.0.0'} @@ -2270,6 +2482,10 @@ packages: resolution: {integrity: sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==} engines: {node: ^12.20 || >= 14.13} + file-type@21.3.4: + resolution: {integrity: sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==} + engines: {node: '>=20'} + file-type@22.0.1: resolution: {integrity: sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA==} engines: {node: '>=22'} @@ -2294,6 +2510,15 @@ packages: debug: optional: true + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + foreground-child@3.3.1: resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} engines: {node: '>=14'} @@ -2322,10 +2547,18 @@ packages: function-bind@1.1.2: resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + gaxios@6.7.1: + resolution: {integrity: sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==} + engines: {node: '>=14'} + gaxios@7.1.4: resolution: {integrity: sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==} engines: {node: '>=18'} + gcp-metadata@6.1.1: + resolution: {integrity: sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==} + engines: {node: '>=14'} + gcp-metadata@8.1.2: resolution: {integrity: sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==} engines: {node: '>=18'} @@ -2346,6 +2579,18 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + get-uri@8.0.0: + resolution: {integrity: sha512-CqtZlMKvfJeY0Zxv8wazDwXmSKmnMnsmNy8j8+wudi8EyG/pMUB1NqHc+Tv1QaNtpYsK9nOYjb7r7Ufu32RPSw==} + engines: {node: '>= 20'} + glob@10.5.0: resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me @@ -2355,10 +2600,26 @@ packages: resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} engines: {node: 18 || 20 || >=22} + global-agent@4.1.3: + resolution: {integrity: sha512-KUJEViiuFT3I97t+GYMikLPJS2Lfo/S2F+DQuBWzuzaMPnvt5yyZePzArx36fBzpGTxZjIpDbXLeySLgh+k76g==} + engines: {node: '>=10.0'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + google-auth-library@10.6.2: resolution: {integrity: sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==} engines: {node: '>=18'} + google-auth-library@9.15.1: + resolution: {integrity: sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==} + engines: {node: '>=14'} + + google-logging-utils@0.0.2: + resolution: {integrity: sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==} + engines: {node: '>=14'} + google-logging-utils@1.1.3: resolution: {integrity: sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==} engines: {node: '>=14'} @@ -2370,14 +2631,21 @@ packages: graceful-fs@4.2.11: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} - grammy@1.42.0: - resolution: {integrity: sha512-1AdCge+AkjSdp2FwfICSFnVbl8Mq3KVHJDy+DgTI9+D6keJ0zWALPRKas5jv/8psiCzL4N2cEOcGW7O45Kn39g==} + grammy@1.43.0: + resolution: {integrity: sha512-7dYm06A945mXuIk/5HUlSjeyIYChW8vCEiU2dkOKKqJJzwAWxTkCc91Eqbz7TgODh2rtFFKWI/fekowWHOkmjQ==} engines: {node: ^12.20.0 || >=14.13.1} + gtoken@7.1.0: + resolution: {integrity: sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==} + engines: {node: '>=14.0.0'} + has-flag@4.0.0: resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} engines: {node: '>=8'} + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + has-symbols@1.1.0: resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} engines: {node: '>= 0.4'} @@ -2399,8 +2667,8 @@ packages: highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} - hono@4.12.9: - resolution: {integrity: sha512-wy3T8Zm2bsEvxKZM5w21VdHDDcwVS1yUFFY6i8UobSsKfFceT7TOwhbhfKsDyx7tYQlmRM5FLpIuYvNFyjctiA==} + hono@4.12.10: + resolution: {integrity: sha512-mx/p18PLy5og9ufies2GOSUqep98Td9q4i/EF6X7yJgAiIopxqdfIO3jbqsi3jRgTgw88jMDEzVKi+V2EF+27w==} engines: {node: '>=16.9.0'} hookable@5.5.3: @@ -2430,14 +2698,26 @@ packages: resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} engines: {node: '>= 14'} + http-proxy-agent@9.0.0: + resolution: {integrity: sha512-FcF8VhXYLQcxWCnt/cCpT2apKsRDUGeVEeMqGu4HSTu29U8Yw0TLOjdYIlDsYk3IkUh+taX4IDWpPcCqKDhCjA==} + engines: {node: '>= 20'} + http_ece@1.2.0: resolution: {integrity: sha512-JrF8SSLVmcvc5NducxgyOrKXe3EsyHMgBFgSaIUGmArKe+rwr0uphRkRXvwiom3I+fpIfoItveHrfudL8/rxuA==} engines: {node: '>=16'} + https-proxy-agent@5.0.1: + resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} + engines: {node: '>= 6'} + https-proxy-agent@7.0.6: resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} engines: {node: '>= 14'} + https-proxy-agent@9.0.0: + resolution: {integrity: sha512-/MVmHp58WkOypgFhCLk4fzpPcFQvTJ/e6LBI7irpIO2HfxUbpmYoHF+KzipzJpxxzJu7aJNWQ0xojJ/dzV2G5g==} + engines: {node: '>= 20'} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -2459,6 +2739,10 @@ packages: resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} engines: {node: '>= 12'} + ip-address@10.2.0: + resolution: {integrity: sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==} + engines: {node: '>= 12'} + ipaddr.js@1.9.1: resolution: {integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==} engines: {node: '>= 0.10'} @@ -2467,6 +2751,9 @@ packages: resolution: {integrity: sha512-9VGk3HGanVE6JoZXHiCpnGy5X0jYDnN4EA4lntFPj+1vIWlFhIylq2CrrCOJH9EAhc5CYhq18F2Av2tgoAPsYQ==} engines: {node: '>= 10'} + is-electron@2.2.2: + resolution: {integrity: sha512-FO/Rhvz5tuw4MCWkpMzHFKWD2LsfHzIb7i6MdPYZ/KW7AlxawyLkqdy+jPZP1WubqEADE3O4FUENlJHDfQASRg==} + is-fullwidth-code-point@3.0.0: resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} engines: {node: '>=8'} @@ -2474,6 +2761,10 @@ packages: is-promise@4.0.0: resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==} + is-stream@2.0.1: + resolution: {integrity: sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==} + engines: {node: '>=8'} + is-what@5.5.0: resolution: {integrity: sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw==} engines: {node: '>=18'} @@ -2534,6 +2825,10 @@ packages: engines: {node: '>=6'} hasBin: true + jsonwebtoken@9.0.3: + resolution: {integrity: sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==} + engines: {node: '>=12', npm: '>=6'} + jszip@3.10.1: resolution: {integrity: sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==} @@ -2550,10 +2845,6 @@ packages: koffi@2.15.2: resolution: {integrity: sha512-r9tjJLVRSOhCRWdVyQlF3/Ugzeg13jlzS4czS82MAgLff4W+BcYOW7g8Y62t9O5JYjYOLAjAovAZDNlDfZNu+g==} - kysely@0.29.1: - resolution: {integrity: sha512-mOW4e+UMfrV1u/+a4uXO72mkwEJCIL4Tb/OQ8wU8jY5spUHxLKFfC1AnfNhfSoHubnIRly3u/xgnMdD0Vzq2RQ==} - engines: {node: '>=22.0.0'} - lie@3.3.0: resolution: {integrity: sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==} @@ -2573,6 +2864,27 @@ packages: resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==} engines: {node: '>=8'} + lodash.includes@4.3.0: + resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==} + + lodash.isboolean@3.0.3: + resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==} + + lodash.isinteger@4.0.4: + resolution: {integrity: sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==} + + lodash.isnumber@3.0.3: + resolution: {integrity: sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==} + + lodash.isplainobject@4.0.6: + resolution: {integrity: sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==} + + lodash.isstring@4.0.1: + resolution: {integrity: sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==} + + lodash.once@4.1.1: + resolution: {integrity: sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} @@ -2589,6 +2901,10 @@ packages: resolution: {integrity: sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==} engines: {node: 20 || >=22} + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} @@ -2616,6 +2932,10 @@ packages: engines: {node: '>= 18'} hasBin: true + matcher@4.0.0: + resolution: {integrity: sha512-S6x5wmcDmsDRRU/c2dkccDwQPXoFczc5+HpQ2lON8pnvHlnvHAHj5WlLVvw6n6vNyHuVugYrFohYxbS+pvFpKQ==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2668,8 +2988,8 @@ packages: minimalistic-assert@1.0.1: resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==} - minimatch@10.2.4: - resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==} + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} engines: {node: 18 || 20 || >=22} minimatch@9.0.6: @@ -2693,9 +3013,16 @@ packages: mitt@3.0.1: resolution: {integrity: sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==} + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mz@2.7.0: + resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} + nanoid@3.3.11: resolution: {integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==} engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} @@ -2705,6 +3032,10 @@ packages: resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==} engines: {node: '>= 0.6'} + netmask@2.1.1: + resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} + engines: {node: '>= 0.4.0'} + node-addon-api@8.7.0: resolution: {integrity: sha512-9MdFxmkKaOYVTV+XVRG8ArDwwQ77XIgIPyKASB1k3JPq3M8fGQQQE3YpMOrKm6g//Ktx8ivZr8xo1Qmtqub+GA==} engines: {node: ^18 || ^20 || >= 21} @@ -2746,6 +3077,10 @@ packages: resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} engines: {node: '>= 0.4'} + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -2780,9 +3115,14 @@ packages: zod: optional: true - openclaw@2026.5.18: - resolution: {integrity: sha512-a9p2jdD0SEFUIxyCeOsf8gcO7fdo3vn1zGSYi04gA5mE+J1gHCSJTmk+R+hDPg6XOgHLXD+S2PrKi/74qTGPKw==} - engines: {node: '>=22.19.0'} + openclaw@2026.5.7: + resolution: {integrity: sha512-hjvpgconK20YltQPrzDY6cehjM8ijQyZnLKhqLBTngiFEPum9gmXwCDsrisPEXVRFtzuMhap+w6zSEmSQ1047Q==} + engines: {node: '>=22.14.0'} + hasBin: true + + openshell@0.1.0: + resolution: {integrity: sha512-B7jLewH+d73hraWcrSFgNOjvd+frW5JPejkTpqgj2EJBjX/Yk1Y4blgP5pDl4FwrBxfmwsTKR08Uwgrdo+xpSg==} + engines: {node: '>=18'} hasBin: true option@0.2.4: @@ -2807,6 +3147,10 @@ packages: oxlint-tsgolint: optional: true + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + p-limit@2.3.0: resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} engines: {node: '>=6'} @@ -2815,20 +3159,59 @@ packages: resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==} engines: {node: '>=8'} + p-queue@6.6.2: + resolution: {integrity: sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==} + engines: {node: '>=8'} + p-retry@4.6.2: resolution: {integrity: sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==} engines: {node: '>=8'} + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-timeout@4.1.0: + resolution: {integrity: sha512-+/wmHtzJuWii1sXn3HCuH/FTwGhrp4tmJTxSKJbfS+vkipci6osxXM5mY0jUiRzWKMTgUT8l7HFbeSwZAynqHw==} + engines: {node: '>=10'} + p-try@2.2.0: resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} engines: {node: '>=6'} + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-proxy-agent@9.0.1: + resolution: {integrity: sha512-3ZOSpLboOlpW4yp8Cuv21KlTULRqyJ5Uuad3wXpSKFrxdNgcHEyoa22GRaZ2UlgCVuR6z+5BiavtYVvbajL/Yw==} + engines: {node: '>= 20'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + pac-resolver@9.0.1: + resolution: {integrity: sha512-lJbS008tmkj08VhoM8Hzuv/VE5tK9MS0OIQ/7+s0lIF+BYhiQWFYzkSpML7lXs9iBu2jfmzBTLzhe9n6BX+dYw==} + engines: {node: '>= 20'} + peerDependencies: + quickjs-wasi: ^2.2.0 + package-json-from-dist@1.0.1: resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} pako@1.0.11: resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parse5-htmlparser2-tree-adapter@6.0.1: + resolution: {integrity: sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==} + + parse5@5.1.1: + resolution: {integrity: sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==} + + parse5@6.0.1: + resolution: {integrity: sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==} + parseurl@1.3.3: resolution: {integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==} engines: {node: '>= 0.8'} @@ -2883,6 +3266,9 @@ packages: resolution: {integrity: sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==} engines: {node: '>=22.13.0 || >=24'} + pend@1.2.0: + resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==} + perfect-debounce@1.0.0: resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==} @@ -2897,8 +3283,8 @@ packages: resolution: {integrity: sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==} engines: {node: '>=16.20.0'} - playwright-core@1.60.0: - resolution: {integrity: sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==} + playwright-core@1.59.1: + resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==} engines: {node: '>=18'} hasBin: true @@ -2934,9 +3320,24 @@ packages: resolution: {integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==} engines: {node: '>= 0.10'} + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-agent@8.0.1: + resolution: {integrity: sha512-kccqGBqHZXR8onQhY/ganJjoO8QIKKRiFBhPOzbTZK16attzSZ/0XSmp9H7jrRxPKHjhGyx1q32lMPrJ3uLFgA==} + engines: {node: '>= 20'} + proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + proxy-from-env@2.1.0: + resolution: {integrity: sha512-cJ+oHTW1VAEa8cJslgmUZrc+sjRKgAKl3Zyse6+PV38hZe/V6Z14TbCuXcan9F9ghlz4QrFr2c92TNF82UkYHA==} + engines: {node: '>=10'} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + punycode.js@2.3.1: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} @@ -3011,9 +3412,16 @@ packages: safe-buffer@5.1.2: resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + safe-compare@1.1.4: + resolution: {integrity: sha512-b9wZ986HHCo/HbKrRpBJb2kqXMK9CEWIE1egeEvZsYn69ay3kdfl9nG3RyOcR+jInTDf7a86WQ1d4VJX7goSSQ==} + safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + sandwich-stream@2.0.2: + resolution: {integrity: sha512-jLYV0DORrzY3xaz/S9ydJL6Iz7essZeAfnAavsJ+zsJGZ1MOnsS52yRjU3uF3pJa/lla7+wisp//fxOwOH8SKQ==} + engines: {node: '>= 0.10'} + search-insights@2.17.3: resolution: {integrity: sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==} @@ -3026,6 +3434,10 @@ packages: resolution: {integrity: sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==} engines: {node: '>= 18'} + serialize-error@8.1.0: + resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} + engines: {node: '>=10'} + serve-static@2.2.1: resolution: {integrity: sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==} engines: {node: '>= 18'} @@ -3039,10 +3451,6 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sharp@0.34.5: - resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} - engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} - shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -3083,6 +3491,22 @@ packages: sisteransi@1.0.5: resolution: {integrity: sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==} + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@10.0.0: + resolution: {integrity: sha512-pyp2YR3mNxAMu0mGLtzs4g7O3uT4/9sQOLAKcViAkaS9fJWkud7nmaf6ZREFqQEi24IPkBcjfHjXhPTUWjo3uA==} + engines: {node: '>= 20'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -3189,14 +3613,22 @@ packages: resolution: {integrity: sha512-tOG/7GyXpFevhXVh8jOPJrmtRpOTsYqUIkVdVooZYJS/z8WhfQUX8RJILmeuJNinGAMSu1veBr4asSHFt5/hng==} engines: {node: '>=18'} - tar@7.5.15: - resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==} - engines: {node: '>=18'} + telegraf@4.16.3: + resolution: {integrity: sha512-yjEu2NwkHlXu0OARWoNhJlIjX09dRktiMQFsM678BAH/PEPVwctzL67+tvXqLCRQQvm3SDtki2saGO9hLlz68w==} + engines: {node: ^12.20.0 || >=14.13.1} + hasBin: true test-exclude@7.0.1: resolution: {integrity: sha512-pFYqmTw68LXVjeWJMST4+borgQP2AyMNbg1BpZh9LbyhUeNkeaPF9gzfPGUAnSMV3qPYdWUwDIjjCLiSDOl7vg==} engines: {node: '>=18'} + thenify-all@1.6.0: + resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} + engines: {node: '>=0.8'} + + thenify@3.3.1: + resolution: {integrity: sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==} + tinybench@2.9.0: resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} @@ -3231,8 +3663,8 @@ packages: resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==} engines: {node: '>=14.16'} - tokenjuice@0.7.1: - resolution: {integrity: sha512-eO048hm9UcGHASjYkIWEij8QN68amGp+S1nJyo685qB1/ol+VGEYjPglcVPvCbJbZyFHvI+BBAMvOfnqYCtpsQ==} + tokenjuice@0.7.0: + resolution: {integrity: sha512-RZIyFmzztf/8V4q1cUS5L+q8UISMSfsjzh4UoWVxQbE7/zX91SfNmHpNqopqyB4oc5hwH4XqC9O/yakVzJCu8g==} engines: {node: '>=20'} hasBin: true @@ -3260,23 +3692,26 @@ packages: resolution: {integrity: sha512-XuELoRpMR+sq8fuWwX7P0bcj+PRNiicOKDEb3fGNURhxWVyykCi9BNq7c4uVz7h7P0sj8qgBsr5SWS6yBClq3g==} engines: {node: '>=16'} + tsscmp@1.0.6: + resolution: {integrity: sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==} + engines: {node: '>=0.6.x'} + + type-fest@0.20.2: + resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} + engines: {node: '>=10'} + type-is@2.0.1: resolution: {integrity: sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==} engines: {node: '>= 0.6'} - typebox@1.1.38: - resolution: {integrity: sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==} + typebox@1.1.37: + resolution: {integrity: sha512-jb7jp6KvOvvy5sd+11AfJ0/e0F0AS9RcOXd55oGi2ZnRHIGmFvrTaNF+ZidRmGBmmNTkM5KKl0Z37KzxJ+owEQ==} typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} hasBin: true - typescript@6.0.3: - resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} - engines: {node: '>=14.17'} - hasBin: true - uc.micro@2.1.0: resolution: {integrity: sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==} @@ -3293,8 +3728,12 @@ packages: undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} - undici@8.3.0: - resolution: {integrity: sha512-TkUDgb6tl7KOGZ+7e8E3d2FYgUQgF6z5YypqjWmixVQSQERFcVrVg0ySADm2LVLRh5ljAaHTCR5Fmz3Q34rB7Q==} + undici@7.25.0: + resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==} + engines: {node: '>=20.18.1'} + + undici@8.2.0: + resolution: {integrity: sha512-Z+4Hx9GE26Lh9Upwfnc8C7SsrpBPGaM/Gm6kMFtiG7c+5IvQKlXi/t+9x9DrrCh29cww5TSP9YdVaBcnLDs5fQ==} engines: {node: '>=22.19.0'} unist-util-is@6.0.1: @@ -3319,6 +3758,15 @@ packages: util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + uuid@14.0.0: + resolution: {integrity: sha512-Qo+uWgilfSmAhXCMav1uYFynlQO7fMFiMVZsQqZRMIXp0O7rR7qjkj+cPvBHLgBqi960QCoo/PH2/6ZtVqKvrg==} + hasBin: true + + uuid@9.0.1: + resolution: {integrity: sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==} + deprecated: uuid@10 and below is no longer supported. For ESM codebases, update to uuid@latest. For CommonJS codebases, use uuid@11 (but be aware this version will likely be deprecated in 2028). + hasBin: true + vary@1.1.2: resolution: {integrity: sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==} engines: {node: '>= 0.8'} @@ -3551,6 +3999,10 @@ packages: resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==} engines: {node: '>=6'} + yargs-parser@20.2.9: + resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==} + engines: {node: '>=10'} + yargs-parser@21.1.1: resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} engines: {node: '>=12'} @@ -3559,10 +4011,21 @@ packages: resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==} engines: {node: '>=8'} + yargs@16.2.0: + resolution: {integrity: sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==} + engines: {node: '>=10'} + yargs@17.7.2: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yauzl@2.10.0: + resolution: {integrity: sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==} + + yoctocolors@2.1.2: + resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} + engines: {node: '>=18'} + zod-to-json-schema@3.25.1: resolution: {integrity: sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==} peerDependencies: @@ -3576,7 +4039,7 @@ packages: snapshots: - '@agentclientprotocol/sdk@0.21.1(zod@4.4.3)': + '@agentclientprotocol/sdk@0.21.0(zod@4.4.3)': dependencies: zod: 4.4.3 @@ -3703,6 +4166,21 @@ snapshots: optionalDependencies: zod: 4.4.3 + '@anthropic-ai/sdk@0.93.0(zod@4.4.3)': + dependencies: + json-schema-to-ts: 3.1.1 + optionalDependencies: + zod: 4.4.3 + + '@anthropic-ai/vertex-sdk@0.16.1(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.93.0(zod@4.4.3) + google-auth-library: 9.15.1 + transitivePeerDependencies: + - encoding + - supports-color + - zod + '@aws-crypto/crc32@5.2.0': dependencies: '@aws-crypto/util': 5.2.0 @@ -3735,6 +4213,56 @@ snapshots: '@smithy/util-utf8': 2.3.0 tslib: 2.8.1 + '@aws-sdk/client-bedrock-runtime@3.1042.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/credential-provider-node': 3.972.43 + '@aws-sdk/eventstream-handler-node': 3.972.16 + '@aws-sdk/middleware-eventstream': 3.972.12 + '@aws-sdk/middleware-host-header': 3.972.13 + '@aws-sdk/middleware-logger': 3.972.12 + '@aws-sdk/middleware-recursion-detection': 3.972.14 + '@aws-sdk/middleware-user-agent': 3.972.42 + '@aws-sdk/middleware-websocket': 3.972.20 + '@aws-sdk/region-config-resolver': 3.972.16 + '@aws-sdk/token-providers': 3.1042.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.11 + '@aws-sdk/util-user-agent-browser': 3.972.13 + '@aws-sdk/util-user-agent-node': 3.973.28 + '@smithy/config-resolver': 4.5.3 + '@smithy/core': 3.24.3 + '@smithy/eventstream-serde-browser': 4.3.3 + '@smithy/eventstream-serde-config-resolver': 4.4.3 + '@smithy/eventstream-serde-node': 4.3.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/hash-node': 4.3.3 + '@smithy/invalid-dependency': 4.3.3 + '@smithy/middleware-content-length': 4.3.3 + '@smithy/middleware-endpoint': 4.5.3 + '@smithy/middleware-retry': 4.6.3 + '@smithy/middleware-serde': 4.3.3 + '@smithy/middleware-stack': 4.3.3 + '@smithy/node-config-provider': 4.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/protocol-http': 5.4.3 + '@smithy/smithy-client': 4.13.3 + '@smithy/types': 4.14.2 + '@smithy/url-parser': 4.3.3 + '@smithy/util-base64': 4.4.3 + '@smithy/util-body-length-browser': 4.3.3 + '@smithy/util-body-length-node': 4.3.3 + '@smithy/util-defaults-mode-browser': 4.4.3 + '@smithy/util-defaults-mode-node': 4.3.3 + '@smithy/util-endpoints': 3.5.3 + '@smithy/util-middleware': 4.3.3 + '@smithy/util-retry': 4.4.3 + '@smithy/util-stream': 4.6.3 + '@smithy/util-utf8': 4.3.3 + tslib: 2.8.1 + '@aws-sdk/client-bedrock-runtime@3.1049.0': dependencies: '@aws-crypto/sha256-browser': 5.2.0 @@ -3752,6 +4280,62 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 + '@aws-sdk/client-bedrock@3.1042.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/credential-provider-node': 3.972.43 + '@aws-sdk/middleware-host-header': 3.972.13 + '@aws-sdk/middleware-logger': 3.972.12 + '@aws-sdk/middleware-recursion-detection': 3.972.14 + '@aws-sdk/middleware-user-agent': 3.972.42 + '@aws-sdk/region-config-resolver': 3.972.16 + '@aws-sdk/token-providers': 3.1042.0 + '@aws-sdk/types': 3.973.8 + '@aws-sdk/util-endpoints': 3.996.11 + '@aws-sdk/util-user-agent-browser': 3.972.13 + '@aws-sdk/util-user-agent-node': 3.973.28 + '@smithy/config-resolver': 4.5.3 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/hash-node': 4.3.3 + '@smithy/invalid-dependency': 4.3.3 + '@smithy/middleware-content-length': 4.3.3 + '@smithy/middleware-endpoint': 4.5.3 + '@smithy/middleware-retry': 4.6.3 + '@smithy/middleware-serde': 4.3.3 + '@smithy/middleware-stack': 4.3.3 + '@smithy/node-config-provider': 4.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/protocol-http': 5.4.3 + '@smithy/smithy-client': 4.13.3 + '@smithy/types': 4.14.2 + '@smithy/url-parser': 4.3.3 + '@smithy/util-base64': 4.4.3 + '@smithy/util-body-length-browser': 4.3.3 + '@smithy/util-body-length-node': 4.3.3 + '@smithy/util-defaults-mode-browser': 4.4.3 + '@smithy/util-defaults-mode-node': 4.3.3 + '@smithy/util-endpoints': 3.5.3 + '@smithy/util-middleware': 4.3.3 + '@smithy/util-retry': 4.4.3 + '@smithy/util-utf8': 4.3.3 + tslib: 2.8.1 + + '@aws-sdk/client-cognito-identity@3.1049.0': + dependencies: + '@aws-crypto/sha256-browser': 5.2.0 + '@aws-crypto/sha256-js': 5.2.0 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/credential-provider-node': 3.972.43 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/fetch-http-handler': 5.4.3 + '@smithy/node-http-handler': 4.7.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/core@3.974.12': dependencies: '@aws-sdk/types': 3.973.8 @@ -3763,6 +4347,14 @@ snapshots: bowser: 2.14.1 tslib: 2.8.1 + '@aws-sdk/credential-provider-cognito-identity@3.972.35': + dependencies: + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-env@3.972.38': dependencies: '@aws-sdk/core': 3.974.12 @@ -3806,6 +4398,21 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 + '@aws-sdk/credential-provider-node@3.972.39': + dependencies: + '@aws-sdk/credential-provider-env': 3.972.38 + '@aws-sdk/credential-provider-http': 3.972.40 + '@aws-sdk/credential-provider-ini': 3.972.42 + '@aws-sdk/credential-provider-process': 3.972.38 + '@aws-sdk/credential-provider-sso': 3.972.42 + '@aws-sdk/credential-provider-web-identity': 3.972.42 + '@aws-sdk/types': 3.973.8 + '@smithy/credential-provider-imds': 4.3.3 + '@smithy/property-provider': 4.3.3 + '@smithy/shared-ini-file-loader': 4.5.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/credential-provider-node@3.972.43': dependencies: '@aws-sdk/credential-provider-env': 3.972.38 @@ -3847,6 +4454,26 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 + '@aws-sdk/credential-providers@3.1049.0': + dependencies: + '@aws-sdk/client-cognito-identity': 3.1049.0 + '@aws-sdk/core': 3.974.12 + '@aws-sdk/credential-provider-cognito-identity': 3.972.35 + '@aws-sdk/credential-provider-env': 3.972.38 + '@aws-sdk/credential-provider-http': 3.972.40 + '@aws-sdk/credential-provider-ini': 3.972.42 + '@aws-sdk/credential-provider-login': 3.972.42 + '@aws-sdk/credential-provider-node': 3.972.43 + '@aws-sdk/credential-provider-process': 3.972.38 + '@aws-sdk/credential-provider-sso': 3.972.42 + '@aws-sdk/credential-provider-web-identity': 3.972.42 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/core': 3.24.3 + '@smithy/credential-provider-imds': 4.3.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/eventstream-handler-node@3.972.16': dependencies: '@aws-sdk/types': 3.973.8 @@ -3861,6 +4488,26 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 + '@aws-sdk/middleware-host-header@3.972.13': + dependencies: + '@aws-sdk/core': 3.974.12 + tslib: 2.8.1 + + '@aws-sdk/middleware-logger@3.972.12': + dependencies: + '@aws-sdk/core': 3.974.12 + tslib: 2.8.1 + + '@aws-sdk/middleware-recursion-detection@3.972.14': + dependencies: + '@aws-sdk/core': 3.974.12 + tslib: 2.8.1 + + '@aws-sdk/middleware-user-agent@3.972.42': + dependencies: + '@aws-sdk/core': 3.974.12 + tslib: 2.8.1 + '@aws-sdk/middleware-websocket@3.972.20': dependencies: '@aws-sdk/core': 3.974.12 @@ -3884,7 +4531,12 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 - '@aws-sdk/signature-v4-multi-region@3.996.27': + '@aws-sdk/region-config-resolver@3.972.16': + dependencies: + '@aws-sdk/core': 3.974.12 + tslib: 2.8.1 + + '@aws-sdk/signature-v4-multi-region@3.996.27': dependencies: '@aws-sdk/types': 3.973.8 '@smithy/core': 3.24.3 @@ -3892,6 +4544,16 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 + '@aws-sdk/token-providers@3.1042.0': + dependencies: + '@aws-sdk/core': 3.974.12 + '@aws-sdk/nested-clients': 3.997.10 + '@aws-sdk/types': 3.973.8 + '@smithy/property-provider': 4.3.3 + '@smithy/shared-ini-file-loader': 4.5.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@aws-sdk/token-providers@3.1049.0': dependencies: '@aws-sdk/core': 3.974.12 @@ -3906,10 +4568,31 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 + '@aws-sdk/util-endpoints@3.996.11': + dependencies: + '@aws-sdk/core': 3.974.12 + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@aws-sdk/util-format-url@3.972.14': + dependencies: + '@aws-sdk/core': 3.974.12 + tslib: 2.8.1 + '@aws-sdk/util-locate-window@3.965.5': dependencies: tslib: 2.8.1 + '@aws-sdk/util-user-agent-browser@3.972.13': + dependencies: + '@aws-sdk/core': 3.974.12 + tslib: 2.8.1 + + '@aws-sdk/util-user-agent-node@3.973.28': + dependencies: + '@aws-sdk/core': 3.974.12 + tslib: 2.8.1 + '@aws-sdk/xml-builder@3.972.24': dependencies: '@nodable/entities': 2.1.0 @@ -3917,6 +4600,18 @@ snapshots: fast-xml-parser: 5.7.3 tslib: 2.8.1 + '@aws/bedrock-token-generator@1.1.0': + dependencies: + '@aws-sdk/credential-providers': 3.1049.0 + '@aws-sdk/util-format-url': 3.972.14 + '@smithy/config-resolver': 4.5.3 + '@smithy/hash-node': 4.3.3 + '@smithy/invalid-dependency': 4.3.3 + '@smithy/node-config-provider': 4.4.3 + '@smithy/protocol-http': 5.4.3 + '@smithy/signature-v4': 5.4.3 + '@smithy/types': 4.14.2 + '@aws/lambda-invoke-store@0.2.4': {} '@babel/helper-string-parser@7.27.1': {} @@ -3978,79 +4673,6 @@ snapshots: transitivePeerDependencies: - '@algolia/client-search' - '@earendil-works/pi-agent-core@0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': - dependencies: - '@earendil-works/pi-ai': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - ignore: 7.0.5 - typebox: 1.1.38 - yaml: 2.9.0 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@earendil-works/pi-ai@0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': - dependencies: - '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) - '@aws-sdk/client-bedrock-runtime': 3.1049.0 - '@google/genai': 1.46.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) - '@mistralai/mistralai': 2.2.1 - http-proxy-agent: 7.0.2 - https-proxy-agent: 7.0.6 - openai: 6.26.0(ws@8.20.1)(zod@4.4.3) - partial-json: 0.1.7 - typebox: 1.1.38 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@earendil-works/pi-coding-agent@0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': - dependencies: - '@earendil-works/pi-agent-core': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-ai': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-tui': 0.75.1 - '@silvia-odwyer/photon-node': 0.3.4 - chalk: 5.6.2 - diff: 8.0.3 - glob: 13.0.6 - highlight.js: 10.7.3 - hosted-git-info: 9.0.2 - ignore: 7.0.5 - jiti: 2.7.0 - minimatch: 10.2.4 - proper-lockfile: 4.1.2 - typebox: 1.1.38 - undici: 8.3.0 - yaml: 2.9.0 - optionalDependencies: - '@mariozechner/clipboard': 0.3.6 - transitivePeerDependencies: - - '@modelcontextprotocol/sdk' - - bufferutil - - supports-color - - utf-8-validate - - ws - - zod - - '@earendil-works/pi-tui@0.75.1': - dependencies: - get-east-asian-width: 1.5.0 - marked: 15.0.12 - optionalDependencies: - koffi: 2.15.2 - - '@emnapi/runtime@1.9.1': - dependencies: - tslib: 2.8.1 - optional: true - '@esbuild/aix-ppc64@0.21.5': optional: true @@ -4198,20 +4820,7 @@ snapshots: '@esbuild/win32-x64@0.27.3': optional: true - '@google/genai@1.46.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': - dependencies: - google-auth-library: 10.6.2 - p-retry: 4.6.2 - protobufjs: 7.5.4 - ws: 8.20.1 - optionalDependencies: - '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) - transitivePeerDependencies: - - bufferutil - - supports-color - - utf-8-validate - - '@google/genai@2.3.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': + '@google/genai@1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))': dependencies: google-auth-library: 10.6.2 p-retry: 4.6.2 @@ -4224,17 +4833,17 @@ snapshots: - supports-color - utf-8-validate - '@grammyjs/runner@2.0.3(grammy@1.42.0)': + '@grammyjs/runner@2.0.3(grammy@1.43.0)': dependencies: abort-controller: 3.0.0 - grammy: 1.42.0 + grammy: 1.43.0 - '@grammyjs/transformer-throttler@1.2.1(grammy@1.42.0)': + '@grammyjs/transformer-throttler@1.2.1(grammy@1.43.0)': dependencies: bottleneck: 2.19.5 - grammy: 1.42.0 + grammy: 1.43.0 - '@grammyjs/types@3.26.0': {} + '@grammyjs/types@3.27.3': {} '@homebridge/ciao@1.3.8': dependencies: @@ -4245,9 +4854,9 @@ snapshots: transitivePeerDependencies: - supports-color - '@hono/node-server@1.19.11(hono@4.12.9)': + '@hono/node-server@1.19.11(hono@4.12.10)': dependencies: - hono: 4.12.9 + hono: 4.12.10 '@iconify-json/simple-icons@1.2.75': dependencies: @@ -4255,103 +4864,6 @@ snapshots: '@iconify/types@2.0.0': {} - '@img/colour@1.1.0': - optional: true - - '@img/sharp-darwin-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-arm64': 1.2.4 - optional: true - - '@img/sharp-darwin-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-darwin-x64': 1.2.4 - optional: true - - '@img/sharp-libvips-darwin-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-darwin-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-arm@1.2.4': - optional: true - - '@img/sharp-libvips-linux-ppc64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-riscv64@1.2.4': - optional: true - - '@img/sharp-libvips-linux-s390x@1.2.4': - optional: true - - '@img/sharp-libvips-linux-x64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-arm64@1.2.4': - optional: true - - '@img/sharp-libvips-linuxmusl-x64@1.2.4': - optional: true - - '@img/sharp-linux-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm64': 1.2.4 - optional: true - - '@img/sharp-linux-arm@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-arm': 1.2.4 - optional: true - - '@img/sharp-linux-ppc64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-ppc64': 1.2.4 - optional: true - - '@img/sharp-linux-riscv64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-riscv64': 1.2.4 - optional: true - - '@img/sharp-linux-s390x@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-s390x': 1.2.4 - optional: true - - '@img/sharp-linux-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linux-x64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-arm64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - optional: true - - '@img/sharp-linuxmusl-x64@0.34.5': - optionalDependencies: - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - optional: true - - '@img/sharp-wasm32@0.34.5': - dependencies: - '@emnapi/runtime': 1.9.1 - optional: true - - '@img/sharp-win32-arm64@0.34.5': - optional: true - - '@img/sharp-win32-ia32@0.34.5': - optional: true - - '@img/sharp-win32-x64@0.34.5': - optional: true - '@isaacs/cliui@8.0.2': dependencies: string-width: 5.1.2 @@ -4452,6 +4964,87 @@ snapshots: '@mariozechner/clipboard-win32-x64-msvc': 0.3.6 optional: true + '@mariozechner/jiti@2.6.5': + dependencies: + std-env: 3.10.0 + yoctocolors: 2.1.2 + + '@mariozechner/pi-agent-core@0.73.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + dependencies: + '@mariozechner/pi-ai': 0.73.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + typebox: 1.1.37 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@mariozechner/pi-ai@0.73.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + dependencies: + '@anthropic-ai/sdk': 0.91.1(zod@4.4.3) + '@aws-sdk/client-bedrock-runtime': 3.1049.0 + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) + '@mistralai/mistralai': 2.2.1 + chalk: 5.6.2 + openai: 6.26.0(ws@8.20.1)(zod@4.4.3) + partial-json: 0.1.7 + proxy-agent: 6.5.0 + typebox: 1.1.37 + undici: 7.25.0 + zod-to-json-schema: 3.25.1(zod@4.4.3) + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@mariozechner/pi-coding-agent@0.73.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3)': + dependencies: + '@mariozechner/jiti': 2.6.5 + '@mariozechner/pi-agent-core': 0.73.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@mariozechner/pi-ai': 0.73.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@mariozechner/pi-tui': 0.73.0 + '@silvia-odwyer/photon-node': 0.3.4 + chalk: 5.6.2 + cli-highlight: 2.1.11 + diff: 8.0.3 + extract-zip: 2.0.1 + file-type: 21.3.4 + glob: 13.0.6 + hosted-git-info: 9.0.2 + ignore: 7.0.5 + marked: 15.0.12 + minimatch: 10.2.5 + proper-lockfile: 4.1.2 + strip-ansi: 7.1.2 + typebox: 1.1.37 + undici: 7.25.0 + uuid: 14.0.0 + yaml: 2.9.0 + optionalDependencies: + '@mariozechner/clipboard': 0.3.6 + transitivePeerDependencies: + - '@modelcontextprotocol/sdk' + - bufferutil + - supports-color + - utf-8-validate + - ws + - zod + + '@mariozechner/pi-tui@0.73.0': + dependencies: + '@types/mime-types': 2.1.4 + chalk: 5.6.2 + get-east-asian-width: 1.5.0 + marked: 15.0.12 + mime-types: 3.0.2 + optionalDependencies: + koffi: 2.15.2 + '@mistralai/mistralai@2.2.1': dependencies: ws: 8.20.1 @@ -4463,7 +5056,7 @@ snapshots: '@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)': dependencies: - '@hono/node-server': 1.19.11(hono@4.12.9) + '@hono/node-server': 1.19.11(hono@4.12.10) ajv: 8.20.0 ajv-formats: 3.0.1(ajv@8.20.0) content-type: 1.0.5 @@ -4473,7 +5066,7 @@ snapshots: eventsource-parser: 3.0.6 express: 5.2.1 express-rate-limit: 8.3.1(express@5.2.1) - hono: 4.12.9 + hono: 4.12.10 jose: 6.2.2 json-schema-typed: 8.0.2 pkce-challenge: 5.0.1 @@ -4578,15 +5171,6 @@ snapshots: '@nodable/entities@2.1.0': {} - '@openclaw/fs-safe@0.2.4': - optionalDependencies: - jszip: 3.10.1 - tar: 7.5.13 - - '@openclaw/proxyline@0.3.3(undici@8.3.0)': - dependencies: - undici: 8.3.0 - '@oxfmt/binding-android-arm-eabi@0.34.0': optional: true @@ -4862,6 +5446,79 @@ snapshots: '@silvia-odwyer/photon-node@0.3.4': {} + '@slack/bolt@4.7.2(@types/express@5.0.6)': + dependencies: + '@slack/logger': 4.0.1 + '@slack/oauth': 3.0.5 + '@slack/socket-mode': 2.0.7 + '@slack/types': 2.21.1 + '@slack/web-api': 7.16.0 + '@types/express': 5.0.6 + axios: 1.13.6(debug@4.4.3) + express: 5.2.1 + path-to-regexp: 8.3.0 + raw-body: 3.0.2 + tsscmp: 1.0.6 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + '@slack/logger@4.0.1': + dependencies: + '@types/node': 25.2.0 + + '@slack/oauth@3.0.5': + dependencies: + '@slack/logger': 4.0.1 + '@slack/web-api': 7.16.0 + '@types/jsonwebtoken': 9.0.10 + '@types/node': 25.2.0 + jsonwebtoken: 9.0.3 + transitivePeerDependencies: + - debug + - supports-color + + '@slack/socket-mode@2.0.7': + dependencies: + '@slack/logger': 4.0.1 + '@slack/web-api': 7.16.0 + '@types/node': 25.2.0 + '@types/ws': 8.18.1 + eventemitter3: 5.0.4 + ws: 8.20.1 + transitivePeerDependencies: + - bufferutil + - debug + - supports-color + - utf-8-validate + + '@slack/types@2.21.1': {} + + '@slack/web-api@7.16.0': + dependencies: + '@slack/logger': 4.0.1 + '@slack/types': 2.21.1 + '@types/node': 25.2.0 + '@types/retry': 0.12.0 + axios: 1.16.1 + eventemitter3: 5.0.4 + form-data: 4.0.5 + is-electron: 2.2.2 + is-stream: 2.0.1 + p-queue: 6.6.2 + p-retry: 4.6.2 + retry: 0.13.1 + transitivePeerDependencies: + - debug + - supports-color + + '@smithy/config-resolver@4.5.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + '@smithy/core@3.24.3': dependencies: '@aws-crypto/crc32': 5.2.0 @@ -4874,42 +5531,175 @@ snapshots: '@smithy/types': 4.14.2 tslib: 2.8.1 + '@smithy/eventstream-serde-browser@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/eventstream-serde-config-resolver@4.4.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/eventstream-serde-node@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + '@smithy/fetch-http-handler@5.4.3': dependencies: '@smithy/core': 3.24.3 '@smithy/types': 4.14.2 tslib: 2.8.1 + '@smithy/hash-node@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/invalid-dependency@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + '@smithy/is-array-buffer@2.2.0': dependencies: tslib: 2.8.1 + '@smithy/middleware-content-length@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/middleware-endpoint@4.5.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/middleware-retry@4.6.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/middleware-serde@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/middleware-stack@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/node-config-provider@4.4.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + '@smithy/node-http-handler@4.7.3': dependencies: '@smithy/core': 3.24.3 '@smithy/types': 4.14.2 tslib: 2.8.1 + '@smithy/property-provider@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/protocol-http@5.4.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/shared-ini-file-loader@4.5.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + '@smithy/signature-v4@5.4.3': dependencies: '@smithy/core': 3.24.3 '@smithy/types': 4.14.2 tslib: 2.8.1 + '@smithy/smithy-client@4.13.3': + dependencies: + '@smithy/core': 3.24.3 + '@smithy/types': 4.14.2 + tslib: 2.8.1 + '@smithy/types@4.14.2': dependencies: tslib: 2.8.1 + '@smithy/url-parser@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/util-base64@4.4.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/util-body-length-browser@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/util-body-length-node@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + '@smithy/util-buffer-from@2.2.0': dependencies: '@smithy/is-array-buffer': 2.2.0 tslib: 2.8.1 + '@smithy/util-defaults-mode-browser@4.4.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/util-defaults-mode-node@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/util-endpoints@3.5.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/util-middleware@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/util-retry@4.4.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@smithy/util-stream@4.6.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + '@smithy/util-utf8@2.3.0': dependencies: '@smithy/util-buffer-from': 2.2.0 tslib: 2.8.1 + '@smithy/util-utf8@4.3.3': + dependencies: + '@smithy/core': 3.24.3 + tslib: 2.8.1 + + '@telegraf/types@7.1.0': {} + '@tokenizer/inflate@0.4.1': dependencies: debug: 4.4.3 @@ -4919,19 +5709,50 @@ snapshots: '@tokenizer/token@0.3.0': {} + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@types/body-parser@1.19.6': + dependencies: + '@types/connect': 3.4.38 + '@types/node': 25.2.0 + '@types/chai@5.2.3': dependencies: '@types/deep-eql': 4.0.2 assertion-error: 2.0.1 + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.2.0 + '@types/deep-eql@4.0.2': {} '@types/estree@1.0.8': {} + '@types/express-serve-static-core@5.1.1': + dependencies: + '@types/node': 25.2.0 + '@types/qs': 6.15.1 + '@types/range-parser': 1.2.7 + '@types/send': 1.2.1 + + '@types/express@5.0.6': + dependencies: + '@types/body-parser': 1.19.6 + '@types/express-serve-static-core': 5.1.1 + '@types/serve-static': 2.2.0 + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 + '@types/http-errors@2.0.5': {} + + '@types/jsonwebtoken@9.0.10': + dependencies: + '@types/ms': 2.1.0 + '@types/node': 25.2.0 + '@types/linkify-it@5.0.0': {} '@types/markdown-it@14.1.2': @@ -4945,16 +5766,42 @@ snapshots: '@types/mdurl@2.0.0': {} + '@types/mime-types@2.1.4': {} + + '@types/ms@2.1.0': {} + '@types/node@25.2.0': dependencies: undici-types: 7.16.0 + '@types/qs@6.15.1': {} + + '@types/range-parser@1.2.7': {} + '@types/retry@0.12.0': {} + '@types/send@1.2.1': + dependencies: + '@types/node': 25.2.0 + + '@types/serve-static@2.2.0': + dependencies: + '@types/http-errors': 2.0.5 + '@types/node': 25.2.0 + '@types/unist@3.0.3': {} '@types/web-bluetooth@0.0.21': {} + '@types/ws@8.18.1': + dependencies: + '@types/node': 25.2.0 + + '@types/yauzl@2.10.3': + dependencies: + '@types/node': 25.2.0 + optional: true + '@ungap/structured-clone@1.3.0': {} '@vitejs/plugin-vue@5.2.4(vite@5.4.21(@types/node@25.2.0))(vue@3.5.31(typescript@5.9.3))': @@ -5136,8 +5983,16 @@ snapshots: mime-types: 3.0.2 negotiator: 1.0.0 + agent-base@6.0.2: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + agent-base@7.1.4: {} + agent-base@9.0.0: {} + ajv-formats@3.0.1(ajv@8.20.0): optionalDependencies: ajv: 8.20.0 @@ -5176,6 +6031,8 @@ snapshots: ansi-styles@6.2.3: {} + any-promise@1.3.0: {} + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -5191,6 +6048,10 @@ snapshots: assertion-error@2.0.1: {} + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + ast-v8-to-istanbul@0.3.11: dependencies: '@jridgewell/trace-mapping': 0.3.31 @@ -5207,10 +6068,22 @@ snapshots: transitivePeerDependencies: - debug + axios@1.16.1: + dependencies: + follow-redirects: 1.16.0 + form-data: 4.0.5 + https-proxy-agent: 5.0.1 + proxy-from-env: 2.1.0 + transitivePeerDependencies: + - debug + - supports-color + balanced-match@4.0.3: {} base64-js@1.5.1: {} + basic-ftp@5.3.1: {} + bignumber.js@9.3.1: {} birpc@2.9.0: {} @@ -5243,8 +6116,23 @@ snapshots: dependencies: balanced-match: 4.0.3 + brace-expansion@5.0.6: + dependencies: + balanced-match: 4.0.3 + + buffer-alloc-unsafe@1.1.0: {} + + buffer-alloc@1.2.0: + dependencies: + buffer-alloc-unsafe: 1.1.0 + buffer-fill: 1.0.0 + + buffer-crc32@0.2.13: {} + buffer-equal-constant-time@1.0.1: {} + buffer-fill@1.0.0: {} + buffer-from@1.1.2: {} bytes@3.1.2: {} @@ -5273,6 +6161,11 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + chalk@5.6.2: {} character-entities-html4@2.1.0: {} @@ -5287,11 +6180,26 @@ snapshots: chownr@3.0.0: {} - cliui@6.0.0: + cli-highlight@2.1.11: + dependencies: + chalk: 4.1.2 + highlight.js: 10.7.3 + mz: 2.7.0 + parse5: 5.1.1 + parse5-htmlparser2-tree-adapter: 6.0.1 + yargs: 16.2.0 + + cliui@6.0.0: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + + cliui@7.0.4: dependencies: string-width: 4.2.3 strip-ansi: 6.0.1 - wrap-ansi: 6.2.0 + wrap-ansi: 7.0.0 cliui@8.0.1: dependencies: @@ -5356,6 +6264,10 @@ snapshots: data-uri-to-buffer@4.0.1: {} + data-uri-to-buffer@6.0.2: {} + + data-uri-to-buffer@8.0.0: {} + debug@4.4.3: dependencies: ms: 2.1.3 @@ -5364,15 +6276,37 @@ snapshots: deep-eql@5.0.2: {} + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + degenerator@7.0.1(quickjs-wasi@2.2.0): + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + quickjs-wasi: 2.2.0 + delayed-stream@1.0.0: {} depd@2.0.0: {} dequal@2.0.3: {} - detect-libc@2.1.2: - optional: true - devlop@1.1.0: dependencies: dequal: 2.0.3 @@ -5411,6 +6345,8 @@ snapshots: domelementtype: 2.3.0 domhandler: 5.0.3 + dotenv@16.6.1: {} + dotenv@17.4.2: {} duck@0.1.12: @@ -5439,6 +6375,10 @@ snapshots: encodeurl@2.0.0: {} + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + entities@4.5.0: {} entities@7.0.1: {} @@ -5519,16 +6459,36 @@ snapshots: escape-html@1.0.3: {} + escape-string-regexp@4.0.0: {} + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + estree-walker@2.0.2: {} estree-walker@3.0.3: dependencies: '@types/estree': 1.0.8 + esutils@2.0.3: {} + etag@1.8.1: {} event-target-shim@5.0.1: {} + eventemitter3@4.0.7: {} + + eventemitter3@5.0.4: {} + eventsource-parser@3.0.6: {} eventsource@3.0.7: @@ -5577,6 +6537,16 @@ snapshots: extend@3.0.2: {} + extract-zip@2.0.1: + dependencies: + debug: 4.4.3 + get-stream: 5.2.0 + yauzl: 2.10.0 + optionalDependencies: + '@types/yauzl': 2.10.3 + transitivePeerDependencies: + - supports-color + fast-deep-equal@3.1.3: {} fast-string-truncated-width@3.0.3: {} @@ -5603,6 +6573,10 @@ snapshots: path-expression-matcher: 1.5.0 strnum: 2.3.0 + fd-slicer@1.1.0: + dependencies: + pend: 1.2.0 + fdir@6.5.0(picomatch@4.0.3): optionalDependencies: picomatch: 4.0.3 @@ -5612,6 +6586,15 @@ snapshots: node-domexception: 1.0.0 web-streams-polyfill: 3.3.3 + file-type@21.3.4: + dependencies: + '@tokenizer/inflate': 0.4.1 + strtok3: 10.3.5 + token-types: 6.1.2 + uint8array-extras: 1.5.0 + transitivePeerDependencies: + - supports-color + file-type@22.0.1: dependencies: '@tokenizer/inflate': 0.4.1 @@ -5645,6 +6628,8 @@ snapshots: optionalDependencies: debug: 4.4.3 + follow-redirects@1.16.0: {} + foreground-child@3.3.1: dependencies: cross-spawn: 7.0.6 @@ -5671,6 +6656,17 @@ snapshots: function-bind@1.1.2: {} + gaxios@6.7.1: + dependencies: + extend: 3.0.2 + https-proxy-agent: 7.0.6 + is-stream: 2.0.1 + node-fetch: 2.7.0 + uuid: 9.0.1 + transitivePeerDependencies: + - encoding + - supports-color + gaxios@7.1.4: dependencies: extend: 3.0.2 @@ -5679,6 +6675,15 @@ snapshots: transitivePeerDependencies: - supports-color + gcp-metadata@6.1.1: + dependencies: + gaxios: 6.7.1 + google-logging-utils: 0.0.2 + json-bigint: 1.0.0 + transitivePeerDependencies: + - encoding + - supports-color + gcp-metadata@8.1.2: dependencies: gaxios: 7.1.4 @@ -5709,6 +6714,26 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.3.1 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + get-uri@8.0.0: + dependencies: + basic-ftp: 5.3.1 + data-uri-to-buffer: 8.0.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + glob@10.5.0: dependencies: foreground-child: 3.3.1 @@ -5720,10 +6745,22 @@ snapshots: glob@13.0.6: dependencies: - minimatch: 10.2.4 + minimatch: 10.2.5 minipass: 7.1.3 path-scurry: 2.0.2 + global-agent@4.1.3: + dependencies: + globalthis: 1.0.4 + matcher: 4.0.0 + semver: 7.7.3 + serialize-error: 8.1.0 + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + google-auth-library@10.6.2: dependencies: base64-js: 1.5.1 @@ -5735,15 +6772,29 @@ snapshots: transitivePeerDependencies: - supports-color + google-auth-library@9.15.1: + dependencies: + base64-js: 1.5.1 + ecdsa-sig-formatter: 1.0.11 + gaxios: 6.7.1 + gcp-metadata: 6.1.1 + gtoken: 7.1.0 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + + google-logging-utils@0.0.2: {} + google-logging-utils@1.1.3: {} gopd@1.2.0: {} graceful-fs@4.2.11: {} - grammy@1.42.0: + grammy@1.43.0: dependencies: - '@grammyjs/types': 3.26.0 + '@grammyjs/types': 3.27.3 abort-controller: 3.0.0 debug: 4.4.3 node-fetch: 2.7.0 @@ -5751,8 +6802,20 @@ snapshots: - encoding - supports-color + gtoken@7.1.0: + dependencies: + gaxios: 6.7.1 + jws: 4.0.1 + transitivePeerDependencies: + - encoding + - supports-color + has-flag@4.0.0: {} + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + has-symbols@1.1.0: {} has-tostringtag@1.0.2: @@ -5783,7 +6846,7 @@ snapshots: highlight.js@10.7.3: {} - hono@4.12.9: {} + hono@4.12.10: {} hookable@5.5.3: {} @@ -5819,8 +6882,22 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy-agent@9.0.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + http_ece@1.2.0: {} + https-proxy-agent@5.0.1: + dependencies: + agent-base: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + https-proxy-agent@7.0.6: dependencies: agent-base: 7.1.4 @@ -5828,6 +6905,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@9.0.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 @@ -5842,14 +6926,20 @@ snapshots: ip-address@10.1.0: {} + ip-address@10.2.0: {} + ipaddr.js@1.9.1: {} ipaddr.js@2.4.0: {} + is-electron@2.2.2: {} + is-fullwidth-code-point@3.0.0: {} is-promise@4.0.0: {} + is-stream@2.0.1: {} + is-what@5.5.0: {} isarray@1.0.0: {} @@ -5906,6 +6996,19 @@ snapshots: json5@2.2.3: {} + jsonwebtoken@9.0.3: + dependencies: + jws: 4.0.1 + lodash.includes: 4.3.0 + lodash.isboolean: 3.0.3 + lodash.isinteger: 4.0.4 + lodash.isnumber: 3.0.3 + lodash.isplainobject: 4.0.6 + lodash.isstring: 4.0.1 + lodash.once: 4.1.1 + ms: 2.1.3 + semver: 7.7.3 + jszip@3.10.1: dependencies: lie: 3.3.0 @@ -5930,8 +7033,6 @@ snapshots: koffi@2.15.2: optional: true - kysely@0.29.1: {} - lie@3.3.0: dependencies: immediate: 3.0.6 @@ -5952,6 +7053,20 @@ snapshots: dependencies: p-locate: 4.1.0 + lodash.includes@4.3.0: {} + + lodash.isboolean@3.0.3: {} + + lodash.isinteger@4.0.4: {} + + lodash.isnumber@3.0.3: {} + + lodash.isplainobject@4.0.6: {} + + lodash.isstring@4.0.1: {} + + lodash.once@4.1.1: {} + long@5.3.2: {} lop@0.4.2: @@ -5966,6 +7081,8 @@ snapshots: lru-cache@11.2.7: {} + lru-cache@7.18.3: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6006,6 +7123,10 @@ snapshots: marked@15.0.12: {} + matcher@4.0.0: + dependencies: + escape-string-regexp: 4.0.0 + math-intrinsics@1.1.0: {} mdast-util-to-hast@13.2.1: @@ -6057,9 +7178,9 @@ snapshots: minimalistic-assert@1.0.1: {} - minimatch@10.2.4: + minimatch@10.2.5: dependencies: - brace-expansion: 5.0.2 + brace-expansion: 5.0.6 minimatch@9.0.6: dependencies: @@ -6077,12 +7198,22 @@ snapshots: mitt@3.0.1: {} + mri@1.2.0: {} + ms@2.1.3: {} + mz@2.7.0: + dependencies: + any-promise: 1.3.0 + object-assign: 4.1.1 + thenify-all: 1.6.0 + nanoid@3.3.11: {} negotiator@1.0.0: {} + netmask@2.1.1: {} + node-addon-api@8.7.0: {} node-domexception@1.0.0: {} @@ -6117,6 +7248,8 @@ snapshots: object-inspect@1.13.4: {} + object-keys@1.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -6141,24 +7274,30 @@ snapshots: ws: 8.20.1 zod: 4.4.3 - openclaw@2026.5.18: + openclaw@2026.5.7(@types/express@5.0.6): dependencies: - '@agentclientprotocol/sdk': 0.21.1(zod@4.4.3) - '@clack/core': 1.3.1 + '@agentclientprotocol/sdk': 0.21.0(zod@4.4.3) + '@anthropic-ai/sdk': 0.93.0(zod@4.4.3) + '@anthropic-ai/vertex-sdk': 0.16.1(zod@4.4.3) + '@aws-sdk/client-bedrock': 3.1042.0 + '@aws-sdk/client-bedrock-runtime': 3.1042.0 + '@aws-sdk/credential-provider-node': 3.972.39 + '@aws/bedrock-token-generator': 1.1.0 '@clack/prompts': 1.4.0 - '@earendil-works/pi-agent-core': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-ai': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-coding-agent': 0.75.1(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) - '@earendil-works/pi-tui': 0.75.1 - '@google/genai': 2.3.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) - '@grammyjs/runner': 2.0.3(grammy@1.42.0) - '@grammyjs/transformer-throttler': 1.2.1(grammy@1.42.0) + '@google/genai': 1.52.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3)) + '@grammyjs/runner': 2.0.3(grammy@1.43.0) + '@grammyjs/transformer-throttler': 1.2.1(grammy@1.43.0) '@homebridge/ciao': 1.3.8 '@lydell/node-pty': 1.2.0-beta.12 + '@mariozechner/pi-agent-core': 0.73.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@mariozechner/pi-ai': 0.73.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@mariozechner/pi-coding-agent': 0.73.0(@modelcontextprotocol/sdk@1.29.0(zod@4.4.3))(ws@8.20.1)(zod@4.4.3) + '@mariozechner/pi-tui': 0.73.0 '@modelcontextprotocol/sdk': 1.29.0(zod@4.4.3) '@mozilla/readability': 0.6.0 - '@openclaw/fs-safe': 0.2.4 - '@openclaw/proxyline': 0.3.3(undici@8.3.0) + '@slack/bolt': 4.7.2(@types/express@5.0.6) + '@slack/types': 2.21.1 + '@slack/web-api': 7.16.0 ajv: 8.20.0 chalk: 5.6.2 chokidar: 5.0.0 @@ -6167,44 +7306,55 @@ snapshots: dotenv: 17.4.2 express: 5.2.1 file-type: 22.0.1 - grammy: 1.42.0 + global-agent: 4.1.3 + grammy: 1.43.0 + https-proxy-agent: 9.0.0 ipaddr.js: 2.4.0 jiti: 2.7.0 json5: 2.2.3 jszip: 3.10.1 - kysely: 0.29.1 linkedom: 0.18.12 markdown-it: 14.1.1 + minimatch: 10.2.5 node-edge-tts: 1.2.10 openai: 6.38.0(ws@8.20.1)(zod@4.4.3) + openshell: 0.1.0 pdfjs-dist: 5.7.284 - playwright-core: 1.60.0 + playwright-core: 1.59.1 + proxy-agent: 8.0.1 qrcode: 1.5.4 - quickjs-wasi: 2.2.0 - tar: 7.5.15 - tokenjuice: 0.7.1 + tar: 7.5.13 + tokenjuice: 0.7.0 tree-sitter-bash: 0.25.1 tslog: 4.10.2 - typebox: 1.1.38 - typescript: 6.0.3 - undici: 8.3.0 + typebox: 1.1.37 + undici: 8.2.0 web-push: 3.6.7 web-tree-sitter: 0.26.8 ws: 8.20.1 yaml: 2.9.0 zod: 4.4.3 optionalDependencies: - sharp: 0.34.5 sqlite-vec: 0.1.9 transitivePeerDependencies: - '@cfworker/json-schema' + - '@types/express' - bufferutil - canvas + - debug - encoding - supports-color - tree-sitter - utf-8-validate + openshell@0.1.0: + dependencies: + dotenv: 16.6.1 + telegraf: 4.16.3 + transitivePeerDependencies: + - encoding + - supports-color + option@0.2.4: {} oxfmt@0.34.0: @@ -6263,6 +7413,8 @@ snapshots: '@oxlint/binding-win32-x64-msvc': 1.49.0 oxlint-tsgolint: 0.18.1 + p-finally@1.0.0: {} + p-limit@2.3.0: dependencies: p-try: 2.2.0 @@ -6271,17 +7423,73 @@ snapshots: dependencies: p-limit: 2.3.0 + p-queue@6.6.2: + dependencies: + eventemitter3: 4.0.7 + p-timeout: 3.2.0 + p-retry@4.6.2: dependencies: '@types/retry': 0.12.0 retry: 0.13.1 + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-timeout@4.1.0: {} + p-try@2.2.0: {} + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-proxy-agent@9.0.1: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + get-uri: 8.0.0 + http-proxy-agent: 9.0.0 + https-proxy-agent: 9.0.0 + pac-resolver: 9.0.1(quickjs-wasi@2.2.0) + quickjs-wasi: 2.2.0 + socks-proxy-agent: 10.0.0 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.1.1 + + pac-resolver@9.0.1(quickjs-wasi@2.2.0): + dependencies: + degenerator: 7.0.1(quickjs-wasi@2.2.0) + netmask: 2.1.1 + quickjs-wasi: 2.2.0 + package-json-from-dist@1.0.1: {} pako@1.0.11: {} + parse5-htmlparser2-tree-adapter@6.0.1: + dependencies: + parse5: 6.0.1 + + parse5@5.1.1: {} + + parse5@6.0.1: {} + parseurl@1.3.3: {} partial-json@0.1.7: {} @@ -6323,6 +7531,8 @@ snapshots: optionalDependencies: '@napi-rs/canvas': 0.1.100 + pend@1.2.0: {} + perfect-debounce@1.0.0: {} picocolors@1.1.1: {} @@ -6331,7 +7541,7 @@ snapshots: pkce-challenge@5.0.1: {} - playwright-core@1.60.0: {} + playwright-core@1.59.1: {} pngjs@5.0.0: {} @@ -6379,8 +7589,41 @@ snapshots: forwarded: 0.2.0 ipaddr.js: 1.9.1 + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-agent@8.0.1: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + http-proxy-agent: 9.0.0 + https-proxy-agent: 9.0.0 + lru-cache: 7.18.3 + pac-proxy-agent: 9.0.1 + proxy-from-env: 2.1.0 + socks-proxy-agent: 10.0.0 + transitivePeerDependencies: + - supports-color + proxy-from-env@1.1.0: {} + proxy-from-env@2.1.0: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + punycode.js@2.3.1: {} qrcode@1.5.4: @@ -6481,8 +7724,14 @@ snapshots: safe-buffer@5.1.2: {} + safe-compare@1.1.4: + dependencies: + buffer-alloc: 1.2.0 + safer-buffer@2.1.2: {} + sandwich-stream@2.0.2: {} + search-insights@2.17.3: {} semver@7.7.3: {} @@ -6503,6 +7752,10 @@ snapshots: transitivePeerDependencies: - supports-color + serialize-error@8.1.0: + dependencies: + type-fest: 0.20.2 + serve-static@2.2.1: dependencies: encodeurl: 2.0.0 @@ -6518,38 +7771,6 @@ snapshots: setprototypeof@1.2.0: {} - sharp@0.34.5: - dependencies: - '@img/colour': 1.1.0 - detect-libc: 2.1.2 - semver: 7.7.3 - optionalDependencies: - '@img/sharp-darwin-arm64': 0.34.5 - '@img/sharp-darwin-x64': 0.34.5 - '@img/sharp-libvips-darwin-arm64': 1.2.4 - '@img/sharp-libvips-darwin-x64': 1.2.4 - '@img/sharp-libvips-linux-arm': 1.2.4 - '@img/sharp-libvips-linux-arm64': 1.2.4 - '@img/sharp-libvips-linux-ppc64': 1.2.4 - '@img/sharp-libvips-linux-riscv64': 1.2.4 - '@img/sharp-libvips-linux-s390x': 1.2.4 - '@img/sharp-libvips-linux-x64': 1.2.4 - '@img/sharp-libvips-linuxmusl-arm64': 1.2.4 - '@img/sharp-libvips-linuxmusl-x64': 1.2.4 - '@img/sharp-linux-arm': 0.34.5 - '@img/sharp-linux-arm64': 0.34.5 - '@img/sharp-linux-ppc64': 0.34.5 - '@img/sharp-linux-riscv64': 0.34.5 - '@img/sharp-linux-s390x': 0.34.5 - '@img/sharp-linux-x64': 0.34.5 - '@img/sharp-linuxmusl-arm64': 0.34.5 - '@img/sharp-linuxmusl-x64': 0.34.5 - '@img/sharp-wasm32': 0.34.5 - '@img/sharp-win32-arm64': 0.34.5 - '@img/sharp-win32-ia32': 0.34.5 - '@img/sharp-win32-x64': 0.34.5 - optional: true - shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 @@ -6603,6 +7824,29 @@ snapshots: sisteransi@1.0.5: {} + smart-buffer@4.2.0: {} + + socks-proxy-agent@10.0.0: + dependencies: + agent-base: 9.0.0 + debug: 4.4.3 + socks: 2.8.9 + transitivePeerDependencies: + - supports-color + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.9 + transitivePeerDependencies: + - supports-color + + socks@2.8.9: + dependencies: + ip-address: 10.2.0 + smart-buffer: 4.2.0 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -6704,15 +7948,20 @@ snapshots: minipass: 7.1.3 minizlib: 3.1.0 yallist: 5.0.0 - optional: true - tar@7.5.15: + telegraf@4.16.3: dependencies: - '@isaacs/fs-minipass': 4.0.1 - chownr: 3.0.0 - minipass: 7.1.3 - minizlib: 3.1.0 - yallist: 5.0.0 + '@telegraf/types': 7.1.0 + abort-controller: 3.0.0 + debug: 4.4.3 + mri: 1.2.0 + node-fetch: 2.7.0 + p-timeout: 4.1.0 + safe-compare: 1.1.4 + sandwich-stream: 2.0.2 + transitivePeerDependencies: + - encoding + - supports-color test-exclude@7.0.1: dependencies: @@ -6720,6 +7969,14 @@ snapshots: glob: 10.5.0 minimatch: 9.0.6 + thenify-all@1.6.0: + dependencies: + thenify: 3.3.1 + + thenify@3.3.1: + dependencies: + any-promise: 1.3.0 + tinybench@2.9.0: {} tinyexec@0.3.2: {} @@ -6745,7 +8002,7 @@ snapshots: '@tokenizer/token': 0.3.0 ieee754: 1.2.1 - tokenjuice@0.7.1: {} + tokenjuice@0.7.0: {} tr46@0.0.3: {} @@ -6762,18 +8019,20 @@ snapshots: tslog@4.10.2: {} + tsscmp@1.0.6: {} + + type-fest@0.20.2: {} + type-is@2.0.1: dependencies: content-type: 1.0.5 media-typer: 1.1.0 mime-types: 3.0.2 - typebox@1.1.38: {} + typebox@1.1.37: {} typescript@5.9.3: {} - typescript@6.0.3: {} - uc.micro@2.1.0: {} uhyphen@0.2.0: {} @@ -6784,7 +8043,9 @@ snapshots: undici-types@7.16.0: {} - undici@8.3.0: {} + undici@7.25.0: {} + + undici@8.2.0: {} unist-util-is@6.0.1: dependencies: @@ -6813,6 +8074,10 @@ snapshots: util-deprecate@1.0.2: {} + uuid@14.0.0: {} + + uuid@9.0.1: {} + vary@1.1.2: {} vfile-message@4.0.3: @@ -7042,6 +8307,8 @@ snapshots: camelcase: 5.3.1 decamelize: 1.2.0 + yargs-parser@20.2.9: {} + yargs-parser@21.1.1: {} yargs@15.4.1: @@ -7058,6 +8325,16 @@ snapshots: y18n: 4.0.3 yargs-parser: 18.1.3 + yargs@16.2.0: + dependencies: + cliui: 7.0.4 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 20.2.9 + yargs@17.7.2: dependencies: cliui: 8.0.1 @@ -7068,6 +8345,13 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yauzl@2.10.0: + dependencies: + buffer-crc32: 0.2.13 + fd-slicer: 1.1.0 + + yoctocolors@2.1.2: {} + zod-to-json-schema@3.25.1(zod@4.4.3): dependencies: zod: 4.4.3 diff --git a/tests/unit/plugin-manifest.test.ts b/tests/unit/plugin-manifest.test.ts index 67ab72ac..41565ce4 100644 --- a/tests/unit/plugin-manifest.test.ts +++ b/tests/unit/plugin-manifest.test.ts @@ -140,9 +140,9 @@ describe("plugin manifest channel metadata", () => { }; }>("package.json"); - expect(packageJson.peerDependencies?.openclaw).toBe(">=2026.4.7"); - expect(packageJson.openclaw?.compat?.pluginApi).toBe(">=2026.4.7"); - expect(packageJson.openclaw?.build?.openclawVersion).toBe("2026.4.7"); - expect(packageJson.openclaw?.install?.minHostVersion).toBe(">=2026.4.7"); + expect(packageJson.peerDependencies?.openclaw).toBe(">=2026.5.7"); + expect(packageJson.openclaw?.compat?.pluginApi).toBe(">=2026.5.7"); + expect(packageJson.openclaw?.build?.openclawVersion).toBe("2026.5.7"); + expect(packageJson.openclaw?.install?.minHostVersion).toBe(">=2026.5.7"); }); }); diff --git a/tests/unit/sdk-import-structure.test.ts b/tests/unit/sdk-import-structure.test.ts index 91b2a6dd..fe600e3f 100644 --- a/tests/unit/sdk-import-structure.test.ts +++ b/tests/unit/sdk-import-structure.test.ts @@ -89,7 +89,7 @@ describe("plugin-sdk import structure", () => { }; expect(packageJson.devDependencies?.openclaw).toBeUndefined(); expect(packageJson.peerDependencies?.openclaw).toBeDefined(); - expect(packageJson.peerDependencies?.openclaw).toBe(">=2026.4.7"); - expect(packageJson.openclaw?.install?.minHostVersion).toBe(">=2026.4.7"); + expect(packageJson.peerDependencies?.openclaw).toBe(">=2026.5.7"); + expect(packageJson.openclaw?.install?.minHostVersion).toBe(">=2026.5.7"); }); }); From bb76790fcc1e55de5b8fa03b5e941e9fa4b453d5 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 17:20:41 +0800 Subject: [PATCH 32/44] ci: bump Node to 22 to match openclaw engines and undici 8.x MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `openclaw@2026.5.7` (this PR's pinned baseline) pulls `undici@8.x`, which declares `engines.node: ">=22.19.0"` and uses `webidl.util.markAsUncloneable` — a Node 21.3+ API. The previous `node-version: 20` therefore fails 28 test files with `TypeError: webidl.util.markAsUncloneable is not a function` at undici CacheStorage construction. Node 22 aligns CI with the upstream openclaw `engines.node: ">=22.16.0"` floor and satisfies undici 8.x's stricter `>=22.19.0` (setup-node@v4 with `node-version: 22` currently resolves to 22.20+). `npm-publish.yml` and `clawhub-publish.yml` are already on Node 24, so they were unaffected. `docs-vercel.yml` is left on Node 20 because its build path does not import undici and currently passes; a separate cleanup can unify it. --- .github/workflows/ci-tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml index 78c80d89..b6d70ef2 100644 --- a/.github/workflows/ci-tests.yml +++ b/.github/workflows/ci-tests.yml @@ -23,7 +23,7 @@ jobs: - name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: 20 + node-version: 22 cache: pnpm - name: Install dependencies From 3b1b0234bac33e325d6af89c562dac04b5f3db40 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 17:33:55 +0800 Subject: [PATCH 33/44] test(approval): integration end-to-end coverage for 12 scenarios MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implements plan Task 21 (previously deferred). Creates `tests/integration/approval-end-to-end.test.ts` covering the 12 scenarios spec §9.3 + plan §Task 21 enumerate: 21a · Multi-approver race — 1st wins, 2nd APPROVAL_ALREADY_RESOLVED still re-PATCHes the card to cleared state 21b · Self-approval in DM — approver clicks own request, no private hint 21c · Non-approver click — private hint, no gateway call, no card PUT 21d · Upstream expired event — transport.updateEntry phase=expired drives applyExpiredPatch 21e · Card PUT explicit failure (HTTP 400) — downgrade to markdown delivery 21f · Card PUT ambiguous failure (ETIMEDOUT) — return null, no markdown duplication 21g · /approve command — bypasses session lock, calls gateway directly 21h · No approvers configured — availability.shouldHandle=false 21i · turnSourceChannel != dingtalk (CLI trigger) — shouldHandle=false 21j · Channel restart, click old button — APPROVAL_NOT_FOUND → applyExpiredPatch writes only the three template variables (no terminal status text) 21k · Exec invalid-decision (APPROVAL_ALLOW_ALWAYS_UNAVAILABLE) — no PUT, private hint asks to re-pick 21l · Plugin invalid-decision (allowedDecisions=['allow-once']) — no PUT, private hint includes allowed list Plan deviation: original plan called for one commit per sub-task (21a–21l TDD red→green); since the source is already implemented, this lands as a single coverage-completion commit. `mocks` are held via `vi.hoisted` so `getAccessToken` returns the mocked token reliably for the callback-handler's static-imported helper (the previous `vi.fn().mockResolvedValue(...)` inside a factory raced with the production import and returned `undefined`). Plan file updated: Task 21 status DEFERRED → DONE with the deviation documented inline. --- .../2026-05-19-gap-01-approval-native.md | 7 +- tests/integration/approval-end-to-end.test.ts | 341 ++++++++++++++++++ 2 files changed, 344 insertions(+), 4 deletions(-) create mode 100644 tests/integration/approval-end-to-end.test.ts diff --git a/docs/plans/2026-05-19-gap-01-approval-native.md b/docs/plans/2026-05-19-gap-01-approval-native.md index 718466e0..3d202d2c 100644 --- a/docs/plans/2026-05-19-gap-01-approval-native.md +++ b/docs/plans/2026-05-19-gap-01-approval-native.md @@ -4248,15 +4248,14 @@ EOF --- -## Task 21 · integration test approval-end-to-end(DEFERRED / follow-up) +## Task 21 · integration test approval-end-to-end(DONE) **Files:** -- Create: `tests/integration/approval-end-to-end.test.ts`(按 sub-task 21a-21l 分 12 次 commit) +- Create: `tests/integration/approval-end-to-end.test.ts`(12 sub-test 单 commit 落地) > spec §9.3 列出 12 个关键 integration 场景(含 v3.11 invalid-decision exec/plugin 两个)。 > **关键约定:** 这是 integration 级——mock HTTP/auth/registry/上游 SDK,但保持 channel 内部模块(callback-handler + resolver + patcher + native-runtime)真实串联(不要 mock `src/approval/*` 内部)。 -> **TDD 严格:** 每个 sub-task 都是「写 → 跑确认 fail / pass → commit」;不允许提交空 `it(..., async () => {})`。 -> **当前分支状态:DEFERRED。** 尚未创建该 integration 文件;现有覆盖主要是各模块 unit test 加 `gateway-inbound-flow.test.ts` 的 TOPIC_CARD 路由 smoke。若严格按本文验收,Task 21 仍是合并前缺口;若先合并,本 task 应作为独立 follow-up PR 追踪。 +> **实施偏差:** 原 plan 要求按 21a–21l 分 12 次 commit(TDD red→green);实施时源码已落地,12 个测试是补覆盖而非 TDD 增量,因此单 commit 一并落地,commit message 列出 12 个场景。共享 setup 用 `vi.hoisted` 持有 mock 实例(解决 vi.fn().mockResolvedValue 在 factory 内对静态 import 的初始化竞态)。 ### 共享 setup(Step 21.0:每个 sub-task 都依赖) diff --git a/tests/integration/approval-end-to-end.test.ts b/tests/integration/approval-end-to-end.test.ts new file mode 100644 index 00000000..22a28cc3 --- /dev/null +++ b/tests/integration/approval-end-to-end.test.ts @@ -0,0 +1,341 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + getAccessToken: vi.fn(() => Promise.resolve("tok")), + updateCardVariables: vi.fn(() => Promise.resolve(200)), + sendProactiveTextOrMarkdown: vi.fn(() => Promise.resolve({ ok: true })), + resolveApprovalOverGateway: vi.fn(), +})); + +vi.mock("openclaw/plugin-sdk/approval-gateway-runtime", () => ({ + resolveApprovalOverGateway: mocks.resolveApprovalOverGateway, +})); + +vi.mock("../../src/card-callback-service", async () => { + const actual = + await vi.importActual( + "../../src/card-callback-service", + ); + return { + ...actual, + updateCardVariables: mocks.updateCardVariables, + }; +}); + +vi.mock("../../src/send-service", () => ({ + sendProactiveTextOrMarkdown: mocks.sendProactiveTextOrMarkdown, +})); + +vi.mock("../../src/auth", () => ({ + getAccessToken: mocks.getAccessToken, +})); + +vi.mock("../../src/logger-context", () => ({ + getLogger: vi.fn(() => undefined), +})); + +const { tryHandleApprovalCallback } = await import( + "../../src/approval/approval-callback-handler" +); +const { tryInterceptApproveCommand } = await import( + "../../src/approval/approval-command-intercept" +); +const { createDingTalkApprovalNativeRuntime } = await import( + "../../src/approval/approval-native-runtime" +); + +const mockGateway = mocks.resolveApprovalOverGateway; +const mockPut = mocks.updateCardVariables; +const mockSend = mocks.sendProactiveTextOrMarkdown; + +const baseCfg = { + channels: { + dingtalk: { + clientId: "x", + clientSecret: "y", + execApprovals: { approvers: ["staffA", "staffB"] }, + }, + }, +} as never; + +function callbackAnalysis(overrides: Record = {}) { + return { + summary: "allow-once", + actionId: "allow-once", + userId: "staffA", + outTrackId: "ai_card_xxx", + cardPrivateData: { + actionIds: ["allow-once"], + params: { action: "allow-once", approveId: "abc123" }, + }, + ...overrides, + } as never; +} + +function approvalRequest(overrides: Record = {}) { + return { + id: "abc", + createdAtMs: Date.now() - 1000, + expiresAtMs: Date.now() + 60_000, + request: { + command: "rm -rf tmp", + sessionKey: "s1", + turnSourceChannel: "dingtalk", + turnSourceTo: "group:c", + turnSourceAccountId: "default", + ...overrides, + }, + }; +} + +beforeEach(() => { + mockGateway.mockReset(); + mockPut.mockReset().mockImplementation(() => Promise.resolve(200)); + mockSend.mockReset().mockImplementation(() => Promise.resolve({ ok: true } as never)); + mocks.getAccessToken.mockReset().mockImplementation(() => Promise.resolve("tok")); +}); + +describe("approval end-to-end · 12 scenarios", () => { + it("(1) multi-approver: 1st wins → 2nd already-resolved → applyExpiredPatch 仍 PUT 三变量", async () => { + mockGateway.mockResolvedValueOnce(undefined); + await tryHandleApprovalCallback({ + cfg: baseCfg, + accountId: "default", + analysis: callbackAnalysis({ userId: "staffA" }), + }); + expect(mockPut).toHaveBeenCalledWith( + "ai_card_xxx", + expect.objectContaining({ show_approve_btns: "false", approveId: "" }), + "tok", + expect.objectContaining({ clientId: "x" }), + ); + mockPut.mockClear(); + + mockGateway.mockRejectedValueOnce( + Object.assign(new Error("already"), { gatewayCode: "APPROVAL_ALREADY_RESOLVED" }), + ); + await tryHandleApprovalCallback({ + cfg: baseCfg, + accountId: "default", + analysis: callbackAnalysis({ userId: "staffB" }), + }); + expect(mockPut).toHaveBeenCalledWith( + "ai_card_xxx", + expect.objectContaining({ show_approve_btns: "false", approveId: "" }), + "tok", + expect.objectContaining({ clientId: "x" }), + ); + }); + + it("(2) self-approval in DM: approver 自己点 → resolveApproval ok=true → 不私聊", async () => { + mockGateway.mockResolvedValue(undefined); + await tryHandleApprovalCallback({ + cfg: baseCfg, + accountId: "default", + analysis: callbackAnalysis({ userId: "staffA" }), + }); + expect(mockGateway).toHaveBeenCalledWith( + expect.objectContaining({ + approvalId: "abc123", + decision: "allow-once", + senderId: "staffA", + }), + ); + expect(mockSend).not.toHaveBeenCalled(); + }); + + it("(3) 非 approver 点击 → 私聊提示 + 不调 gateway + 不 PUT 卡片", async () => { + await tryHandleApprovalCallback({ + cfg: baseCfg, + accountId: "default", + analysis: callbackAnalysis({ userId: "outsider" }), + }); + expect(mockGateway).not.toHaveBeenCalled(); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:outsider", + expect.stringContaining("无权"), + expect.objectContaining({ forceMarkdown: true }), + ); + expect(mockPut).not.toHaveBeenCalled(); + }); + + it("(4) 上游 expired event → transport.updateEntry({phase:'expired'}) → applyExpiredPatch", async () => { + const runtime = createDingTalkApprovalNativeRuntime(); + await runtime.transport.updateEntry?.({ + cfg: baseCfg, + entry: { + mode: "card", + approvalId: "abc", + accountId: "default", + outTrackId: "ot1", + }, + payload: { phase: "expired" }, + phase: "expired", + } as never); + expect(mockPut).toHaveBeenCalledWith( + "ot1", + expect.objectContaining({ show_approve_btns: "false", approveId: "" }), + "tok", + expect.objectContaining({ clientId: "x" }), + ); + }); + + it("(5) card 路径 HTTP 400 → 降级 markdown, entry.mode='markdown'", async () => { + mockPut.mockRejectedValueOnce(Object.assign(new Error("400"), { status: 400 })); + const runtime = createDingTalkApprovalNativeRuntime(); + const entry = await runtime.transport.deliverPending({ + cfg: baseCfg, + preparedTarget: { + route: "card", + to: "group:c", + accountId: "default", + activeCardOutTrackId: "ot1", + }, + request: approvalRequest(), + pendingPayload: { approvalId: "abc", markdownText: "md-payload" }, + } as never); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "group:c", + "md-payload", + expect.objectContaining({ forceMarkdown: true }), + ); + expect((entry as { mode?: string } | null)?.mode).toBe("markdown"); + }); + + it("(6) card 路径 ETIMEDOUT → return null, 不发 markdown", async () => { + mockPut.mockRejectedValueOnce(Object.assign(new Error("timeout"), { code: "ETIMEDOUT" })); + const runtime = createDingTalkApprovalNativeRuntime(); + const entry = await runtime.transport.deliverPending({ + cfg: baseCfg, + preparedTarget: { + route: "card", + to: "group:c", + accountId: "default", + activeCardOutTrackId: "ot1", + }, + request: approvalRequest(), + pendingPayload: { approvalId: "abc", markdownText: "md" }, + } as never); + expect(entry).toBeNull(); + expect(mockSend).not.toHaveBeenCalled(); + }); + + it("(7) /approve 命令路径 → 调 gateway, 命令被早期拦截", async () => { + mockGateway.mockResolvedValue(undefined); + const intercepted = await tryInterceptApproveCommand({ + cfg: baseCfg, + accountId: "default", + text: "/approve abc once", + senderId: "staffA", + }); + expect(intercepted).toBe(true); + expect(mockGateway).toHaveBeenCalledWith( + expect.objectContaining({ + approvalId: "abc", + decision: "allow-once", + senderId: "staffA", + }), + ); + }); + + it("(8) 未配置 approvers → availability.shouldHandle=false", async () => { + const cfgNoApprovers = { + channels: { dingtalk: { clientId: "x", clientSecret: "y" } }, + } as never; + const runtime = createDingTalkApprovalNativeRuntime(); + expect( + runtime.availability.shouldHandle({ + cfg: cfgNoApprovers, + accountId: "default", + request: approvalRequest() as never, + } as never), + ).toBe(false); + }); + + it("(9) turnSourceChannel != dingtalk (CLI) → availability.shouldHandle=false", async () => { + const runtime = createDingTalkApprovalNativeRuntime(); + expect( + runtime.availability.shouldHandle({ + cfg: baseCfg, + accountId: "default", + request: approvalRequest({ turnSourceChannel: "codex-cli" }) as never, + } as never), + ).toBe(false); + }); + + it("(10) Channel 重启后旧按钮 → gateway 抛 APPROVAL_NOT_FOUND → applyExpiredPatch 三变量, 无终态文字", async () => { + mockGateway.mockRejectedValue( + Object.assign(new Error("not found"), { gatewayCode: "APPROVAL_NOT_FOUND" }), + ); + await tryHandleApprovalCallback({ + cfg: baseCfg, + accountId: "default", + analysis: callbackAnalysis({ userId: "staffA" }), + }); + const vars = mockPut.mock.calls[0][1] as Record; + expect(vars).toEqual( + expect.objectContaining({ show_approve_btns: "false", approveId: "" }), + ); + expect(vars).not.toHaveProperty("status"); + expect(vars).not.toHaveProperty("statusFooter"); + expect(vars).not.toHaveProperty("approval_status"); + }); + + it("(11) exec invalid-decision (APPROVAL_ALLOW_ALWAYS_UNAVAILABLE) → 不 PUT + 私聊重选", async () => { + mockGateway.mockRejectedValue( + Object.assign(new Error("invalid"), { + gatewayCode: "INVALID_REQUEST", + details: { reason: "APPROVAL_ALLOW_ALWAYS_UNAVAILABLE" }, + }), + ); + await tryHandleApprovalCallback({ + cfg: baseCfg, + accountId: "default", + analysis: callbackAnalysis({ + userId: "staffA", + actionId: "allow-always", + cardPrivateData: { + actionIds: ["allow-always"], + params: { action: "allow-always", approveId: "abc123" }, + }, + }), + }); + expect(mockPut).not.toHaveBeenCalled(); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:staffA", + expect.stringContaining("不支持 allow-always"), + expect.objectContaining({ forceMarkdown: true }), + ); + }); + + it("(12) plugin invalid-decision (allowedDecisions=['allow-once']) → 私聊含 allowed 列表", async () => { + mockGateway.mockRejectedValue( + Object.assign(new Error("invalid"), { + gatewayCode: "INVALID_REQUEST", + details: { allowedDecisions: ["allow-once"] }, + }), + ); + await tryHandleApprovalCallback({ + cfg: baseCfg, + accountId: "default", + analysis: callbackAnalysis({ + userId: "staffA", + actionId: "allow-always", + cardPrivateData: { + actionIds: ["allow-always"], + params: { action: "allow-always", approveId: "plugin:xyz789" }, + }, + }), + }); + expect(mockPut).not.toHaveBeenCalled(); + expect(mockSend).toHaveBeenCalledWith( + expect.anything(), + "user:staffA", + expect.stringContaining("allow-once"), + expect.objectContaining({ forceMarkdown: true }), + ); + }); +}); From 12fb8b2a00ed2a410361bfded92c1335a07dc738 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 21:50:18 +0800 Subject: [PATCH 34/44] fix(approval): preserve pending approval state across card finalize - card-service: add approvalParamsForTerminal helper consulting card-run-registry.pendingApprovalId; commitAICardBlocks and finalizeStoppedAICard skip clearing show_approve_btns/approveId while an approval is in flight, letting approval-card-patcher remain the single writer of those keys until applyResolvedPatch / applyExpiredPatch fires. - approval-card-locator: emit a debug log when the active card is busy with a different pendingApprovalId so the markdown fallback path is observable from runtime logs. - approval-command-parser: export APPROVE_COMMAND_RE; intercept and inbound-handler share the single regex source instead of three inline duplicates. - card-service.test: two regression cases covering terminal-clear and pending-preserve branches. --- src/approval/approval-card-locator.ts | 4 ++ src/approval/approval-command-intercept.ts | 4 +- src/approval/approval-command-parser.ts | 2 +- src/card-service.ts | 12 +++- src/inbound-handler.ts | 3 +- tests/unit/card-service.test.ts | 80 ++++++++++++++++++++++ 6 files changed, 98 insertions(+), 7 deletions(-) diff --git a/src/approval/approval-card-locator.ts b/src/approval/approval-card-locator.ts index 066d0af9..f799a683 100644 --- a/src/approval/approval-card-locator.ts +++ b/src/approval/approval-card-locator.ts @@ -1,5 +1,6 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; import { resolveActiveCardRunBySession } from "../card/card-run-registry"; +import { getLogger } from "../logger-context"; export interface FindActiveAgentCardInput { cfg: OpenClawConfig; @@ -22,6 +23,9 @@ export function findActiveAgentCard(input: FindActiveAgentCardInput): ActiveAgen return null; } if (record.pendingApprovalId && record.pendingApprovalId !== input.approvalId) { + getLogger(input.accountId)?.debug?.( + `[DingTalk][Approval] active card outTrackId=${record.outTrackId} already pending approvalId=${record.pendingApprovalId}; falling back to markdown for approvalId=${input.approvalId ?? ""}`, + ); return null; } return { outTrackId: record.outTrackId, sessionKey: record.sessionKey }; diff --git a/src/approval/approval-command-intercept.ts b/src/approval/approval-command-intercept.ts index 3d4a1ffd..0aa480e7 100644 --- a/src/approval/approval-command-intercept.ts +++ b/src/approval/approval-command-intercept.ts @@ -2,7 +2,7 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; import { getConfig } from "../config"; import { sendProactiveTextOrMarkdown } from "../send-service"; import type { Logger } from "../types"; -import { parseApproveCommand } from "./approval-command-parser"; +import { APPROVE_COMMAND_RE, parseApproveCommand } from "./approval-command-parser"; import { resolveApproval } from "./approval-resolver"; export interface ApproveCommandInterceptInput { @@ -13,8 +13,6 @@ export interface ApproveCommandInterceptInput { log?: Logger; } -const APPROVE_COMMAND_RE = /^\/?approve(?:\s|$)/i; - async function sendDirectHint( input: Pick, text: string, diff --git a/src/approval/approval-command-parser.ts b/src/approval/approval-command-parser.ts index fec8b5e1..f95654fc 100644 --- a/src/approval/approval-command-parser.ts +++ b/src/approval/approval-command-parser.ts @@ -1,6 +1,6 @@ import type { ApprovalDecision } from "../types"; -const APPROVE_COMMAND_RE = /^\/?approve(?:\s|$)/i; +export const APPROVE_COMMAND_RE = /^\/?approve(?:\s|$)/i; const DECISION_ALIASES: Record = { allow: "allow-once", diff --git a/src/card-service.ts b/src/card-service.ts index 24e39ea1..a5d0fdbf 100644 --- a/src/card-service.ts +++ b/src/card-service.ts @@ -6,6 +6,7 @@ import { getAccessToken } from "./auth"; import { updateCardVariables } from "./card-callback-service"; import { DINGTALK_CARD_TEMPLATE, STOP_ACTION_VISIBLE, STOP_ACTION_HIDDEN } from "./card/card-template"; import { APPROVAL_CARD_INITIAL } from "./approval/approval-card-state"; +import { resolveCardRun } from "./card/card-run-registry"; import { resolveRobotCode, stripTargetPrefix } from "./config"; import { resolveOriginalPeerId } from "./peer-id-registry"; import { @@ -47,6 +48,13 @@ const DYNAMIC_SUMMARY_EXTENSION = { dynamicSummary: "true" } as const; const aicardDegradeByAccount = new Map(); +// Approval lifecycle owns show_approve_btns / approveId while a request is in flight. +// Returning `{}` here lets the approval-card-patcher remain the single writer of those +// keys until applyResolvedPatch / applyExpiredPatch clears them. +function approvalParamsForTerminal(outTrackId: string): Partial { + return resolveCardRun(outTrackId)?.pendingApprovalId ? {} : APPROVAL_CARD_INITIAL; +} + export async function hideCardStopButton( outTrackId: string, token: string, @@ -1135,7 +1143,7 @@ export async function commitAICardBlocks( [template.streamingKey]: options.content, // markdown content for display [template.copyContentKey]: options.content, // same markdown as String type for card copy action flowStatus: 3, // completed state - V2 template hides stop button automatically - ...APPROVAL_CARD_INITIAL, + ...approvalParamsForTerminal(card.outTrackId || card.cardInstanceId), }; // Optional fields @@ -1431,7 +1439,7 @@ export async function finalizeStoppedAICard( [template.streamingKey]: payload.content, [template.copyContentKey]: payload.content, flowStatus: 3, - ...APPROVAL_CARD_INITIAL, + ...approvalParamsForTerminal(card.outTrackId || card.cardInstanceId), }, card.accessToken, card.config, diff --git a/src/inbound-handler.ts b/src/inbound-handler.ts index 88e8432b..b14257f6 100644 --- a/src/inbound-handler.ts +++ b/src/inbound-handler.ts @@ -7,6 +7,7 @@ import { classifyAckReactionEmoji } from "./ack-reaction-classifier"; import { attachNativeAckReaction } from "./ack-reaction-service"; import { createDynamicAckReactionController } from "./ack-reaction/dynamic-ack-reaction-controller"; import { tryInterceptApproveCommand } from "./approval/approval-command-intercept"; +import { APPROVE_COMMAND_RE } from "./approval/approval-command-parser"; import { getAccessToken } from "./auth"; import { createAICard, commitAICardBlocks, isCardInTerminalState } from "./card-service"; import { isCardRunStopRequested, registerCardRun, removeCardRun } from "./card/card-run-registry"; @@ -777,7 +778,7 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams) }); const approveCommandText = stripLeadingMentions(extractedContent.text).trim(); - if (/^\/?approve(?:\s|$)/i.test(approveCommandText)) { + if (APPROVE_COMMAND_RE.test(approveCommandText)) { const intercepted = await tryInterceptApproveCommand({ cfg, accountId, diff --git a/tests/unit/card-service.test.ts b/tests/unit/card-service.test.ts index 77a90d0a..5e5d5f53 100644 --- a/tests/unit/card-service.test.ts +++ b/tests/unit/card-service.test.ts @@ -1197,3 +1197,83 @@ describe('token refresh', () => { expect(mockedAxios.put.mock.calls[0][0]).toContain('/v1.0/card/instances'); }); }); + +describe('commitAICardBlocks · approval lifecycle ownership', () => { + beforeEach(async () => { + mockedAxios.mockReset(); + mockedAxios.post.mockReset(); + mockedAxios.put.mockReset(); + mockedGetAccessToken.mockReset(); + mockedGetAccessToken.mockResolvedValue('token_abc'); + const { clearCardRunRegistryForTest } = await import('../../src/card/card-run-registry'); + clearCardRunRegistryForTest(); + }); + + it('clears show_approve_btns/approveId when no approval is pending', async () => { + const { commitAICardBlocks } = await import('../../src/card-service'); + const { registerCardRun } = await import('../../src/card/card-run-registry'); + registerCardRun('track_no_pending', { + accountId: 'main', + sessionKey: 's1', + agentId: 'a1', + }); + mockedAxios.put.mockResolvedValue({ status: 200, data: { ok: true } }); + + await commitAICardBlocks( + { + cardInstanceId: 'card_no_pending', + outTrackId: 'track_no_pending', + accessToken: 'tok', + conversationId: 'cid_1', + state: AICardStatus.INPUTING, + createdAt: Date.now(), + lastUpdated: Date.now(), + config: { clientId: 'id', clientSecret: 'sec' } as any, + } as any, + { + blockListJson: JSON.stringify([{ type: 0, markdown: 'done' }]), + content: 'done', + }, + ); + + const paramMap = mockedAxios.put.mock.calls[0][1].cardData.cardParamMap; + expect(paramMap.show_approve_btns).toBe('false'); + expect(paramMap.approveId).toBe(''); + }); + + it('preserves show_approve_btns/approveId when an approval is still pending', async () => { + const { commitAICardBlocks } = await import('../../src/card-service'); + const { + registerCardRun, + markCardRunPendingApproval, + } = await import('../../src/card/card-run-registry'); + registerCardRun('track_pending', { + accountId: 'main', + sessionKey: 's1', + agentId: 'a1', + }); + markCardRunPendingApproval('track_pending', 'approval-123'); + mockedAxios.put.mockResolvedValue({ status: 200, data: { ok: true } }); + + await commitAICardBlocks( + { + cardInstanceId: 'card_pending', + outTrackId: 'track_pending', + accessToken: 'tok', + conversationId: 'cid_1', + state: AICardStatus.INPUTING, + createdAt: Date.now(), + lastUpdated: Date.now(), + config: { clientId: 'id', clientSecret: 'sec' } as any, + } as any, + { + blockListJson: JSON.stringify([{ type: 0, markdown: 'done' }]), + content: 'done', + }, + ); + + const paramMap = mockedAxios.put.mock.calls[0][1].cardData.cardParamMap; + expect(paramMap.show_approve_btns).toBeUndefined(); + expect(paramMap.approveId).toBeUndefined(); + }); +}); From 953e122c2c007c9536da0eb6387f7dd62d56463b Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 21:50:23 +0800 Subject: [PATCH 35/44] chore(release): defer 3.6.4 release notes and version bump Release notes belong in the dedicated release prep PR, not the feature PR. Revert package.json to 3.6.3, point latest.md back to v3.6.3.md, and remove v3.6.4.md. --- docs/releases/latest.md | 2 +- docs/releases/v3.6.4.md | 33 --------------------------------- package.json | 2 +- 3 files changed, 2 insertions(+), 35 deletions(-) delete mode 100644 docs/releases/v3.6.4.md diff --git a/docs/releases/latest.md b/docs/releases/latest.md index 4069bf56..1d09913e 100644 --- a/docs/releases/latest.md +++ b/docs/releases/latest.md @@ -1,2 +1,2 @@ - + diff --git a/docs/releases/v3.6.4.md b/docs/releases/v3.6.4.md deleted file mode 100644 index 90d954af..00000000 --- a/docs/releases/v3.6.4.md +++ /dev/null @@ -1,33 +0,0 @@ -# v3.6.4 发布说明 - -本版本新增 DingTalk Native Approval,并将 OpenClaw 兼容基线提升到 `2026.4.7+`。 - -## 新功能 - -### DingTalk Native Approval - -- 支持 OpenClaw exec approval 与 plugin approval 在钉钉内处理。 -- AI Card 模式下复用当前 agent reply card,显示 `允许一次`、`总是允许`、`拒绝` 三个原生按钮。 -- Markdown 路径发送 `/approve ` 命令模板。 -- `/approve` 命令在 DingTalk inbound 阶段提前拦截,避免被原 agent 会话锁阻塞。 -- 审批结果统一经过 OpenClaw gateway resolver,并区分 unauthorized、not-found、already-resolved、invalid-decision、gateway-error。 - -## 内部改动 - -- AI Card 默认模板升级为 v3,新增 `show_approve_btns` 与 `approveId` 变量,并继续保留既有 `hasAction` stop 控制字段。 -- 新增 approval card patcher,统一 pending、resolved、expired 三种 cardParamMap 变更。 -- `CardCallbackAnalysis` 暴露 `cardPrivateData`,用于读取按钮 action 与 `approveId`。 -- card-run registry 新增 `pendingApprovalId` fallback,仅用于异常回调兜底。 -- channel capability 接入 OpenClaw `nativeRuntime`,按 card / markdown 双路由投递 pending approval。 - -## 升级说明 - -- 最低 OpenClaw 版本为 `2026.4.7`。 -- 如使用自定义卡片模板,需要基于 v3 模板重新导入或确保模板包含 approval 按钮组、`show_approve_btns` 和 `approveId`。 -- 详细配置见 [DingTalk Native Approval](../user/features/exec-approval.md)。 - -## 已知限制 - -- v1 只支持 origin chat 投递,不做 approver DM fan-out。 -- 卡片终态只隐藏按钮,不写入“已批准 / 已拒绝”状态文案。 -- 钉钉真机模板回调仍应在发布前抽检,确认 `cardPrivateData.params.approveId` 可回传。 diff --git a/package.json b/package.json index f1cfeb6a..b63e7c04 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@soimy/dingtalk", - "version": "3.6.4", + "version": "3.6.3", "description": "DingTalk (钉钉) channel plugin for OpenClaw", "keywords": [ "bot", From dd49a9b0cb45287095da055c3e973c7afbfcec66 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 22:37:49 +0800 Subject: [PATCH 36/44] fix(approval): declare execApprovals in plugin manifest schema The plugin manifest's channelConfigs.dingtalk.schema is the source of truth for openclaw's host-side strict config validation (config validate, config set --strict-json, gateway restart). It is hand-maintained and must mirror the runtime zod schema in src/config-schema.ts. This PR added execApprovals to the runtime zod schema (and config.ts resolve) but missed the static manifest, so host-side validation rejected any execApprovals key as "additional property" while still accepting all legacy fields. gateway start was lenient enough to mask the gap; gateway restart and config set --strict-json surfaced it. Mirror execApprovals at all three manifest sites that other fields use: - channelConfigs.dingtalk.schema.properties.execApprovals - channelConfigs.dingtalk.schema.properties.accounts.additionalProperties.properties.execApprovals - channelConfigs.dingtalk.uiHints.execApprovals --- openclaw.plugin.json | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/openclaw.plugin.json b/openclaw.plugin.json index a3113d06..ac69278b 100644 --- a/openclaw.plugin.json +++ b/openclaw.plugin.json @@ -257,6 +257,25 @@ "type": "boolean", "description": "Enable the local feedback-learning loop for notes, reflections, and command-assisted learning." }, + "execApprovals": { + "type": "object", + "additionalProperties": false, + "description": "Native OpenClaw exec/plugin approval configuration. v1 intentionally rejects v2 future fields.", + "properties": { + "enabled": { + "anyOf": [ + { "type": "boolean" }, + { "type": "string", "const": "auto" } + ], + "description": "Enable native approval delivery. true to force on, false to disable, or \"auto\" to follow upstream host capability." + }, + "approvers": { + "type": "array", + "items": { "type": "string" }, + "description": "Approver staffIds (supports dingtalk:, dd:, ding: prefixes). Falls back to commands.ownerAllowFrom when empty." + } + } + }, "learningAutoApply": { "type": "boolean", "description": "Automatically apply generated learning output into session notes or global rules when available." @@ -563,6 +582,25 @@ "type": "boolean", "description": "Enable the local feedback-learning loop for notes, reflections, and command-assisted learning." }, + "execApprovals": { + "type": "object", + "additionalProperties": false, + "description": "Native OpenClaw exec/plugin approval configuration. v1 intentionally rejects v2 future fields.", + "properties": { + "enabled": { + "anyOf": [ + { "type": "boolean" }, + { "type": "string", "const": "auto" } + ], + "description": "Enable native approval delivery. true to force on, false to disable, or \"auto\" to follow upstream host capability." + }, + "approvers": { + "type": "array", + "items": { "type": "string" }, + "description": "Approver staffIds (supports dingtalk:, dd:, ding: prefixes). Falls back to commands.ownerAllowFrom when empty." + } + } + }, "learningAutoApply": { "type": "boolean", "description": "Automatically apply generated learning output into session notes or global rules when available." @@ -782,6 +820,10 @@ "label": "Learning Enabled", "help": "Enable the local feedback-learning loop for notes, reflections, and learning commands." }, + "execApprovals": { + "label": "Exec Approvals", + "help": "Approver staffIds and toggle for native exec/plugin approval delivery. Falls back to commands.ownerAllowFrom when approvers is empty." + }, "learningAutoApply": { "label": "Learning Auto Apply", "help": "Automatically apply generated learning output into session notes or global rules when available." From 0306167ca9252e682b09352debe41cbcebf0af78 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 23:35:55 +0800 Subject: [PATCH 37/44] chore(approval): add INFO diagnostics to native runtime adapters Real-device validation showed no [DingTalk][Approval] log lines fired, indicating our nativeRuntime adapters never engaged. Add explicit INFO logs at each decision point so subsequent reproduction tells us exactly where the chain breaks (e.g. shouldHandle skipped, prepareTarget chose markdown route, etc.). - availability.shouldHandle: log accept/skip with reason (native-delivery-disabled / turnSourceChannel-mismatch / missing-turnSourceTo / no-approvers) - presentation.buildPendingPayload: log approval id + kind on entry - transport.prepareTarget: log route=card|markdown with outTrackId or no-active-card sessionKey context INFO-level so they remain useful as long-term observability. --- src/approval/approval-native-runtime.ts | 55 ++++++++++++++++++++----- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/src/approval/approval-native-runtime.ts b/src/approval/approval-native-runtime.ts index 02935309..2547570c 100644 --- a/src/approval/approval-native-runtime.ts +++ b/src/approval/approval-native-runtime.ts @@ -86,26 +86,54 @@ export function createDingTalkApprovalNativeRuntime(): ChannelApprovalNativeRunt getExecApprovalsConfig({ cfg, accountId: accountId ?? "default" }).isNativeDeliveryEnabled, shouldHandle: ({ cfg, accountId, request }) => { const resolvedAccountId = accountId ?? "default"; + const log = getLogger(resolvedAccountId); + const tsc = request.request.turnSourceChannel; + const tst = request.request.turnSourceTo; + const reqId = request.id; if (!getExecApprovalsConfig({ cfg, accountId: resolvedAccountId }).isNativeDeliveryEnabled) { + log?.info?.( + `[DingTalk][Approval][shouldHandle] skip approval=${reqId} reason=native-delivery-disabled account=${resolvedAccountId}`, + ); + return false; + } + if (tsc !== "dingtalk") { + log?.info?.( + `[DingTalk][Approval][shouldHandle] skip approval=${reqId} reason=turnSourceChannel-mismatch got=${String(tsc)}`, + ); return false; } - if (request.request.turnSourceChannel !== "dingtalk") { + if (!tst) { + log?.info?.( + `[DingTalk][Approval][shouldHandle] skip approval=${reqId} reason=missing-turnSourceTo`, + ); return false; } - if (!request.request.turnSourceTo) { + const approvers = listExecApprovers({ cfg, accountId: resolvedAccountId }).length; + if (approvers === 0) { + log?.info?.( + `[DingTalk][Approval][shouldHandle] skip approval=${reqId} reason=no-approvers account=${resolvedAccountId}`, + ); return false; } - return listExecApprovers({ cfg, accountId: resolvedAccountId }).length > 0; + log?.info?.( + `[DingTalk][Approval][shouldHandle] accept approval=${reqId} account=${resolvedAccountId} approvers=${approvers}`, + ); + return true; }, }, presentation: { - buildPendingPayload: ({ request, approvalKind, nowMs }) => ({ - approvalId: request.id, - markdownText: - approvalKind === "plugin" - ? buildPluginApprovalMarkdown(request as never, nowMs) - : buildExecApprovalMarkdown(request as never, nowMs), - }), + buildPendingPayload: ({ request, approvalKind, nowMs }) => { + getLogger()?.info?.( + `[DingTalk][Approval][buildPendingPayload] approval=${request.id} kind=${approvalKind}`, + ); + return { + approvalId: request.id, + markdownText: + approvalKind === "plugin" + ? buildPluginApprovalMarkdown(request as never, nowMs) + : buildExecApprovalMarkdown(request as never, nowMs), + }; + }, buildResolvedResult: ({ resolved }) => ({ kind: "update", payload: { phase: "resolved", decision: resolved.decision }, @@ -120,6 +148,7 @@ export function createDingTalkApprovalNativeRuntime(): ChannelApprovalNativeRunt const target = plannedTarget.target as { to: string; accountId?: string | null }; const resolvedAccountId = target.accountId ?? accountId ?? request.request.turnSourceAccountId ?? "default"; + const log = getLogger(resolvedAccountId); const to = normalizeApprovalTargetTo(target.to); const activeCard = findActiveAgentCard({ cfg, @@ -128,6 +157,9 @@ export function createDingTalkApprovalNativeRuntime(): ChannelApprovalNativeRunt approvalId: request.id, }); if (activeCard) { + log?.info?.( + `[DingTalk][Approval][prepareTarget] route=card approval=${request.id} account=${resolvedAccountId} to=${to} outTrackId=${activeCard.outTrackId}`, + ); return { dedupeKey: `dingtalk:${resolvedAccountId}:${to}:${activeCard.outTrackId}:${request.id}`, target: { @@ -138,6 +170,9 @@ export function createDingTalkApprovalNativeRuntime(): ChannelApprovalNativeRunt }, }; } + log?.info?.( + `[DingTalk][Approval][prepareTarget] route=markdown approval=${request.id} account=${resolvedAccountId} to=${to} reason=no-active-card sessionKey=${request.request.sessionKey ?? ""}`, + ); return { dedupeKey: `dingtalk:${resolvedAccountId}:${to}:markdown:${request.id}`, target: { From 0e74899c983ff51c8b64e3b2ef353b4d282cdd38 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Tue, 19 May 2026 23:42:19 +0800 Subject: [PATCH 38/44] fix(approval): register native approval runtime context on startup Real-device diagnostics showed our nativeRuntime adapters (shouldHandle / buildPendingPayload / prepareTarget) never fired even with execApprovals.approvers + approvals.exec.enabled set. Root cause: upstream's startChannelApprovalHandlerBootstrap subscribes via watchChannelRuntimeContexts and waits for a registration of CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY on the channel runtime. Bundled channels (telegram/discord/slack/matrix/qqbot) all call registerChannelRuntimeContext from their monitor startup. The DingTalk plugin lacked this call entirely, so the bootstrap waited indefinitely and Path 2 (native card-button delivery) never engaged. Add the registration to startAccount, mirroring telegram's pattern: - Skip when execApprovals.isNativeDeliveryEnabled is false to avoid spurious bootstrap on accounts that do not opt in. - Skip when ctx.channelRuntime is undefined (no-op guard, the SDK field is optional). - Pass abortSignal so context is released on account stop/restart. - Log INFO whether registration happened or was skipped so we can verify from gateway logs. The bootstrap will now engage on the next gateway restart and the nativeRuntime adapters will receive approval events. --- src/gateway/channel-gateway.ts | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/gateway/channel-gateway.ts b/src/gateway/channel-gateway.ts index 94c2a87f..d11031d9 100644 --- a/src/gateway/channel-gateway.ts +++ b/src/gateway/channel-gateway.ts @@ -1,7 +1,10 @@ import { DWClient, TOPIC_CARD, TOPIC_ROBOT } from "dingtalk-stream"; +import { CHANNEL_APPROVAL_NATIVE_RUNTIME_CONTEXT_CAPABILITY } from "openclaw/plugin-sdk/approval-handler-adapter-runtime"; +import { registerChannelRuntimeContext } from "openclaw/plugin-sdk/channel-runtime-context"; import { analyzeCardCallback } from "../card-callback-service"; import { handleCardAction } from "../card/card-action-handler"; import { tryHandleApprovalCallback } from "../approval/approval-callback-handler"; +import { getExecApprovalsConfig } from "../approval/approval-config"; import { finalizeActiveCardsForAccount, recoverPendingCardsForAccount, @@ -182,6 +185,33 @@ export function createDingTalkGateway(): NonNullable Date: Tue, 19 May 2026 23:50:51 +0800 Subject: [PATCH 39/44] chore(approval): log terminalPatch branch decision User reported buttons disappear shortly after appearing. Timeline: applyPendingPatch (T) -> agent stream -> Card finalized state=FINISHED (T+8s) -> buttons gone. Need to know whether approvalParamsForTerminal returned preserve ({}) or clear (APPROVAL_CARD_INITIAL) at finalize. Add INFO log inside approvalParamsForTerminal recording outTrackId, the resolved pendingApprovalId, and the chosen action (preserve|clear). --- src/card-service.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/card-service.ts b/src/card-service.ts index a5d0fdbf..87fddabe 100644 --- a/src/card-service.ts +++ b/src/card-service.ts @@ -7,6 +7,7 @@ import { updateCardVariables } from "./card-callback-service"; import { DINGTALK_CARD_TEMPLATE, STOP_ACTION_VISIBLE, STOP_ACTION_HIDDEN } from "./card/card-template"; import { APPROVAL_CARD_INITIAL } from "./approval/approval-card-state"; import { resolveCardRun } from "./card/card-run-registry"; +import { getLogger } from "./logger-context"; import { resolveRobotCode, stripTargetPrefix } from "./config"; import { resolveOriginalPeerId } from "./peer-id-registry"; import { @@ -52,7 +53,11 @@ const aicardDegradeByAccount = new Map { - return resolveCardRun(outTrackId)?.pendingApprovalId ? {} : APPROVAL_CARD_INITIAL; + const pending = resolveCardRun(outTrackId)?.pendingApprovalId; + getLogger()?.info?.( + `[DingTalk][Approval][terminalPatch] outTrackId=${outTrackId} pendingApprovalId=${pending ?? ""} action=${pending ? "preserve" : "clear"}`, + ); + return pending ? {} : APPROVAL_CARD_INITIAL; } export async function hideCardStopButton( From 13ba057f0f2ed472fd9de5af277f87199c3ec673 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Wed, 20 May 2026 00:07:39 +0800 Subject: [PATCH 40/44] fix(approval): defer card finalize while approval is pending DingTalk hard constraint: action buttons (including the v3 template's approve_btns ButtonGroup) only render while the card is in PROCESSING/INPUTING. flowStatus=3 (FINISHED) hides every action button regardless of show_approve_btns. Real-device verification showed the three approval buttons appearing for ~8s and then vanishing the moment commitAICardBlocks ran with the agent's "approval required" tool result. Defer the finalize until the approval resolves or expires: - card-run-registry: new deferredFinalize flag with mark/clear helpers. - card-service.commitAICardBlocks: when pendingApprovalId is set, omit flowStatus=3 and APPROVAL_CARD_INITIAL from the PUT, skip the local state=FINISHED + removePendingCard transition, and mark the run deferredFinalize so the patcher can complete it later. - approval-card-patcher.applyResolvedPatch / applyExpiredPatch: when the run is flagged deferredFinalize, append flowStatus=3 to the PUT, transition the in-memory card.state to FINISHED, and clear both the pendingApprovalId and deferredFinalize flags. - inbound-handler: hold the card-run record past the reply finally block while pendingApprovalId or deferredFinalize is set so the patcher can still resolve it. 30-minute TTL sweep handles abandons. - tests/approval-card-patcher: cover the deferred-finalize branch (flowStatus=3 added) and the in-memory card.state transition. If the approver never responds upstream emits an expired event after the 30-minute TTL; applyExpiredPatch then finalizes the deferred card, so cards do not hang indefinitely. --- src/approval/approval-card-patcher.ts | 48 ++++++++++++++++-- src/card-service.ts | 29 ++++++++--- src/card/card-run-registry.ts | 20 ++++++++ src/inbound-handler.ts | 22 ++++++--- tests/unit/approval-card-patcher.test.ts | 63 ++++++++++++++++++++++-- 5 files changed, 163 insertions(+), 19 deletions(-) diff --git a/src/approval/approval-card-patcher.ts b/src/approval/approval-card-patcher.ts index d33bb5b6..06f9bb66 100644 --- a/src/approval/approval-card-patcher.ts +++ b/src/approval/approval-card-patcher.ts @@ -1,13 +1,15 @@ -import type { DingTalkConfig } from "../types"; import { updateCardVariables } from "../card-callback-service"; import { buildApprovalClearedCardParams, buildApprovalPendingCardParams, } from "./approval-card-state"; import { + clearCardRunDeferredFinalize, clearCardRunPendingApproval, markCardRunPendingApproval, + resolveCardRun, } from "../card/card-run-registry"; +import { AICardStatus, type DingTalkConfig } from "../types"; export async function applyPendingPatch( outTrackId: string, @@ -19,6 +21,34 @@ export async function applyPendingPatch( markCardRunPendingApproval(outTrackId, approvalId); } +/** + * Resolve or expire an approval that lives on a DingTalk AI card. If the card + * was deferred-finalized (commitAICardBlocks skipped flowStatus=3 while the + * approval was pending), include flowStatus=3 in the PUT so the card finishes + * now that the buttons no longer need to render. + */ +function buildTerminalPatchParams( + outTrackId: string, + cardStillActive: boolean, +): Record { + const params: Record = { + ...buildApprovalClearedCardParams(cardStillActive), + }; + if (resolveCardRun(outTrackId)?.deferredFinalize) { + params.flowStatus = 3; + } + return params; +} + +function completeDeferredFinalize(outTrackId: string): void { + const record = resolveCardRun(outTrackId); + if (record?.deferredFinalize && record.card) { + record.card.state = AICardStatus.FINISHED; + record.card.lastUpdated = Date.now(); + } + clearCardRunDeferredFinalize(outTrackId); +} + export async function applyResolvedPatch( outTrackId: string, _decision: string, @@ -26,8 +56,14 @@ export async function applyResolvedPatch( cardStillActive: boolean, config?: Pick, ): Promise { - await updateCardVariables(outTrackId, buildApprovalClearedCardParams(cardStillActive), token, config); + await updateCardVariables( + outTrackId, + buildTerminalPatchParams(outTrackId, cardStillActive), + token, + config, + ); clearCardRunPendingApproval(outTrackId); + completeDeferredFinalize(outTrackId); } export async function applyExpiredPatch( @@ -36,6 +72,12 @@ export async function applyExpiredPatch( cardStillActive: boolean, config?: Pick, ): Promise { - await updateCardVariables(outTrackId, buildApprovalClearedCardParams(cardStillActive), token, config); + await updateCardVariables( + outTrackId, + buildTerminalPatchParams(outTrackId, cardStillActive), + token, + config, + ); clearCardRunPendingApproval(outTrackId); + completeDeferredFinalize(outTrackId); } diff --git a/src/card-service.ts b/src/card-service.ts index 87fddabe..5ff3cf43 100644 --- a/src/card-service.ts +++ b/src/card-service.ts @@ -6,7 +6,7 @@ import { getAccessToken } from "./auth"; import { updateCardVariables } from "./card-callback-service"; import { DINGTALK_CARD_TEMPLATE, STOP_ACTION_VISIBLE, STOP_ACTION_HIDDEN } from "./card/card-template"; import { APPROVAL_CARD_INITIAL } from "./approval/approval-card-state"; -import { resolveCardRun } from "./card/card-run-registry"; +import { markCardRunDeferredFinalize, resolveCardRun } from "./card/card-run-registry"; import { getLogger } from "./logger-context"; import { resolveRobotCode, stripTargetPrefix } from "./config"; import { resolveOriginalPeerId } from "./peer-id-registry"; @@ -1143,12 +1143,19 @@ export async function commitAICardBlocks( await finalizeAICardStreamingLifecycleIfNeeded(card, log); const template = DINGTALK_CARD_TEMPLATE; + const trackId = card.outTrackId || card.cardInstanceId; + // DingTalk constraint: action buttons (including approve_btns) only render + // while the card is in PROCESSING/INPUTING state. flowStatus=3 (FINISHED) + // hides every action button regardless of show_approve_btns. So when an + // approval is pending we must defer the finalize: skip flowStatus=3, skip + // the in-memory state transition, skip removePendingCard. The deferred + // finalize completes from applyResolvedPatch / applyExpiredPatch. + const approvalPending = Boolean(resolveCardRun(trackId)?.pendingApprovalId); const updates: Record = { [template.blockListKey]: options.blockListJson, [template.streamingKey]: options.content, // markdown content for display [template.copyContentKey]: options.content, // same markdown as String type for card copy action - flowStatus: 3, // completed state - V2 template hides stop button automatically - ...approvalParamsForTerminal(card.outTrackId || card.cardInstanceId), + ...(approvalPending ? {} : { flowStatus: 3, ...APPROVAL_CARD_INITIAL }), }; // Optional fields @@ -1160,14 +1167,14 @@ export async function commitAICardBlocks( } log?.debug?.( - `[DingTalk][AICard] Finalizing via instances API: outTrackId=${card.outTrackId || card.cardInstanceId} ` + - `blockListLen=${options.blockListJson.length} contentLen=${options.content.length} flowStatus=3` + + `[DingTalk][AICard] Finalizing via instances API: outTrackId=${trackId} ` + + `blockListLen=${options.blockListJson.length} contentLen=${options.content.length} flowStatus=${approvalPending ? "deferred" : 3}` + (options.statusLine ? ` statusLine="${options.statusLine}"` : ""), ); try { await updateCardVariables( - card.outTrackId || card.cardInstanceId, + trackId, updates, card.accessToken, card.config, @@ -1196,11 +1203,19 @@ export async function commitAICardBlocks( ); } + if (approvalPending) { + markCardRunDeferredFinalize(trackId); + log?.info?.( + `[DingTalk][AICard] Card finalize deferred (approval pending): outTrackId=${trackId} state=${card.state}`, + ); + return; + } + // Update local state card.state = AICardStatus.FINISHED; card.lastUpdated = Date.now(); removePendingCard(card, log); - log?.info?.(`[DingTalk][AICard] Card finalized: outTrackId=${card.outTrackId || card.cardInstanceId} state=FINISHED`); + log?.info?.(`[DingTalk][AICard] Card finalized: outTrackId=${trackId} state=FINISHED`); } diff --git a/src/card/card-run-registry.ts b/src/card/card-run-registry.ts index e1793e6e..242bcfb7 100644 --- a/src/card/card-run-registry.ts +++ b/src/card/card-run-registry.ts @@ -21,6 +21,12 @@ export interface CardRunRecord { card?: AICardInstance; controller?: CardDraftController; pendingApprovalId?: string; + /** When commitAICardBlocks ran with a pending approval, the flowStatus=3 PUT and + * state transition were skipped so the card stays in PROCESSING/INPUTING and + * DingTalk keeps the approval buttons visible. Set true at defer time so + * applyResolvedPatch / applyExpiredPatch knows to complete the deferred + * finalize on resolve/expire. */ + deferredFinalize?: boolean; stopRequestedAt?: number; registeredAt: number; } @@ -129,6 +135,20 @@ export function clearCardRunPendingApproval(outTrackId: string): void { } } +export function markCardRunDeferredFinalize(outTrackId: string): void { + const record = records.get(outTrackId.trim()); + if (record) { + record.deferredFinalize = true; + } +} + +export function clearCardRunDeferredFinalize(outTrackId: string): void { + const record = records.get(outTrackId.trim()); + if (record) { + record.deferredFinalize = undefined; + } +} + /** * Find the most recently registered card run for a given account + conversation. * Uses case-insensitive match of the conversationId within sessionKey. diff --git a/src/inbound-handler.ts b/src/inbound-handler.ts index b14257f6..fcdb5e75 100644 --- a/src/inbound-handler.ts +++ b/src/inbound-handler.ts @@ -10,7 +10,12 @@ import { tryInterceptApproveCommand } from "./approval/approval-command-intercep import { APPROVE_COMMAND_RE } from "./approval/approval-command-parser"; import { getAccessToken } from "./auth"; import { createAICard, commitAICardBlocks, isCardInTerminalState } from "./card-service"; -import { isCardRunStopRequested, registerCardRun, removeCardRun } from "./card/card-run-registry"; +import { + isCardRunStopRequested, + registerCardRun, + removeCardRun, + resolveCardRun, +} from "./card/card-run-registry"; import { renderStatusLine } from "./card/statusline-renderer"; import { handleInboundCommandDispatch } from "./command/inbound-command-dispatch-service"; import { @@ -2246,12 +2251,17 @@ export async function handleDingTalkMessage(params: HandleDingTalkMessageParams) await strategy.finalize(); } finally { - // Only remove the registry entry if no stop was requested. When a stop is - // in progress, card-stop-handler may still be running async operations - // (finalize card, hide button, gateway abort) that read the record. - // In that case, let the 30-minute TTL sweep handle cleanup. + // Only remove the registry entry when the run is fully closed. Three + // cases keep the record alive past this finally block: + // - a stop is in progress (card-stop-handler may still be reading it), + // - an approval is pending (applyResolvedPatch will need the record), + // - the card was deferred-finalize while approval pending (same). + // The 30-minute TTL sweep handles cleanup if approval never resolves. if (currentOutTrackId && !isCardRunStopRequested(currentOutTrackId)) { - removeCardRun(currentOutTrackId); + const record = resolveCardRun(currentOutTrackId); + if (!record?.pendingApprovalId && !record?.deferredFinalize) { + removeCardRun(currentOutTrackId); + } } await waitForDynamicAckDispose({ dispose: () => dynamicAckReactionController.dispose(MIN_THINKING_REACTION_VISIBLE_MS), diff --git a/tests/unit/approval-card-patcher.test.ts b/tests/unit/approval-card-patcher.test.ts index 0d750223..eab693d9 100644 --- a/tests/unit/approval-card-patcher.test.ts +++ b/tests/unit/approval-card-patcher.test.ts @@ -7,25 +7,35 @@ vi.mock("../../src/card-callback-service", () => ({ vi.mock("../../src/card/card-run-registry", () => ({ markCardRunPendingApproval: vi.fn(), clearCardRunPendingApproval: vi.fn(), + clearCardRunDeferredFinalize: vi.fn(), + resolveCardRun: vi.fn(), })); const { applyExpiredPatch, applyPendingPatch, applyResolvedPatch } = await import( "../../src/approval/approval-card-patcher" ); const { updateCardVariables } = await import("../../src/card-callback-service"); -const { clearCardRunPendingApproval, markCardRunPendingApproval } = await import( - "../../src/card/card-run-registry" -); +const { + clearCardRunDeferredFinalize, + clearCardRunPendingApproval, + markCardRunPendingApproval, + resolveCardRun, +} = await import("../../src/card/card-run-registry"); +const { AICardStatus } = await import("../../src/types"); const mockUpdate = vi.mocked(updateCardVariables); const mockMark = vi.mocked(markCardRunPendingApproval); const mockClear = vi.mocked(clearCardRunPendingApproval); +const mockClearDeferred = vi.mocked(clearCardRunDeferredFinalize); +const mockResolveRun = vi.mocked(resolveCardRun); describe("approval-card-patcher", () => { beforeEach(() => { mockUpdate.mockReset().mockResolvedValue(200); mockMark.mockReset(); mockClear.mockReset(); + mockClearDeferred.mockReset(); + mockResolveRun.mockReset().mockReturnValue(null); }); it("applies pending card variables and records fallback approval id", async () => { @@ -50,6 +60,7 @@ describe("approval-card-patcher", () => { {}, ); expect(mockClear).toHaveBeenCalledWith("ot1"); + expect(mockClearDeferred).toHaveBeenCalledWith("ot1"); }); it("does not restore stop action for inactive resolved cards", async () => { @@ -63,6 +74,51 @@ describe("approval-card-patcher", () => { ); }); + it("includes flowStatus=3 on resolve when the card was deferred-finalize", async () => { + mockResolveRun.mockReturnValue({ + outTrackId: "ot1", + accountId: "default", + sessionKey: "session:abc", + agentId: "main", + registeredAt: Date.now(), + deferredFinalize: true, + card: { + state: AICardStatus.INPUTING, + lastUpdated: 0, + } as never, + }); + + await applyResolvedPatch("ot1", "allow-once", "tok", true, {}); + + expect(mockUpdate).toHaveBeenCalledWith( + "ot1", + { show_approve_btns: "false", approveId: "", hasAction: "true", flowStatus: 3 }, + "tok", + {}, + ); + expect(mockClearDeferred).toHaveBeenCalledWith("ot1"); + }); + + it("transitions in-memory card state to FINISHED when completing a deferred finalize", async () => { + const cardRef = { + state: AICardStatus.INPUTING, + lastUpdated: 0, + } as never; + mockResolveRun.mockReturnValue({ + outTrackId: "ot1", + accountId: "default", + sessionKey: "session:abc", + agentId: "main", + registeredAt: Date.now(), + deferredFinalize: true, + card: cardRef, + }); + + await applyResolvedPatch("ot1", "deny", "tok", false, {}); + + expect((cardRef as { state: number }).state).toBe(AICardStatus.FINISHED); + }); + it("applies expired variables using the same cleared field set", async () => { await applyExpiredPatch("ot1", "tok", false, {}); @@ -73,5 +129,6 @@ describe("approval-card-patcher", () => { {}, ); expect(mockClear).toHaveBeenCalledWith("ot1"); + expect(mockClearDeferred).toHaveBeenCalledWith("ot1"); }); }); From e803c22f84eb7c8ac81b4322b93c264761a2fbfb Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Wed, 20 May 2026 00:15:15 +0800 Subject: [PATCH 41/44] fix(approval): defer streaming-lifecycle finalize while approval pending Buttons still disappeared after the previous defer-finalize fix because commitAICardBlocks calls finalizeAICardStreamingLifecycleIfNeeded before the deferred-aware updateCardVariables. That helper PUTs /v1.0/card/streaming with isFinalize=true, closing DingTalk's streaming lifecycle. The DingTalk client treats that lifecycle close as another "card finished" signal independent of flowStatus, and hides every action button (including approve_btns) regardless of show_approve_btns. - commitAICardBlocks: when approvalPending, also skip finalizeAICardStreamingLifecycleIfNeeded so the card's streaming lifecycle stays open. - finalizeAICardStreamingLifecycleIfNeeded: export so the approval patcher can close the lifecycle from the deferred-finalize completion path. - approval-card-patcher.completeDeferredFinalize: now async; call finalizeAICardStreamingLifecycleIfNeeded on the in-memory card before transitioning state to FINISHED so DingTalk sees both flowStatus=3 and isFinalize=true at resolve/expire time. --- src/approval/approval-card-patcher.ts | 11 ++++++++--- src/card-service.ts | 22 ++++++++++++++-------- 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/approval/approval-card-patcher.ts b/src/approval/approval-card-patcher.ts index 06f9bb66..9b9e5c80 100644 --- a/src/approval/approval-card-patcher.ts +++ b/src/approval/approval-card-patcher.ts @@ -1,4 +1,5 @@ import { updateCardVariables } from "../card-callback-service"; +import { finalizeAICardStreamingLifecycleIfNeeded } from "../card-service"; import { buildApprovalClearedCardParams, buildApprovalPendingCardParams, @@ -40,9 +41,13 @@ function buildTerminalPatchParams( return params; } -function completeDeferredFinalize(outTrackId: string): void { +async function completeDeferredFinalize(outTrackId: string): Promise { const record = resolveCardRun(outTrackId); if (record?.deferredFinalize && record.card) { + // commitAICardBlocks skipped the DingTalk streaming-lifecycle close when + // it deferred this finalize. Close it now so DingTalk treats the card as + // fully finished (in addition to flowStatus=3 the caller already PUT). + await finalizeAICardStreamingLifecycleIfNeeded(record.card).catch(() => {}); record.card.state = AICardStatus.FINISHED; record.card.lastUpdated = Date.now(); } @@ -63,7 +68,7 @@ export async function applyResolvedPatch( config, ); clearCardRunPendingApproval(outTrackId); - completeDeferredFinalize(outTrackId); + await completeDeferredFinalize(outTrackId); } export async function applyExpiredPatch( @@ -79,5 +84,5 @@ export async function applyExpiredPatch( config, ); clearCardRunPendingApproval(outTrackId); - completeDeferredFinalize(outTrackId); + await completeDeferredFinalize(outTrackId); } diff --git a/src/card-service.ts b/src/card-service.ts index 5ff3cf43..300f6a65 100644 --- a/src/card-service.ts +++ b/src/card-service.ts @@ -1085,7 +1085,7 @@ export async function clearAICardStreamingContent( } } -async function finalizeAICardStreamingLifecycleIfNeeded( +export async function finalizeAICardStreamingLifecycleIfNeeded( card: AICardInstance, log?: Logger, ): Promise { @@ -1140,17 +1140,23 @@ export async function commitAICardBlocks( } await ensureFreshToken(card, log); - await finalizeAICardStreamingLifecycleIfNeeded(card, log); - - const template = DINGTALK_CARD_TEMPLATE; const trackId = card.outTrackId || card.cardInstanceId; // DingTalk constraint: action buttons (including approve_btns) only render // while the card is in PROCESSING/INPUTING state. flowStatus=3 (FINISHED) - // hides every action button regardless of show_approve_btns. So when an - // approval is pending we must defer the finalize: skip flowStatus=3, skip - // the in-memory state transition, skip removePendingCard. The deferred - // finalize completes from applyResolvedPatch / applyExpiredPatch. + // hides every action button regardless of show_approve_btns. Equally, the + // PUT /v1.0/card/streaming { isFinalize: true } call closes DingTalk's + // streaming lifecycle which the client also treats as "card done" and + // therefore hides action buttons. So when an approval is pending we must + // defer both signals: skip the streaming-lifecycle finalize, skip + // flowStatus=3, skip the in-memory state transition, skip + // removePendingCard. applyResolvedPatch / applyExpiredPatch completes the + // deferred finalize once the approval terminates. const approvalPending = Boolean(resolveCardRun(trackId)?.pendingApprovalId); + if (!approvalPending) { + await finalizeAICardStreamingLifecycleIfNeeded(card, log); + } + + const template = DINGTALK_CARD_TEMPLATE; const updates: Record = { [template.blockListKey]: options.blockListJson, [template.streamingKey]: options.content, // markdown content for display From 0d38b71472612221951d809ae3422e642246b313 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Wed, 20 May 2026 00:30:39 +0800 Subject: [PATCH 42/44] fix(approval): tighten deferred-finalize lifecycle and fence rendering MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-device validation surfaced three classes of issue beyond the initial defer-finalize patch; address them together so the buttons stay visible, terminal patches complete cleanly, and the existing process-block markdown bug no longer leaks raw tags whenever an approval text contains code fences. CR-1 narrow deferred-finalize ownership to card-service - New export completeDeferredAICardFinalize(outTrackId, log) owns the full deferred-finalize completion: close DingTalk streaming lifecycle, transition the in-memory card.state to FINISHED, drop the pending-card persistence row, clear deferredFinalize, and remove the card-run registry entry. approval-card-patcher delegates to it rather than mutating record.card directly. CR-2 close pending-mark / commit race - applyPendingPatch now markCardRunPendingApproval BEFORE the PUT so any commitAICardBlocks that fires concurrently sees the pending state and defers flowStatus=3. On PUT failure we clear the mark and, if commit already deferred, rescue the card by terminalizing via applyExpiredPatch. CR-3 active terminal cleanup - completeDeferredAICardFinalize calls removeCardRun once the card transitions to FINISHED, so resolve/expire terminations cleanly reclaim the registry slot instead of waiting on the 30-minute TTL sweep. inbound-handler still keeps the record alive while pending or deferred — terminal completion now actively closes it. Fence-safe process-block wrap - wrapProcessBlockMarkdown was splitting multi-line ```lang ... ``` fences across separate wrappers, breaking DingTalk markdown parsing whenever an approval reply text contained code fences and leaving raw markup on the card. The wrap now toggles fence state and emits fence delimiters / body lines under a plain `> ` blockquote prefix, only wrapping outside-fence prose with the small-grey font tokens. Tests - approval-card-patcher: pre-mark ordering, PUT-failure rollback, rescue path when commit deferred mid-PUT, terminal completion is delegated to completeDeferredAICardFinalize. - card-service: commitAICardBlocks defers (no flowStatus=3, state stays INPUTING, deferredFinalize set) when pending; finalizes normally otherwise. completeDeferredAICardFinalize transitions state + removes registry entry; no-op when no defer flag. - card-draft-controller: fence-integrity regression covering the approval-required reply payload shape. Housekeeping - .gitignore: exclude pnpm pack tarball output (*.tgz) so local deploy artifacts don't leak into commits. --- .gitignore | 3 + src/approval/approval-card-patcher.ts | 43 +++---- src/card-draft-controller.ts | 32 ++++- src/card-service.ts | 40 +++++- tests/unit/approval-card-patcher.test.ts | 92 ++++++++------ tests/unit/card-draft-controller.test.ts | 39 ++++++ tests/unit/card-service.test.ts | 154 +++++++++++++++++++++++ 7 files changed, 340 insertions(+), 63 deletions(-) diff --git a/.gitignore b/.gitignore index 666612ba..bfad9a64 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ output/ .vercel .env*.local .gitnexus + +# pnpm pack output (local-only tarball used for ad-hoc deploys) +*.tgz diff --git a/src/approval/approval-card-patcher.ts b/src/approval/approval-card-patcher.ts index 9b9e5c80..e126b6dd 100644 --- a/src/approval/approval-card-patcher.ts +++ b/src/approval/approval-card-patcher.ts @@ -1,16 +1,15 @@ import { updateCardVariables } from "../card-callback-service"; -import { finalizeAICardStreamingLifecycleIfNeeded } from "../card-service"; +import { completeDeferredAICardFinalize } from "../card-service"; import { buildApprovalClearedCardParams, buildApprovalPendingCardParams, } from "./approval-card-state"; import { - clearCardRunDeferredFinalize, clearCardRunPendingApproval, markCardRunPendingApproval, resolveCardRun, } from "../card/card-run-registry"; -import { AICardStatus, type DingTalkConfig } from "../types"; +import type { DingTalkConfig } from "../types"; export async function applyPendingPatch( outTrackId: string, @@ -18,15 +17,26 @@ export async function applyPendingPatch( token: string, config?: Pick, ): Promise { - await updateCardVariables(outTrackId, buildApprovalPendingCardParams(approvalId), token, config); + // Pre-mark the run so a concurrent commitAICardBlocks that fires while the + // pending PUT is in flight defers the finalize (it keys the decision on + // pendingApprovalId). On PUT failure we roll back the mark and, if commit + // already deferred, rescue the card by terminalizing via applyExpiredPatch. markCardRunPendingApproval(outTrackId, approvalId); + try { + await updateCardVariables(outTrackId, buildApprovalPendingCardParams(approvalId), token, config); + } catch (err) { + clearCardRunPendingApproval(outTrackId); + if (resolveCardRun(outTrackId)?.deferredFinalize) { + await applyExpiredPatch(outTrackId, token, false, config); + } + throw err; + } } /** - * Resolve or expire an approval that lives on a DingTalk AI card. If the card - * was deferred-finalized (commitAICardBlocks skipped flowStatus=3 while the - * approval was pending), include flowStatus=3 in the PUT so the card finishes - * now that the buttons no longer need to render. + * Build terminal-state cardParamMap for an approval that has resolved or + * expired. When the run was deferred-finalize while the approval pended, + * include flowStatus=3 so DingTalk completes the card in the same PUT. */ function buildTerminalPatchParams( outTrackId: string, @@ -41,19 +51,6 @@ function buildTerminalPatchParams( return params; } -async function completeDeferredFinalize(outTrackId: string): Promise { - const record = resolveCardRun(outTrackId); - if (record?.deferredFinalize && record.card) { - // commitAICardBlocks skipped the DingTalk streaming-lifecycle close when - // it deferred this finalize. Close it now so DingTalk treats the card as - // fully finished (in addition to flowStatus=3 the caller already PUT). - await finalizeAICardStreamingLifecycleIfNeeded(record.card).catch(() => {}); - record.card.state = AICardStatus.FINISHED; - record.card.lastUpdated = Date.now(); - } - clearCardRunDeferredFinalize(outTrackId); -} - export async function applyResolvedPatch( outTrackId: string, _decision: string, @@ -68,7 +65,7 @@ export async function applyResolvedPatch( config, ); clearCardRunPendingApproval(outTrackId); - await completeDeferredFinalize(outTrackId); + await completeDeferredAICardFinalize(outTrackId); } export async function applyExpiredPatch( @@ -84,5 +81,5 @@ export async function applyExpiredPatch( config, ); clearCardRunPendingApproval(outTrackId); - await completeDeferredFinalize(outTrackId); + await completeDeferredAICardFinalize(outTrackId); } diff --git a/src/card-draft-controller.ts b/src/card-draft-controller.ts index 18ef3959..509f6858 100644 --- a/src/card-draft-controller.ts +++ b/src/card-draft-controller.ts @@ -91,10 +91,34 @@ function normalizeAnswerText(text: string | undefined): string { } function wrapProcessBlockMarkdown(text: string): string { - const lines = text.split("\n").filter((line) => line.trim()); - return lines - .map((line) => `> ${line}`) - .join("\n"); + // Per-line `> ` wrap renders the process block as DingTalk's + // small-grey quoted style. Code fences (```lang ... ```) must be emitted as a + // single contiguous block — splitting font tags across fence boundaries + // breaks the renderer and leaks raw markup. Fence content lines and + // the fence delimiters themselves keep the `> ` blockquote prefix to preserve + // the surrounding quote continuity but skip the wrap. + const lines = text.split("\n"); + const result: string[] = []; + let insideFence = false; + for (const line of lines) { + const trimmed = line.trim(); + if (trimmed.startsWith("```")) { + insideFence = !insideFence; + result.push(`> ${line}`); + continue; + } + if (insideFence) { + result.push(`> ${line}`); + continue; + } + if (!trimmed) { + continue; + } + result.push( + `> ${line}`, + ); + } + return result.join("\n"); } export function createCardDraftController(params: { diff --git a/src/card-service.ts b/src/card-service.ts index 300f6a65..10130fad 100644 --- a/src/card-service.ts +++ b/src/card-service.ts @@ -6,7 +6,12 @@ import { getAccessToken } from "./auth"; import { updateCardVariables } from "./card-callback-service"; import { DINGTALK_CARD_TEMPLATE, STOP_ACTION_VISIBLE, STOP_ACTION_HIDDEN } from "./card/card-template"; import { APPROVAL_CARD_INITIAL } from "./approval/approval-card-state"; -import { markCardRunDeferredFinalize, resolveCardRun } from "./card/card-run-registry"; +import { + clearCardRunDeferredFinalize, + markCardRunDeferredFinalize, + removeCardRun, + resolveCardRun, +} from "./card/card-run-registry"; import { getLogger } from "./logger-context"; import { resolveRobotCode, stripTargetPrefix } from "./config"; import { resolveOriginalPeerId } from "./peer-id-registry"; @@ -1105,6 +1110,39 @@ export async function finalizeAICardStreamingLifecycleIfNeeded( } } +/** + * Complete the deferred finalize for a card whose commitAICardBlocks ran while + * an approval was pending. Closes DingTalk's streaming lifecycle (which the + * commit deferred too), transitions the in-memory card state to FINISHED, + * drops the pending-card persistence row, clears the deferredFinalize flag, + * and removes the card-run registry entry so subsequent lookups treat the run + * as fully closed. No-op when no card-run record exists or deferredFinalize + * is not set. + */ +export async function completeDeferredAICardFinalize( + outTrackId: string, + log?: Logger, +): Promise { + const record = resolveCardRun(outTrackId); + if (!record?.deferredFinalize) { + if (record) { + clearCardRunDeferredFinalize(outTrackId); + } + return; + } + if (record.card) { + await finalizeAICardStreamingLifecycleIfNeeded(record.card, log); + record.card.state = AICardStatus.FINISHED; + record.card.lastUpdated = Date.now(); + removePendingCard(record.card, log); + } + clearCardRunDeferredFinalize(outTrackId); + removeCardRun(outTrackId); + log?.info?.( + `[DingTalk][AICard] Deferred finalize completed: outTrackId=${outTrackId} state=FINISHED`, + ); +} + /** * Options for finalizing an AI Card via instances API. * All variables are written in a single API call for V2 template compatibility. diff --git a/tests/unit/approval-card-patcher.test.ts b/tests/unit/approval-card-patcher.test.ts index eab693d9..81c18ed8 100644 --- a/tests/unit/approval-card-patcher.test.ts +++ b/tests/unit/approval-card-patcher.test.ts @@ -4,10 +4,13 @@ vi.mock("../../src/card-callback-service", () => ({ updateCardVariables: vi.fn().mockResolvedValue(200), })); +vi.mock("../../src/card-service", () => ({ + completeDeferredAICardFinalize: vi.fn().mockResolvedValue(undefined), +})); + vi.mock("../../src/card/card-run-registry", () => ({ markCardRunPendingApproval: vi.fn(), clearCardRunPendingApproval: vi.fn(), - clearCardRunDeferredFinalize: vi.fn(), resolveCardRun: vi.fn(), })); @@ -15,42 +18,85 @@ const { applyExpiredPatch, applyPendingPatch, applyResolvedPatch } = await impor "../../src/approval/approval-card-patcher" ); const { updateCardVariables } = await import("../../src/card-callback-service"); +const { completeDeferredAICardFinalize } = await import("../../src/card-service"); const { - clearCardRunDeferredFinalize, clearCardRunPendingApproval, markCardRunPendingApproval, resolveCardRun, } = await import("../../src/card/card-run-registry"); -const { AICardStatus } = await import("../../src/types"); const mockUpdate = vi.mocked(updateCardVariables); +const mockComplete = vi.mocked(completeDeferredAICardFinalize); const mockMark = vi.mocked(markCardRunPendingApproval); const mockClear = vi.mocked(clearCardRunPendingApproval); -const mockClearDeferred = vi.mocked(clearCardRunDeferredFinalize); const mockResolveRun = vi.mocked(resolveCardRun); describe("approval-card-patcher", () => { beforeEach(() => { mockUpdate.mockReset().mockResolvedValue(200); + mockComplete.mockReset().mockResolvedValue(undefined); mockMark.mockReset(); mockClear.mockReset(); - mockClearDeferred.mockReset(); mockResolveRun.mockReset().mockReturnValue(null); }); - it("applies pending card variables and records fallback approval id", async () => { + it("pre-marks the run pending before issuing the PUT (CR-2 race fix)", async () => { + const callOrder: string[] = []; + mockMark.mockImplementation(() => { + callOrder.push("mark"); + }); + mockUpdate.mockImplementation(async () => { + callOrder.push("put"); + return 200; + }); + await applyPendingPatch("ot1", "abc123", "tok", { bypassProxyForSend: true }); + expect(callOrder).toEqual(["mark", "put"]); + expect(mockMark).toHaveBeenCalledWith("ot1", "abc123"); expect(mockUpdate).toHaveBeenCalledWith( "ot1", { show_approve_btns: "true", approveId: "abc123", hasAction: "false" }, "tok", { bypassProxyForSend: true }, ); + }); + + it("rolls back pending mark when the PUT fails", async () => { + mockUpdate.mockRejectedValueOnce(new Error("network down")); + + await expect(applyPendingPatch("ot1", "abc123", "tok", {})).rejects.toThrow("network down"); + expect(mockMark).toHaveBeenCalledWith("ot1", "abc123"); + expect(mockClear).toHaveBeenCalledWith("ot1"); + }); + + it("rescues a deferred-finalize card when the pending PUT fails after commit deferred", async () => { + // Simulate the race: commit deferred while our PUT was in flight. + mockResolveRun.mockReturnValue({ + outTrackId: "ot1", + accountId: "default", + sessionKey: "session:abc", + agentId: "main", + registeredAt: Date.now(), + deferredFinalize: true, + }); + mockUpdate.mockRejectedValueOnce(new Error("network down")); + + await expect(applyPendingPatch("ot1", "abc123", "tok", {})).rejects.toThrow("network down"); + + // Rollback ran, and the rescue terminal PUT fired (the second updateCardVariables call). + expect(mockClear).toHaveBeenCalledWith("ot1"); + expect(mockUpdate).toHaveBeenCalledWith( + "ot1", + { show_approve_btns: "false", approveId: "", hasAction: "false", flowStatus: 3 }, + "tok", + {}, + ); + expect(mockComplete).toHaveBeenCalledWith("ot1"); }); - it("applies resolved variables and clears fallback approval id", async () => { + it("applies resolved variables and delegates terminal completion", async () => { await applyResolvedPatch("ot1", "allow-once", "tok", true, {}); expect(mockUpdate).toHaveBeenCalledWith( @@ -60,7 +106,7 @@ describe("approval-card-patcher", () => { {}, ); expect(mockClear).toHaveBeenCalledWith("ot1"); - expect(mockClearDeferred).toHaveBeenCalledWith("ot1"); + expect(mockComplete).toHaveBeenCalledWith("ot1"); }); it("does not restore stop action for inactive resolved cards", async () => { @@ -82,10 +128,6 @@ describe("approval-card-patcher", () => { agentId: "main", registeredAt: Date.now(), deferredFinalize: true, - card: { - state: AICardStatus.INPUTING, - lastUpdated: 0, - } as never, }); await applyResolvedPatch("ot1", "allow-once", "tok", true, {}); @@ -96,30 +138,10 @@ describe("approval-card-patcher", () => { "tok", {}, ); - expect(mockClearDeferred).toHaveBeenCalledWith("ot1"); - }); - - it("transitions in-memory card state to FINISHED when completing a deferred finalize", async () => { - const cardRef = { - state: AICardStatus.INPUTING, - lastUpdated: 0, - } as never; - mockResolveRun.mockReturnValue({ - outTrackId: "ot1", - accountId: "default", - sessionKey: "session:abc", - agentId: "main", - registeredAt: Date.now(), - deferredFinalize: true, - card: cardRef, - }); - - await applyResolvedPatch("ot1", "deny", "tok", false, {}); - - expect((cardRef as { state: number }).state).toBe(AICardStatus.FINISHED); + expect(mockComplete).toHaveBeenCalledWith("ot1"); }); - it("applies expired variables using the same cleared field set", async () => { + it("applies expired variables and delegates terminal completion", async () => { await applyExpiredPatch("ot1", "tok", false, {}); expect(mockUpdate).toHaveBeenCalledWith( @@ -129,6 +151,6 @@ describe("approval-card-patcher", () => { {}, ); expect(mockClear).toHaveBeenCalledWith("ot1"); - expect(mockClearDeferred).toHaveBeenCalledWith("ot1"); + expect(mockComplete).toHaveBeenCalledWith("ot1"); }); }); diff --git a/tests/unit/card-draft-controller.test.ts b/tests/unit/card-draft-controller.test.ts index 1eaa5bea..ae62e679 100644 --- a/tests/unit/card-draft-controller.test.ts +++ b/tests/unit/card-draft-controller.test.ts @@ -168,6 +168,45 @@ describe("card-draft-controller", () => { }); }); + it("preserves code-fence integrity in tool blocks", async () => { + // Regression: approval reply payloads contain multi-line ```lang ... ``` + // code fences. The previous per-line wrap split the fence open/close + // across separate wrappers, breaking DingTalk markdown parsing and + // leaking raw tags into the rendered card. + const card = makeCard(); + const ctrl = createCardDraftController({ card, throttleMs: 0 }) as any; + + const input = [ + "Approval required.", + "Run:", + "```txt", + "/approve 0d673f7b allow-once", + "```", + "Pending command:", + "```sh", + "ls -la /root/.openclaw/workspace", + "```", + ].join("\n"); + + await ctrl.updateTool(input); + await vi.advanceTimersByTimeAsync(0); + + const sentContent = updateAICardBlockListMock.mock.calls[0]?.[1] as string; + const blocks = parseBlocks(sentContent); + const markdown = blocks[0]?.markdown ?? ""; + + expect(markdown).toContain("> ```txt"); + expect(markdown).toContain("> ```sh"); + expect(markdown).toContain("> /approve 0d673f7b allow-once"); + expect(markdown).toContain("> ls -la /root/.openclaw/workspace"); + expect(markdown).toMatch(/^> ]+>Approval required\.<\/font>/m); + expect(markdown).toMatch(/^> ]+>Run:<\/font>/m); + expect(markdown).toMatch(/^> ]+>Pending command:<\/font>/m); + expect(markdown).not.toMatch(/]+>```/); + expect(markdown).not.toMatch(/]+>\/approve/); + expect(markdown).not.toMatch(/]+>ls -la/); + }); + it("answer rendering keeps the latest thinking block in the same timeline", async () => { const card = makeCard(); const ctrl = createCardDraftController({ card, throttleMs: 0 }); diff --git a/tests/unit/card-service.test.ts b/tests/unit/card-service.test.ts index 5e5d5f53..072fdbf6 100644 --- a/tests/unit/card-service.test.ts +++ b/tests/unit/card-service.test.ts @@ -1276,4 +1276,158 @@ describe('commitAICardBlocks · approval lifecycle ownership', () => { expect(paramMap.show_approve_btns).toBeUndefined(); expect(paramMap.approveId).toBeUndefined(); }); + + it('defers finalize when an approval is pending (no flowStatus=3, state stays INPUTING, deferredFinalize flag set)', async () => { + const { commitAICardBlocks } = await import('../../src/card-service'); + const { + registerCardRun, + markCardRunPendingApproval, + resolveCardRun, + } = await import('../../src/card/card-run-registry'); + registerCardRun('track_defer', { + accountId: 'main', + sessionKey: 's1', + agentId: 'a1', + }); + markCardRunPendingApproval('track_defer', 'approval-defer'); + mockedAxios.put.mockResolvedValue({ status: 200, data: { ok: true } }); + + const card = { + cardInstanceId: 'card_defer', + outTrackId: 'track_defer', + accessToken: 'tok', + conversationId: 'cid_1', + state: AICardStatus.INPUTING, + createdAt: Date.now(), + lastUpdated: Date.now(), + config: { clientId: 'id', clientSecret: 'sec' } as any, + } as any; + await commitAICardBlocks(card, { + blockListJson: JSON.stringify([{ type: 0, markdown: 'done' }]), + content: 'done', + }); + + const paramMap = mockedAxios.put.mock.calls[0][1].cardData.cardParamMap; + // No flowStatus=3 PUT — the deferred state keeps the card in PROCESSING/INPUTING + // so DingTalk continues rendering the approve_btns ButtonGroup. + expect(paramMap.flowStatus).toBeUndefined(); + // Card state NOT transitioned to FINISHED yet. + expect(card.state).toBe(AICardStatus.INPUTING); + // deferredFinalize flag set so applyResolvedPatch / applyExpiredPatch can + // complete the finalize at resolve/expire time. + expect(resolveCardRun('track_defer')?.deferredFinalize).toBe(true); + }); + + it('finalizes normally (flowStatus=3, state FINISHED, deferredFinalize unset) when no approval pending', async () => { + const { commitAICardBlocks } = await import('../../src/card-service'); + const { registerCardRun, resolveCardRun } = await import( + '../../src/card/card-run-registry' + ); + registerCardRun('track_normal', { + accountId: 'main', + sessionKey: 's1', + agentId: 'a1', + }); + mockedAxios.put.mockResolvedValue({ status: 200, data: { ok: true } }); + + const card = { + cardInstanceId: 'card_normal', + outTrackId: 'track_normal', + accessToken: 'tok', + conversationId: 'cid_1', + state: AICardStatus.INPUTING, + createdAt: Date.now(), + lastUpdated: Date.now(), + config: { clientId: 'id', clientSecret: 'sec' } as any, + } as any; + await commitAICardBlocks(card, { + blockListJson: JSON.stringify([{ type: 0, markdown: 'done' }]), + content: 'done', + }); + + const paramMap = mockedAxios.put.mock.calls[0][1].cardData.cardParamMap; + // cardParamMap is string-serialized by updateCardVariables, so flowStatus + // arrives as "3" rather than 3. + expect(String(paramMap.flowStatus)).toBe('3'); + expect(card.state).toBe(AICardStatus.FINISHED); + expect(resolveCardRun('track_normal')?.deferredFinalize).toBeFalsy(); + }); +}); + +describe('completeDeferredAICardFinalize', () => { + beforeEach(async () => { + mockedAxios.mockReset(); + mockedAxios.post.mockReset(); + mockedAxios.put.mockReset(); + mockedGetAccessToken.mockReset(); + mockedGetAccessToken.mockResolvedValue('token_abc'); + const { clearCardRunRegistryForTest } = await import('../../src/card/card-run-registry'); + clearCardRunRegistryForTest(); + }); + + it('transitions a deferred card to FINISHED and removes its registry entry', async () => { + const { + completeDeferredAICardFinalize, + } = await import('../../src/card-service'); + const { + registerCardRun, + markCardRunPendingApproval, + markCardRunDeferredFinalize, + resolveCardRun, + attachCardRunController, + } = await import('../../src/card/card-run-registry'); + + const card: any = { + cardInstanceId: 'card_finalize', + outTrackId: 'track_finalize', + accessToken: 'tok', + conversationId: 'cid_1', + state: AICardStatus.INPUTING, + createdAt: Date.now(), + lastUpdated: Date.now(), + // streamLifecycleOpened intentionally falsy so the lifecycle PUT is + // skipped — keeps the test free of extra HTTP mocks while still + // exercising the rest of the cleanup path. + streamLifecycleOpened: false, + config: { clientId: 'id', clientSecret: 'sec' } as any, + }; + registerCardRun('track_finalize', { + accountId: 'main', + sessionKey: 's1', + agentId: 'a1', + }); + const record = resolveCardRun('track_finalize'); + // Attach the card to the registry record so the helper finds it. + if (record) { + record.card = card; + } + // Plausible mid-life state: still has pendingApprovalId AND deferredFinalize. + markCardRunPendingApproval('track_finalize', 'approval-x'); + markCardRunDeferredFinalize('track_finalize'); + // Avoid an unused-binding warning from attachCardRunController import. + void attachCardRunController; + + await completeDeferredAICardFinalize('track_finalize'); + + expect(card.state).toBe(AICardStatus.FINISHED); + // Record removed from registry — no longer resolvable. + expect(resolveCardRun('track_finalize')).toBeNull(); + }); + + it('is a no-op when the run has no deferredFinalize flag set', async () => { + const { completeDeferredAICardFinalize } = await import('../../src/card-service'); + const { registerCardRun, resolveCardRun } = await import( + '../../src/card/card-run-registry' + ); + + registerCardRun('track_noop', { + accountId: 'main', + sessionKey: 's1', + agentId: 'a1', + }); + await completeDeferredAICardFinalize('track_noop'); + + // Record still present (we did not remove it because nothing was deferred). + expect(resolveCardRun('track_noop')).not.toBeNull(); + }); }); From 0def9525774ff5af26843381928ad02d3ba5d805 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Wed, 20 May 2026 00:37:12 +0800 Subject: [PATCH 43/44] fix(approval): classify "unknown or expired approval id" as not-found MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-device validation showed a follow-up bug: when the user clicked an approval button after upstream had already moved past the approval (askFallback resolved it, expired, etc.), the gateway returned INVALID_REQUEST with message "unknown or expired approval id". Our resolver only matched gatewayCode === "APPROVAL_NOT_FOUND" so it fell through to gateway-error, and the operator saw a misleading "审批暂时处理失败,请稍后重试" hint that suggested transient retry. Replace the gatewayCode equality check with the upstream isApprovalNotFoundError helper, which already canonicalises all three not-found surfaces: - gatewayCode=APPROVAL_NOT_FOUND - gatewayCode=INVALID_REQUEST with details.reason=APPROVAL_NOT_FOUND - gatewayCode=INVALID_REQUEST with message "unknown or expired approval id" The hint flow then runs applyExpiredPatch (clears buttons + completes the deferred finalize) instead of telling the operator to retry. Tests cover both newly recognised surfaces plus the legacy code path. --- src/approval/approval-resolver.ts | 9 ++++++++- tests/unit/approval-resolver.test.ts | 29 ++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/approval/approval-resolver.ts b/src/approval/approval-resolver.ts index 107d254b..084fed78 100644 --- a/src/approval/approval-resolver.ts +++ b/src/approval/approval-resolver.ts @@ -1,5 +1,6 @@ import { resolveApprovalOverGateway } from "openclaw/plugin-sdk/approval-gateway-runtime"; import type { OpenClawConfig } from "openclaw/plugin-sdk/core"; +import { isApprovalNotFoundError } from "openclaw/plugin-sdk/error-runtime"; import type { ApprovalDecision, Logger } from "../types"; import { isExecAuthorizedSender, isPluginAuthorizedSender } from "./approval-config"; @@ -114,7 +115,13 @@ export async function resolveApproval(input: ResolveApprovalInput): Promise { expect(result).toEqual(expect.objectContaining({ ok: false, reason: "gateway-error" })); }); + it("maps INVALID_REQUEST + details.reason=APPROVAL_NOT_FOUND to not-found", async () => { + mockGateway.mockRejectedValue( + Object.assign(new Error("approval not found"), { + gatewayCode: "INVALID_REQUEST", + details: { reason: "APPROVAL_NOT_FOUND" }, + }), + ); + + const result = await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + + expect(result).toEqual(expect.objectContaining({ ok: false, reason: "not-found" })); + }); + + it('maps INVALID_REQUEST + "unknown or expired approval id" message to not-found', async () => { + // Real-device repro: clicking an approval button after upstream had already + // resolved the approval (e.g. via askFallback) returned INVALID_REQUEST + // with this exact message; previously misclassified as gateway-error and + // shown to the operator as "稍后重试". + mockGateway.mockRejectedValue( + Object.assign(new Error("unknown or expired approval id"), { + gatewayCode: "INVALID_REQUEST", + }), + ); + + const result = await resolveApproval({ ...base, approvalId: "abc", decision: "allow-once" }); + + expect(result).toEqual(expect.objectContaining({ ok: false, reason: "not-found" })); + }); + it("maps arbitrary errors to gateway-error", async () => { mockGateway.mockRejectedValue(new Error("network down")); From 155d5eea405cb907feac4e08b426927bcd36b557 Mon Sep 17 00:00:00 2001 From: zhumin <605614935@qq.com> Date: Wed, 20 May 2026 01:00:04 +0800 Subject: [PATCH 44/44] feat(approval): replace card body with friendly approval prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Real-device validation showed that even after the native button group attaches to the card, the body still rendered the upstream tool-result text — "Approval required. Run: ```txt /approve allow-once``` …" — which duplicates the button labels in technical markdown and reads poorly when the buttons render natively. When deliverPending takes the card route, also write a concise body that names the command awaiting approval and points at the buttons: 🔒 该命令需要您的审批 ``` ls /root ``` 工作目录:/root/.openclaw/workspace 过期时间:30 分钟 _请通过下方按钮批准或拒绝_ Implementation: - approval-markdown-render: new buildExecApprovalCardBody and buildPluginApprovalCardBody helpers. They reuse the same payload source as the markdown-fallback builders but drop the decision block (the buttons render those decisions). - approval-native-runtime: presentation.buildPendingPayload returns the new cardBodyMarkdown alongside markdownText. The DingTalk pending payload type carries both, so the card route overrides the body while the markdown fallback keeps its existing /approve command listing. - approval-card-patcher.applyPendingPatch: optional cardBodyMarkdown argument; when present, PUT blockList (single type:0 markdown block), content (streaming key), and copy_content alongside the show_approve_btns / approveId / hasAction triple. Tests: extend approval-card-patcher and approval-native-runtime suites for the new pathway; verify body keys are omitted when cardBodyMarkdown is not supplied. --- src/approval/approval-card-patcher.ts | 35 +++++++++++++++++- src/approval/approval-markdown-render.ts | 43 ++++++++++++++++++++++ src/approval/approval-native-runtime.ts | 21 +++++++++-- tests/unit/approval-card-patcher.test.ts | 27 ++++++++++++++ tests/unit/approval-native-runtime.test.ts | 22 +++++++++-- 5 files changed, 139 insertions(+), 9 deletions(-) diff --git a/src/approval/approval-card-patcher.ts b/src/approval/approval-card-patcher.ts index e126b6dd..4dc1f649 100644 --- a/src/approval/approval-card-patcher.ts +++ b/src/approval/approval-card-patcher.ts @@ -1,5 +1,6 @@ import { updateCardVariables } from "../card-callback-service"; import { completeDeferredAICardFinalize } from "../card-service"; +import { DINGTALK_CARD_TEMPLATE } from "../card/card-template"; import { buildApprovalClearedCardParams, buildApprovalPendingCardParams, @@ -9,13 +10,38 @@ import { markCardRunPendingApproval, resolveCardRun, } from "../card/card-run-registry"; -import type { DingTalkConfig } from "../types"; +import type { CardBlock, DingTalkConfig } from "../types"; + +/** + * Build the cardParamMap diff used by applyPendingPatch. The required keys + * come from buildApprovalPendingCardParams (show_approve_btns / approveId / + * hasAction). When the caller provides a cardBodyMarkdown we additionally + * overwrite the card body so the user sees a friendly approval prompt rather + * than the upstream tool-result text (e.g. "Approval required. Run: /approve + * allow-once …") that the agent reply pipeline streamed in. + */ +function buildPendingCardPutVariables( + approvalId: string, + cardBodyMarkdown?: string, +): Record { + const params: Record = { + ...buildApprovalPendingCardParams(approvalId), + }; + if (cardBodyMarkdown) { + const block: CardBlock = { type: 0, markdown: cardBodyMarkdown }; + params[DINGTALK_CARD_TEMPLATE.blockListKey] = JSON.stringify([block]); + params[DINGTALK_CARD_TEMPLATE.streamingKey] = cardBodyMarkdown; + params[DINGTALK_CARD_TEMPLATE.copyContentKey] = cardBodyMarkdown; + } + return params; +} export async function applyPendingPatch( outTrackId: string, approvalId: string, token: string, config?: Pick, + cardBodyMarkdown?: string, ): Promise { // Pre-mark the run so a concurrent commitAICardBlocks that fires while the // pending PUT is in flight defers the finalize (it keys the decision on @@ -23,7 +49,12 @@ export async function applyPendingPatch( // already deferred, rescue the card by terminalizing via applyExpiredPatch. markCardRunPendingApproval(outTrackId, approvalId); try { - await updateCardVariables(outTrackId, buildApprovalPendingCardParams(approvalId), token, config); + await updateCardVariables( + outTrackId, + buildPendingCardPutVariables(approvalId, cardBodyMarkdown), + token, + config, + ); } catch (err) { clearCardRunPendingApproval(outTrackId); if (resolveCardRun(outTrackId)?.deferredFinalize) { diff --git a/src/approval/approval-markdown-render.ts b/src/approval/approval-markdown-render.ts index 6b27cca3..d98c19c3 100644 --- a/src/approval/approval-markdown-render.ts +++ b/src/approval/approval-markdown-render.ts @@ -58,6 +58,49 @@ export function buildExecApprovalMarkdown(request: ExecApprovalRequest, nowMs: n ].join("\n"); } +/** + * Friendly approval card body used when the v3 template renders the + * approve_btns ButtonGroup natively. Skips the /approve command listing + * (the buttons render those decisions inline) so the card body stays + * compact and human-readable. + */ +export function buildExecApprovalCardBody(request: ExecApprovalRequest, nowMs: number): string { + const payload = request.request; + const command = payload.commandPreview || payload.command || "(no command)"; + const cwdLine = payload.cwd ? `\n**工作目录**:\`${payload.cwd}\`` : ""; + const expireLine = formatExpireHint(request.expiresAtMs, nowMs); + return [ + "🔒 **该命令需要您的审批**", + "", + "```", + command, + "```", + cwdLine ? cwdLine.replace(/^\n/, "") : "", + expireLine ? expireLine.replace(/^\n/, "") : "", + "", + "_请通过下方按钮批准或拒绝_", + ] + .filter((line) => line !== "") + .join("\n"); +} + +export function buildPluginApprovalCardBody(request: PluginApprovalRequest, nowMs: number): string { + const payload = request.request; + const tool = payload.toolName || "(unknown tool)"; + const expireLine = formatExpireHint(request.expiresAtMs, nowMs); + return [ + "🔒 **插件操作需要您的审批**", + "", + `**工具**:\`${tool}\``, + payload.description ? payload.description : "", + expireLine ? expireLine.replace(/^\n/, "") : "", + "", + "_请通过下方按钮批准或拒绝_", + ] + .filter((line) => line !== "") + .join("\n"); +} + export function buildPluginApprovalMarkdown(request: PluginApprovalRequest, nowMs: number): string { const payload = request.request; const allowed = normalizePluginAllowedDecisions(payload.allowedDecisions); diff --git a/src/approval/approval-native-runtime.ts b/src/approval/approval-native-runtime.ts index 2547570c..1ef31c80 100644 --- a/src/approval/approval-native-runtime.ts +++ b/src/approval/approval-native-runtime.ts @@ -18,14 +18,21 @@ import { } from "./approval-card-patcher"; import { getExecApprovalsConfig, listExecApprovers } from "./approval-config"; import { + buildExecApprovalCardBody, buildExecApprovalMarkdown, + buildPluginApprovalCardBody, buildPluginApprovalMarkdown, } from "./approval-markdown-render"; import { normalizeApprovalTargetTo } from "./approval-target-resolver"; export type DingTalkApprovalPendingPayload = { approvalId: string; + /** Markdown body used for the fallback markdown route (sendProactive). */ markdownText: string; + /** Friendly card-body markdown used for the v3-card route — overrides the + * upstream tool-result text the agent reply pipeline streamed in so the + * user sees a concise approval prompt above the native button group. */ + cardBodyMarkdown: string; }; export type DingTalkApprovalPreparedTarget = { @@ -126,12 +133,17 @@ export function createDingTalkApprovalNativeRuntime(): ChannelApprovalNativeRunt getLogger()?.info?.( `[DingTalk][Approval][buildPendingPayload] approval=${request.id} kind=${approvalKind}`, ); + if (approvalKind === "plugin") { + return { + approvalId: request.id, + markdownText: buildPluginApprovalMarkdown(request as never, nowMs), + cardBodyMarkdown: buildPluginApprovalCardBody(request as never, nowMs), + }; + } return { approvalId: request.id, - markdownText: - approvalKind === "plugin" - ? buildPluginApprovalMarkdown(request as never, nowMs) - : buildExecApprovalMarkdown(request as never, nowMs), + markdownText: buildExecApprovalMarkdown(request as never, nowMs), + cardBodyMarkdown: buildExecApprovalCardBody(request as never, nowMs), }; }, buildResolvedResult: ({ resolved }) => ({ @@ -193,6 +205,7 @@ export function createDingTalkApprovalNativeRuntime(): ChannelApprovalNativeRunt pendingPayload.approvalId, token, dtConfig, + pendingPayload.cardBodyMarkdown, ); return { mode: "card", diff --git a/tests/unit/approval-card-patcher.test.ts b/tests/unit/approval-card-patcher.test.ts index 81c18ed8..83b43c98 100644 --- a/tests/unit/approval-card-patcher.test.ts +++ b/tests/unit/approval-card-patcher.test.ts @@ -40,6 +40,33 @@ describe("approval-card-patcher", () => { mockResolveRun.mockReset().mockReturnValue(null); }); + it("PUTs cardBodyMarkdown into blockList/content/copy_content when provided (UX body override)", async () => { + await applyPendingPatch("ot1", "abc123", "tok", {}, "🔒 friendly card body"); + + expect(mockUpdate).toHaveBeenCalledWith( + "ot1", + expect.objectContaining({ + show_approve_btns: "true", + approveId: "abc123", + hasAction: "false", + content: "🔒 friendly card body", + copy_content: "🔒 friendly card body", + blockList: JSON.stringify([{ type: 0, markdown: "🔒 friendly card body" }]), + }), + "tok", + {}, + ); + }); + + it("omits body keys when cardBodyMarkdown is not provided", async () => { + await applyPendingPatch("ot1", "abc123", "tok", {}); + + const [, params] = mockUpdate.mock.calls[0] as [string, Record]; + expect(params).not.toHaveProperty("content"); + expect(params).not.toHaveProperty("blockList"); + expect(params).not.toHaveProperty("copy_content"); + }); + it("pre-marks the run pending before issuing the PUT (CR-2 race fix)", async () => { const callOrder: string[] = []; mockMark.mockImplementation(() => { diff --git a/tests/unit/approval-native-runtime.test.ts b/tests/unit/approval-native-runtime.test.ts index c696f5da..f1b1dbc3 100644 --- a/tests/unit/approval-native-runtime.test.ts +++ b/tests/unit/approval-native-runtime.test.ts @@ -15,6 +15,8 @@ vi.mock("../../src/approval/approval-card-patcher", () => ({ vi.mock("../../src/approval/approval-markdown-render", () => ({ buildExecApprovalMarkdown: vi.fn(() => "exec-md"), buildPluginApprovalMarkdown: vi.fn(() => "plugin-md"), + buildExecApprovalCardBody: vi.fn(() => "exec-card-body"), + buildPluginApprovalCardBody: vi.fn(() => "plugin-card-body"), })); vi.mock("../../src/card/card-run-registry", () => ({ resolveCardRun: vi.fn(), @@ -113,7 +115,11 @@ describe("approval-native-runtime", () => { nowMs: Date.now(), view: {} as never, }), - )).resolves.toEqual({ approvalId: "abc123", markdownText: "exec-md" }); + )).resolves.toEqual({ + approvalId: "abc123", + markdownText: "exec-md", + cardBodyMarkdown: "exec-card-body", + }); }); it("prepareTarget returns the required { dedupeKey, target } wrapper for card route", () => { @@ -183,11 +189,21 @@ describe("approval-native-runtime", () => { }, request: request(), approvalKind: "exec", - pendingPayload: { approvalId: "abc123", markdownText: "md" }, + pendingPayload: { + approvalId: "abc123", + markdownText: "md", + cardBodyMarkdown: "card-body", + }, view: {} as never, } as never); - expect(mockPending).toHaveBeenCalledWith("ot1", "abc123", "tok", expect.objectContaining({ clientId: "x" })); + expect(mockPending).toHaveBeenCalledWith( + "ot1", + "abc123", + "tok", + expect.objectContaining({ clientId: "x" }), + "card-body", + ); expect(entry).toEqual({ mode: "card", approvalId: "abc123", accountId: "default", outTrackId: "ot1" }); });