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
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/README.md b/README.md
index 73639302..d4610978 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
# DingTalk Channel for OpenClaw
-
+
@@ -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/assets/card-template-v3.json b/docs/assets/card-template-v3.json
new file mode 100644
index 00000000..e88a171f
--- /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\"},{\"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\",\"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
new file mode 100644
index 00000000..5d15de16
--- /dev/null
+++ b/docs/features/2026-05-18-gap-01-approval-native-design.html
@@ -0,0 +1,2350 @@
+
+
+
+
+
+ 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 触发审批的钉钉会话(群或私聊)以最自然的方式呈现 approval——AI Card 流式模式下把 3 按钮挂到 agent 正在说话的那张卡片上;markdown 模式或无 active card 时发独立 markdown 消息含 /approve 命令模板。approver 点按钮或敲命令均可完成。
+
+
+
+
本设计的核心原则
+
+ - 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 一致
+ - 钉钉特性最大化(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 + approveId 三个变量;按钮内置无需 channel 构造);markdown 路径无需模板。把 v3 模板发布并替换默认 templateId(§10 阶段 0),然后把 approver staffId 列表写进 channels.dingtalk.execApprovals.approvers 即可启用
+
+
+
+ 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 走专用 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)
+
+
+ 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"(无后缀)。
+ 唯一名时无后缀。
+
+
+ 编码(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" },
+// { 参数名: "approveId", 参数类型: 变量, 参数值: "approveId" } // ← D24 绑定到同名变量
+// ]
+//
+// 按钮 2(总是允许):
+// ActionId: "allow-always"
+// 回传参数: [
+// { 参数名: "action", 参数类型: 静态值, 参数值: "allow-always" },
+// { 参数名: "approveId", 参数类型: 变量, 参数值: "approveId" }
+// ]
+//
+// 按钮 3(拒绝):
+// ActionId: "deny"
+// 回传参数: [
+// { 参数名: "action", 参数类型: 静态值, 参数值: "deny" },
+// { 参数名: "approveId", 参数类型: 变量, 参数值: "approveId" }
+// ]
+//
+// 显示控制(条件计算):show_approve_btns 的值为 true → 整组可见
+
+// 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(allow-once / allow-always / deny)和静态参数 action 都在 template 里固定,
+// channel 不传按钮定义
+
+ 回调实测形态(v3.7 用户实配 schema 确认)
+ // 用户点"允许一次"按钮,平台 push 的 callback data.content:
+{
+ "cardPrivateData": {
+ "actionIds": ["allow-once"], // ← 唯一命名,无 index 后缀
+ "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",
+ ...
+}
+// D24 v3.6+ 解码主链路:直接读 cardPrivateData.params.approveId
+// Fallback:仅当 callback 不带 approveId 时反查 resolveCardRun(outTrackId).pendingApprovalId
+
+ 解码(callback 入口)
+ // 第一步:扩展后的 analyzeCardCallback 把 cardPrivateData 整体放进 analysis
+const cpd = analysis.cardPrivateData; // { actionIds, params }
+
+// 第二步:parseApprovalFromCardPrivateData(cpd)
+// 主链路: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 直接取;
+// registry 反查仅作为 fallback(应对老卡片 / 平台异常)
+let approvalId: string | null =
+ (typeof cpd.params?.approveId === "string" && cpd.params.approveId) || null;
+if (!approvalId) {
+ // 兜底:v3 模板未带 approveId,或 callback 字段丢失 → 查 registry
+ const cardRun = resolveCardRun(analysis.outTrackId);
+ approvalId = cardRun?.pendingApprovalId ?? null;
+}
+if (!approvalId) {
+ // 主链路 + fallback 都没拿到 → 视为"已处理或已过期"
+ return { approvalId: null, decision, reason: "no-pending-approval" };
+}
+return { approvalId, decision };
+
+ 回写上游
+ // 用 SDK 公开 API(v2026.4.7+),通过 approval-resolver 单点收敛(D20):
+await approvalResolver.resolveApproval({
+ cfg, accountId,
+ approvalId, decision,
+ senderId: analysis.userId, // staffId
+ log,
+})
+// resolver 内部按 D21 推导 resolveMethod + allowPluginFallback,
+// 然后调 resolveApprovalOverGateway。channel 端零重复
+
+
+ 命令路径与按钮路径的关系
+ 两条路径都最终调同一个 approval-resolver.resolveApproval(D20 单点收敛)。差异仅在 decode 阶段:按钮走 cardPrivateData + outTrackId 反查;命令走文本 regex。用户感知一致。
+
+
+
+
+
+ 2. 已确认的决策清单
+ 本节是设计的 single source of truth,下文所有 section 的实现细节都基于这 10 条决策。任何变更需要回到这里更新。
+
+
+ | # | 决策点 | 选定方案 | 关联 peer |
+
+
+ | D1 |
+ 实现范围 |
+ Native runtime v1:4 个 sub-adapter(availability / presentation / transport / observe),interactions 推迟到 v2。详见 §5 |
+ Discord / Slack / Telegram |
+
+
+ | 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.4 修订:上游 openclaw/src/auto-reply/reply/commands-approve.ts:19-30 实际共 10 个:
allow / once / allow-once / allowonce → allow-once;always / allow-always / allowalways → allow-always;deny / reject / block → deny(v3.1 漏列了 allowonce 和 allowalways 两个无连字符形式)。channel 端 regex 必须完整支持这 10 个 alias × 两种顺序 = 20 个合法形式。若上游新增 alias / 顺序需同步更新(§11.2 风险表登记) |
+ 对齐 PR #489 揭示的运行时约束 |
+
+
+ | D3 |
+ Approval ID 形式 |
+ 全 ID 不短化(DingTalk 无 byte 限制;Telegram 短到 8 字符仅因 64 字节硬限制) |
+ Discord / Slack |
+
+
+ | D4 |
+ 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 |
+ 自批准(self-approval) |
+ 允许(clicker 在 approver 名单里即可,不查 originator vs clicker) |
+ 三家完全一致 |
+
+
+ | D6 |
+ Origin == DM 去重 |
+ 用 transport.prepareTarget 返回的 dedupeKey = accountId:target.to |
+ SDK 标准机制 |
+
+
+ | D7 |
+ Approver schema |
+ 仅 staffId(含 dingtalk:/dd:/ding: 前缀剥离);多账号 override;fallback 到 commands.ownerAllowFrom;enabled: auto |
+ 三家完全一致 |
+
+
+ | D8 |
+ Approval 类别 |
+ exec + plugin 一次到位(eventKinds: ["exec", "plugin"]) |
+ 三家完全一致 |
+
+
+ | D9 |
+ 卡片模板(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:6 的 BUILTIN_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 |
+ 渲染策略(v3.3 重写) |
+ 不读 messageType 配置——按 card-run-registry 实际状态 分两路由:
+ • card 路径:sessionKey 命中 registry 且 record.card?.state ∈ {PROCESSING, INPUTING} → 在 record.outTrackId 上 PUT 注入 approval 按钮(实施细节:CardRunRecord.card 是 AICardInstance | 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 |
+
+
+ | D11 |
+ TTL 归属 |
+ 上游 approval store 管 expiresAtMs;channel 端不跑自己的 timer,跟随上游事件 |
+ SDK 标准 |
+
+
+ | D12 |
+ 重启恢复 |
+ v1 不主动 rebind;遗留卡片的按钮点击触发"过期/失效"显式降级提示 |
+ —(v1 范围) |
+
+
+ | D13 |
+ 停机取消(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 |
+ 终态展示(v3.3 修订:原 agent card patch) |
+ 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 模式参考 |
+
+
+ | D15 |
+ 按钮 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" 触发的消歧行为。
+
+ approvalId 通过 approveId 变量带回(v3.6 D24 主链路):v3 模板的三按钮 params 都绑定到 approveId 变量,channel 端 patch 时 PUT 设值,callback 中 cardPrivateData.params.approveId 自动带回。D24 的 registry pendingApprovalId 仅作 fallback(应对老卡片 / 平台异常) |
+ 对齐用户实测 + OpenClaw 命名习惯 |
+
+
+ | 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 |
+ 前置依赖(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-sdk 在 node_modules/openclaw/dist 之上(开发期),用于 monorepo / linked checkout 场景;
+ 4) PR #480 已合并(CardBtn[] + sendCardRequest 已在当前 main 可用,无需阻塞);
+ 5) PR #489 基于 4 月旧 main 且 CONFLICTING,不可直接合并——本设计是基于当前 main 与上游 openclaw 的重新整理实现 |
+ 对齐 PR #489 + 当前 main 实际工程约束 |
+
+
+ | D18 |
+ 本地 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 |
+ 实施基调(v3 新增) |
+ 本设计是 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 经验 |
+
+
+ | D21 |
+ approval kind 推导规则(v3.2 修订 D20 子规则) |
+ 3 段判断(按 Slack/Telegram 模式):
+ (1) approvalId.startsWith("plugin:") → { resolveMethod: "plugin" }
+ (2) 无前缀 + exec 与 plugin 都授权 → { allowPluginFallback: true }(不传 resolveMethod 即默认 exec;让 resolveApprovalOverGateway 在 exec store 找不到时回退尝试 plugin store)
+ (3) 无前缀 + 仅 plugin 授权 → { resolveMethod: "plugin" }
+ (4) 无前缀 + 仅 exec 授权 → { allowPluginFallback: false }(不传 resolveMethod,默认 exec)
+ (5) 都未授权 → 拒绝(私聊提示 + 不调 gateway) |
+ 对齐上游 Slack/Telegram 当前做法 |
+
+
+ | D22 |
+ agent-card-coalesce(v3.3 新增核心) |
+ card 路径下,approval 按钮挂在 原 agent reply card 而非新建独立卡片:
+ (1) transport.prepareTarget 内部调 approval-card-locator.findActiveAgentCard(request),按 request.sessionKey 查 card-run-registry;
+ (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 流控制) |
+ v3.3 用户拍板 |
+
+
+ | D23 |
+ 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.6 重构:模板内置为主,registry 兜底) |
+ 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", 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。
+
+ 原因(v3.5 D24 内存反查的脆弱性):进程重启会丢、多 worker 部署 callback 可能路由到另一个 worker(src/card/card-run-registry.ts:1-9 多进程约束说明)、TTL sweep 也会清。v3.6 把主链路从"内存映射"改成"卡片自带",registry 仅作降级保护。
+
+ 一卡仍只挂 1 个 approval(同 v3.5:agent 串行 approval 是天然约束) |
+ v3.6 用户拍板(模板已就位,再加一个变量成本低;换来主链路抗重启 / 抗多 worker) |
+
+
+
+
+
+
本设计的版本演进
+
+ - 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 派发边界条目
+ - 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 自动保留。真正会丢字段的是 resolveDingTalkAccount(src/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 条目;
+ (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);
+ (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 已配 ✓,已发布 ✓ 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 字段超集向后兼容,并存会让边界不清;
+ (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;
+ (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 消歧;唯一名时原样回);
+ (3) 按钮在 template 内置("按钮组来源: 指定"),channel 不再构造 CardBtn[],只 toggle show_approve_btns 可见性;
+ (4) v3.5 阶段回传参数曾仅有 params.action,但 v3.6/v3.7 已加 approveId 变量绑定(D24 主链路);早期 v3.5 假设的"必须 outTrackId 反查"已被 v3.6 重构掉,registry 反查只剩 fallback;
+ (5) 新增 D24:approvalId 反查机制——给 CardRunRecord 加 pendingApprovalId?: 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 模板未发布等过时表述统一清理;
+ (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 漏列 allowonce 和 allowalways 两个无连字符形式);测试 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 降级不反映);
+ (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;
+ (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 名单)保持不变
+
+
+
+
+
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) |
+
+
+
+
+
+
+
+ 3. 架构与模块布局
+
+ 3.1 上下游分工
+ ┌──────────────────────────── upstream OpenClaw ────────────────────────────┐
+│ │
+│ exec-approval / plugin-approval store ─┐ (createApprovalRequest) │
+│ │ │
+│ approval-handler-runtime ────────────► nativeRuntime v1: 4 子 adapter │
+│ (availability/presentation/ │
+│ transport/observe; interactions v2)│
+│ (src/infra/approval-handler-runtime.ts) ▲ (DingTalk channel 实现) │
+│ │ │
+│ resolve gateway ◄───────────────────────┤ 调上游回写 │
+│ ├─ exec.approval.resolve { id, decision } │
+│ └─ plugin.approval.resolve { id, decision } │
+│ │
+│ resolveApprovalOverGateway({approvalId,decision,senderId,...}) │
+│ 公开 API(v2026.4.7+):按钮点击 + /approve 命令两条路径都调它 │
+│ 内部按 approval kind dispatch 到 exec/plugin.approval.resolve │
+│ │
+└───────────────────────────────────┬────────────────────────────────────────┘
+ │ adapter 实现
+ ▼
+┌──────────────────── openclaw-channel-dingtalk (本仓库) ────────────────────┐
+│ │
+│ src/approval/ ── 新增 domain 目录(v3.3:10 个文件,含 card-locator + 双路由)
+│ ├─ approval-capability.ts ApprovalCapability 单例装配 │
+│ ├─ approval-native-runtime.ts 4 子 adapter(availability/ │
+│ │ presentation/transport/observe) │
+│ ├─ approval-card-locator.ts ★ v3.3 新增:按 sessionKey 查 │
+│ │ card-run-registry,决定 route │
+│ ├─ approval-card-patcher.ts ★ v3.3 替代 card-template+render: │
+│ │ 在原 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 │
+│ ├─ 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 单一收敛点
+│ 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_DINGTALK_CARD_TEMPLATE_ID │
+│ │ 旧→新(v2→v3,参 §10 阶段 0) │
+│ └─ src/types.ts 加 ApprovalEntry / Decision │
+│ │
+│ 资产 │
+│ ├─ 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 用户配置指南 │
+│ │
+└────────────────────────────────────────────────────────────────────────────┘
+
+ 3.2 模块单一职责表
+
+ | 模块 | 单一职责 | 主要依赖 | 预计行数 |
+
+
+ approval-capability.ts |
+ 用 createApproverRestrictedNativeApprovalCapability 工厂把所有零件组装成单例 ChannelApprovalCapability |
+ SDK 工厂 + 其它 8 个 approval-* 模块 |
+ ~80 |
+
+
+ approval-native-runtime.ts |
+ 实现 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, approvalId }):按 sessionKey 查 card-run-registry,仅在 record 存在且 record.card?.state ∈ {PROCESSING, INPUTING} 时返回 { outTrackId, sessionKey };否则返回 null(caller 走 markdown 路径)。若同一卡片已有不同 pendingApprovalId,也返回 null,让并发审批降级到 markdown;同一 approvalId 重试保持 card 路径幂等。注意:state 在 record.card.state(src/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 / resolveCardRunByOwner(src/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 ★ 三个 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 并提示稍后重试)。
+
+ v1 已知限制:v3 模板没专门的 approval 终态文字位(statusLine 被 taskInfo 占用,contentKey 与 stream 写冲突)。v1 终态仅靠"按钮消失"——用户感知按钮没了就是已处理。v1.x 升级路径:让维护者在模板加 approval_status 变量后再 PUT 写入。
+
+ 模块内无按钮数据结构(所有按钮定义都在模板)。仅依赖 updateCardVariables(outTrackId, { ...variables }, token) API。 |
+ card-callback-service.updateCardVariables; card-run-registry pendingApprovalId API(v3.5 新增) |
+ ~80 |
+
+
+ approval-markdown-render.ts ★ v3.3 替代 fallback-render |
+ markdown 路径主路径(不再叫 fallback)。导出 buildExecApprovalMarkdown(request, nowMs): string 与 buildPluginApprovalMarkdown(request, nowMs): string:构造含 approval id、命令 preview / tool 描述、过期 hint、3 个 /approve <id> <decision> 复制即用模板的 markdown 文本 |
+ approval-config(读 expire hint 等) |
+ ~90 |
+
+
+ approval-target-resolver.ts |
+ v1:仅 resolveOriginTarget(从 turnSourceTo 还原 DingTalk target,保留 user:/group: 前缀)。v2 future:增加 resolveApproverDmTargets |
+ 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" | "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 |
+
+
+ approval-command-parser.ts ★ v3.2 新增 |
+ 纯解析(无副作用)。导出 parseApproveCommand(text: string): { approvalId, decision } | null:支持 /approve <id> <decision> 与 /approve <decision> <id> 两种顺序 + 10 个 alias(openclaw/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 |
+
+
+ approval-callback-handler.ts |
+ 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 |
+
+
+ approval-config.ts |
+ 纯读 helper:getExecApprovalsConfig / listExecApprovers / isExecAuthorizedSender / isPluginAuthorizedSender(v1 默认同 exec)/ resolveNativeDeliveryMode(v1 永远返回 "channel") |
+ config 模块 |
+ ~110 |
+
+
+ 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.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.3 删除:approval-card-template.ts(D9 不再新建模板,复用 AI Card v2)。
+ v3.3 合计新增约 ~900 行业务代码(比 v3.2 的 ~970 行少 ~70,因为没有完整 card-render,patch 比 render 简单);测试代码预计 ~1800 行。
+
+
+ 3.3 与现有代码的接触面
+
+ | 文件 | 改动 | 风险 |
+
+
+ | src/channel.ts:22-127 |
+ plugin 对象新增 approvalCapability 字段(1 行 import + 1 行赋值) |
+ 极低 |
+
+
+ | src/config-schema.ts |
+ 新增 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: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 新增改动面) |
+ 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) |
+
+
+ src/card/card-run-registry.ts (v3.4 + v3.5 新增改动面) |
+ 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;
+ • 新增 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 类型;v3.10 新增 ExecApprovalsConfig(接 §4.2 schema)。 无按钮数据结构——按钮 actionId/params 全在 v3 模板内置(参 §1.X 单一事实表),channel 端只 PUT cardParamMap 三个字符串变量(show_approve_btns / hasAction / approveId) |
+ 极低 |
+
+
+ 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;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 新增) |
+ 从当前 >=<current-version> bump 到 >=2026.4.7 |
+ BREAKING(用户需升级上游 openclaw) |
+
+
+
+
+
+
明确不修改的文件(向后兼容保证)
+
+ - src/card/card-action-handler.ts(btn_stop 不动)
+ - src/feedback-learning-service.ts(与 approval 完全不交叉)
+ - 所有
src/reply-strategy-*(v3.3 修订后仍 不修改 reply-strategy——但请注意 approval 与 reply-strategy 不再是完全无关:approval card 路径会通过 card-run-registry 找到 reply-strategy-card 创建的 active card,并对它做字段级 patch。reply-strategy 文件本身仍不动)
+
+
+
+
+
+
+ 4. Capability 装配
+
+ 4.1 用 SDK 工厂的输入清单
+ 调 createApproverRestrictedNativeApprovalCapability(openclaw/src/plugin-sdk/approval-delivery-helpers.ts:30-261)一次组装出完整 ChannelApprovalCapability。下面是 DingTalk 端要传的 16 个参数:
+
+
+ | 参数 | 必填 | DingTalk 实现 |
+
+ channel | ✓ | "dingtalk" |
+ channelLabel | ✓ | "DingTalk" |
+ listAccountIds | ✓ | 复用 dingtalkPlugin.config.listAccountIds |
+ hasApprovers | ✓ | listExecApprovers({ cfg, accountId }).length > 0 |
+ isExecAuthorizedSender | ✓ | isExecAuthorizedSender({ cfg, accountId, senderId }),clicker staffId 在名单中 |
+ isPluginAuthorizedSender | 可选 | 默认 = isExecAuthorizedSender(同一份 approver 名单管两类) |
+ isNativeDeliveryEnabled | ✓ | 检查 execApprovals.enabled !== false && hasApprovers(auto 视为 true) |
+ resolveNativeDeliveryMode | ✓ | v1:永远返回 "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: 前缀) |
+ resolveApproverDmTargets | v1 不实现 | 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 |
+
+
+
+ 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: 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 名单
+
+# 全局 fallback(peer 惯例;与 dingtalk 无关,由上游统一处理)
+commands:
+ ownerAllowFrom: ["staff999"]
+
+
+
配置语义细节
+
+ enabled: auto = approvers 非空则启用;显式 false 优先级最高(即使有 approvers 也禁用)
+ - 多账号 override 完全替换名单(不与 channel-level 合并),与现有
allowFrom 的语义保持一致
+ - fallback 优先级:account-level
approvers → channel-level approvers → commands.ownerAllowFrom → 空
+ - 所有 staffId 在写入时 normalize:
raw.replace(/^(dingtalk|dd|ding):/i, "")(与 src/channel.ts:86 dmPolicy 完全一致)
+
+
+
+
+
+
+ 5. Sub-Adapter 详解(v1 实现 4 个)
+ 上游 ChannelApprovalNativeRuntimeAdapter(openclaw/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
+
+ | 方法 | DingTalk 实现 |
+
+
+ isConfigured({ cfg, accountId }) |
+ 返回 isNativeDeliveryEnabled({ cfg, accountId })——即 enabled !== false && hasApprovers |
+
+
+ shouldHandle({ cfg, accountId, request }) |
+ 四连判(v1 origin-only 严格判定):
+ (1) isConfigured 为 true;
+ (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 兜底 |
+
+
+
+
+ 5.2 presentation
+
+ | 方法 | DingTalk 实现 |
+
+
+ buildPendingPayload({ request, nowMs, view }) |
+ 返回 { 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 }) |
+ 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 }) |
+ v3.9 修订(清理 v3.3 残留):返回 { kind: "update", payload: { phase: "expired" } }。同样由 transport.updateEntry 调 patcher.applyExpiredPatch 时落地具体字段(show_approve_btns:"false" + approveId:"" + hasAction 视 card 状态恢复 stop)。
+ 不再返回 buildExpiredCardParamMap(...) / statusFooter |
+
+
+
+
+ 5.3 transport
+
+ | 方法 | DingTalk 实现 |
+
+
+ prepareTarget({ plannedTarget, request, view, pendingPayload }) |
+ 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 }) |
+ v3.4 修订:按 preparedTarget.route 分两条路径 + card 失败时降级 markdown。
+ route="card":
+ (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(不重发避免双消息)。
+
route="markdown":
+ (1) 调 approval-markdown-render 构造 markdown 文本(含 /approve 命令模板);
+ (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 自触发该钩子 |
+
+
+ updateEntry({ cfg, accountId, entry, payload, phase }) |
+ v3.3 修订:按 entry.mode 分支。
+ 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) |
+
+
+ deleteEntry({ cfg, accountId, entry, phase }) |
+ not used(D14:永远 update,从不 delete)。v1 不实现(SDK 允许 deleteEntry 缺省) |
+
+
+
+
+ 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 实现 |
+
+
+ onDelivered({ entry, request }) |
+ INFO 日志 [DingTalk][Approval] delivered approval=<id> outTrackId=<...> |
+
+
+ onDeliveryError({ error, plannedTarget, request }) |
+ WARN 日志 [DingTalk][Approval][DeliveryError] approval=<id> target=<to> error=<msg>。不在此调用 markdown 兜底——兜底在 deliverPending 错误分支同步触发,避免重复发消息 |
+
+
+ onDuplicateSkipped |
+ v1 不实现——origin-only 模式下 dedupe 不会真发生(只有一个 target) |
+
+
+
+
+
+
+
+ 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(v1 不实现)
+
+ v1 不实现该 resolver——SDK 工厂调用 createApproverRestrictedNativeApprovalCapability 时该字段传 undefined。
+ 上游 approval-runtime 看到 supportsApproverDmSurface: false 与 resolveNativeDeliveryMode: "channel" 后不会触发 DM 路径。
+ v2 future 实现时会按 approvers.map(staffId => ({ to: \`user:${staffId}\`, threadId: null })) 模式输出。
+
+
+ 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 分支,v3.5:toggle 变量 + 反查映射):
+ └─ 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)
+ │ approveId: "abc123", // D24 v3.6 主链路:按钮 params.approveId
+ │ // 绑定到此变量,callback 自带
+ │ }, token)
+ │ (按钮 actionId/颜色都在 v3 模板的 approve_btns 按钮组里固化,
+ │ channel 不传按钮定义;只 toggle 三个变量)
+ │
+ └─ markCardRunPendingApproval("ai_card_xxx", "abc123")
+ (D24 v3.6 fallback:approvalId 也写到 registry,应对老卡片 /
+ 平台异常等 callback 没带 params.approveId 的情况)
+
+ → entry = { approvalId:"abc123", accountId:"default",
+ mode:"card", outTrackId:"ai_card_xxx" }
+ └─ core 把 entry 缓存到 activeEntries
+
+→ 群里 staffA 和 staffB 在 agent reply card 底部看到 3 按钮
+ (允许一次 / 总是允许 / 拒绝;btn_stop 暂时隐藏)
+ agent 流式输出暂停(waitDecision 阻塞上游 turn 直到 resolve),
+ 等用户点按钮 / 敲命令 / 过期
+
+ 场景 B:用户在 messageType=markdown 模式触发 exec(markdown 路径)
+ 前提:messageType=markdown,agent reply 已通过 markdown 消息发出。
+ card-run-registry 中无该 sessionKey 的 entry。
+
+调 prepareTarget:
+ ├─ findActiveAgentCard → null
+ ├─ preparedTarget = {
+ │ target: { to:"group:cid_xxx", isGroup:true, accountId:"default" },
+ │ route: "markdown",
+ │ dedupeKey: "dingtalk:default:group:cid_xxx:markdown:abc123",
+ │ }
+
+调 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, {
+ forceMarkdown: true, // ← 必传,否则 messageType=card 下会被
+ // sendProactiveCardText 接管发成另一张
+ // AI Card(src/send-service.ts:371-393)
+ accountId: "default", log })
+ └─ entry = { approvalId:"abc123", accountId:"default", mode:"markdown" }
+ ↑ 注意无 outTrackId
+
+→ 群里出现一条独立 markdown 消息,approver 看到后敲 /approve 命令完成审批
+
+ 场景 C:messageType=card 但 createAndDeliver 中途降级了(runtime 真实场景)
+ 前提:messageType=card 配置,但本次 reply createAndDeliver 失败,
+ reply-strategy 已降级为 markdown,card-run-registry 无 entry。
+
+→ prepareTarget → findActiveAgentCard 返 null → route="markdown"
+→ deliverPending → 走 markdown 路径,与场景 B 完全一致
+
+D10 关键不变量:messageType 配置不参与决策,纯看 card-run-registry 实际状态。
+runtime 任何降级都被天然 cover。
+
+ 场景 D:plugin approval 由 plugin 自己触发,agent 没有 reply card
+ 前提:plugin 调用过程中触发 approval,agent 自身没在生成 reply。
+
+→ findActiveAgentCard 返 null → route="markdown"
+→ markdown 消息发到 plugin 触发会话(turnSourceTo)
+
+注意:此场景下用户可能预期 plugin approval 也走卡片,但 v1 设计明确:
+没有 active agent reply card 就走 markdown。v2 future 可考虑为 plugin
+approval 创建轻量"approval-only" 卡(但当前 D9 v3.3 已明确不新建模板)。
+
+ 场景 E:CLI 触发的 exec approval(turnSourceChannel 非 dingtalk)
+ 前提:用户从 CLI 跑 codex,approval 触发时 turnSourceChannel ≠ "dingtalk"。
+
+→ availability.shouldHandle 直接返 false(§5.1 v1 origin-only 四连判第 2 条)
+→ DingTalk 端不投递;钉钉群里**无任何 approval 痕迹**。
+ 用户必须先从 CLI 终端 / WebUI / 日志获取 approval id,再在钉钉里敲 /approve 完成。
+ v1 不存在"用户在钉钉里被动看见 approval id"的路径——这不是天然兜底,是显式 out-of-band 操作。
+
+v2 future:approver-DM 投递启用后,CLI 场景能自动 DM 给 approver(钉钉端就能看见 id)。
+
+ 6.3 点击 approve → 上游 resolve(核心交互链路)
+ 用户在卡片上点"允许一次"
+
+t=0 DingTalk Stream 平台推送 TOPIC_CARD 回调
+ payload.content/value (内嵌 JSON, v3.5 实配 schema):
+ { cardPrivateData: {
+ actionIds: ["allow-once"], ← 唯一命名无 index 后缀(D15 v3.5)
+ params: { action: "allow-once", approveId: "abc123" } ← D24 主链路
+ } }
+ 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 = "allow-once" ← 唯一命名无后缀
+ │ analysis.userId = "staffA"
+ │ analysis.outTrackId = "ai_card_xxx"
+ │ analysis.cardPrivateData = { ← D16 新增字段
+ │ actionIds: ["allow-once"],
+ │ params: { action: "allow-once", approveId: "abc123" } ← D24
+ │ }
+ │
+ ├─ 【新增分支】tryHandleApprovalCallback(analysis, ...)
+ │ │
+ │ 1. 双源解码(v3.5 对齐实配 schema)
+ │ parsed = parseApprovalFromCardPrivateData(analysis.cardPrivateData)
+ │ // 内部:params.action ∈ {allow-once|allow-always|deny} → decision
+ │ // (actionId 仅用于"是不是 approval 按钮"路由,exact match 即可)
+ │ if (!parsed) return { handled: false }
+ │ parsed = { decision: "allow-once" } ← 注意:还没有 approvalId
+ │
+ │ 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) {
+ │ 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; ← 现在有了
+ │
+ │ 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,
+ │ })
+ │
+ │ 4. 按 result 分支
+ │ if (!result.ok) {
+ │ switch (result.reason) {
+ │ case "unauthorized":
+ │ // 非 approver 点了按钮 → 私聊提示 + 卡片保留(按钮不变)
+ │ await sendProactiveTextOrMarkdown(
+ │ dingtalkConfig,
+ │ `user:${analysis.userId}`,
+ │ "⛔ 你不在 approver 名单,无权批准此请求",
+ │ { 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":
+ │ // 兜底卡片刷成中性终态(参 §1.X 单一事实表)
+ │ await approval-card-patcher.applyExpiredPatch(
+ │ analysis.outTrackId, token, cardStillActive,
+ │ ).catch(() => {})
+ │ return { handled: true, reason: result.reason }
+ │ }
+ │ }
+ │
+ │ 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。
+ │
+ │ 6. 日志 + return { handled: true, reason: "resolved" }
+ │
+ ├─ 分支命中 → 跳过既有 handleCardAction
+ └─ finally: socketCallBackResponse(messageId, { success: true }) (ack 平台)
+
+ 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 上 toggle 变量
+ approval abc123 被 staffA 在 agent reply card 上点了"允许一次"
+
+ ┌──────────────────────────────────────┐
+ │ channel callback handler(§6.3 step 5)│
+ │ approval-card-patcher.applyResolvedPatch(outTrackId,
+ │ decision, token, cardStillActive)
+ │ → 内部 PUT 三个变量(参 §1.X 单一事实表):
+ │ show_approve_btns: "false"
+ │ approveId: ""
+ │ hasAction: cardStillActive ? "true" : "false"
+ │ → clearCardRunPendingApproval(outTrackId) // D24 清反查映射
+ └──────────────────┬───────────────────┘ ← 第一次 update(同步)
+ │
+ ▼ (resolveApprovalOverGateway 异步返回)
+ ┌─────────────────────────────────────┐
+ │ upstream approval store │
+ │ abc123 → resolved │
+ │ decision = allow-once │
+ │ resolvedBy = staffA │
+ └──────────────────┬──────────────────┘
+ │ 触发上游 runtime
+ ▼
+ presentation.buildResolvedResult({ view, entry })
+ returns { kind:"update", payload:{
+ phase:"resolved", decision:"allow-once",
+ resolverDisplayName: view.resolvedBy,
+ }}
+ │
+ ▼
+ transport.updateEntry:entry.mode === "card" 分支
+ → approval-card-patcher.applyResolvedPatch(entry.outTrackId, ...)
+ → 与 callback handler 的 step 5 内容一致,幂等覆盖 OK
+ → core 从 activeEntries 移除该 entry
+
+ 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 三个变量:
+ show_approve_btns / approveId / hasAction,参 §1.X 单一事实表)
+
+ mode === "markdown":v1 不发新通知消息
+ transport.updateEntry:entry.mode === "markdown" 分支
+ └─ no-op(markdown 消息不能 edit,发新通知消息会刷屏)
+
+用户从命令成功的自然反馈感知(agent 继续/停止;approval 状态在上游同步可见)
+
+v2 future 可视用户反馈再加 "✅ 已批准 by @staffA" 的轻量回执消息
+
+ v2 future(DM 投递启用后)
+ core 会对所有 entry(origin + 每个 DM)调 updateEntry,所有卡片同步刷成相同终态。当前 v1 origin-only 每个 approval 只有 1 个 entry。
+
+ 6.5 三个 decision 的差异(v3.5 同步)
+
+ | 按钮 | card 按钮 actionId | params.action | 等价 /approve 命令 | resolve 后 agent 行为 |
+
+ | ✅ 允许一次 | 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)
+
+ - callback-handler 解析按钮 → 主链路读
params.approveId(D24 v3.6);缺失 fallback 到 resolveCardRun(outTrackId).pendingApprovalId
+ - 反查可能命中(resolved 后还没来得及 clear)或未命中(已 clear):
+
+ - 命中:调 resolver → 上游返回 already-resolved → catch 分支调
applyExpiredPatch(字段集见 §1.X 单一事实表:show_approve_btns=false + approveId="" + hasAction 恢复)把按钮再次隐藏
+ - 未命中:直接调
applyExpiredPatch,return "no-pending-approval"
+
+
+ - 对用户:第二次点击 = 按钮立即消失,与第一次结果一致,无打扰提示
+
+
+ 非 approver 点击
+
+ authorizeActorAction 返回 authorized: false
+ - 调
sendProactiveTextOrMarkdown(config, "user:" + clicker.staffId, "⛔ 你不在 approver 名单", ...) 给点击者私聊(target 必须带 user: 前缀走 oto 投递)
+ - 卡片不变(按钮保留,给真正的 approver 用)
+ - 日志
[DingTalk][Approval][Denied] approvalId=<id> clicker=<userId>
+
+
+ Channel 重启后用户点旧卡片(v3.5)
+
+ - channel 端 card-run-registry 内存清空 → 主链路
params.approveId 仍能命中(D24 v3.6 改进),但若卡片是 v3 之前版本则 fallback resolveCardRun(outTrackId).pendingApprovalId 返 null
+ - callback-handler 进 "no-pending-approval" 分支 → 调
applyExpiredPatch(outTrackId, token, cardStillActive)(字段集见 §1.X 单一事实表:show_approve_btns=false + approveId="" + hasAction 按 cardStillActive 恢复),return
+ - 不调上游 resolve(没 approvalId 可传);上游 approval 由它自己的 TTL 兜底
+ - 用户体感:按钮点了一下就消失——降级到"按钮自动消失",对长时间下线可接受
+
+
+ 上游过期事件触达
+ upstream timer 触发 approval.expired event
+ ├─ presentation.buildExpiredResult({ entry, view }) → { kind: "update", payload: {
+ │ phase: "expired",
+ │ }}
+ └─ transport.updateEntry({ entry, payload, phase: "expired" })
+ → entry.mode === "card" 分支:
+ 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
+
+ agent reply card:approval 按钮自动消失,btn_stop 视 card 状态恢复
+
+ Channel stopClient(账号停用 / gateway 重启)—— v3 推迟到 v2
+
+ v1 不实现 stop-time finalize(D13 v3 推迟)。原因:D18 删除本地 store 后 channel 端无法枚举 pending entries。
+ v1 行为:停机时遗留 approval 卡片保留按钮态;用户点击 → §6.6"Channel 重启后用户点旧卡片"路径 → 调 applyExpiredPatch(隐藏按钮 + 清 approveId,参 §1.X;v1 不写终态文字)。
+ v2 future:若 SDK 暴露 activeEntries 查询 API,或 channel 引入轻量 outTrackId Set(仅供 stop-time 清理用,非完整 entry store),再实现 finalize。
+
+
+ 6.7 失败处理(v3.4 修订:card 失败明确降级 markdown)
+
+ card 路径失败的两种亚态
+ approval-card-patcher.applyPendingPatch(outTrackId, ...)
+ ├─ PUT updateCardVariables
+ │
+ ├─ 明确失败(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 标 entry 失败;markdown 已是最低保障路径,再降无意义
+ 此场景用户可能完全感知不到 approval 存在 → 仅靠 OpenClaw 端 pending 告警
+
+
+
v3.4 失败降级策略关键原则
+
+ - card 明确失败 → markdown 降级:保证用户至少能看到 approval id + /approve 命令模板,否则用户连兜底命令都用不上
+ - card 模糊失败(超时)→ 不重发:避免"实际成功 + 重发"导致双消息
+ - 实现层面区分两种亚态的判定:HTTP status code 已收到 + body 含错误码 = 明确失败;socket timeout / connection reset = 模糊失败
+ - markdown 失败不再降级:本来就是最低保障路径
+ - 所有失败都进 OpenClaw 端 approval pending 监控告警(runtime 已有此能力)
+
+
+
+ 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 函数:
+// 既有顺序(行号参 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 决策之前拦截。
+//
+// 插入点必须满足:
+// ✓ 晚于 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 行
+const textForApproveCheck = !isDirect
+ ? extractedContent.text.replace(/^(?:@\S+\s+)*/u, "").trim() // 群里剥前导 @mention
+ : extractedContent.text.trim();
+
+// 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,
+ accountId: account.accountId,
+ text: textForApproveCheck,
+ senderId, // 入站 senderStaffId
+ log,
+ });
+ 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) {
+ // 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 派发
+ }
+
+ 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})`,
+ { 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(() => {});
+ }
+ if (!result.ok) {
+ log?.info?.(`[DingTalk] /approve resolver returned ${result.reason}`);
+ }
+ return true;
+}
+
+
+ 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"
+
+ | 方案 | 结果 |
+
+
+ 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 风险表登记)。
+
+
+
+
+
+ 7. 审批卡片设计
+
+ 7.1 v2 模板字段映射(v3.5 对齐用户实配 schema)
+ v3 模板(schema id 末尾 05061.schema)相比 v2 新增的内容(v3.12 精确化描述):
+
• 2 个 cardParamMap 变量(channel 需要 PUT):show_approve_btns(Boolean 开关)+ approveId(字符串,D24 主链路载体)
+
• 1 个内置 ButtonGroup:approve_btns——按钮组定义本身(含 3 按钮 actionId/params 绑定),不属于 PUT 字段集,channel 永不传它
+
完整低代码 schema(v3.0.0)见 docs/assets/card-template-v3.json(268KB,从开发者平台导出)——可重新导入开放平台卡片搭建器对比 / 定制 / 版本迁移用。
+
+ | cardParamMap 变量 | 类型 | approval 用途 | pending / resolved / expired 时的值 |
+
+
+ approve_btns |
+ 按钮组(template 内置 3 按钮定义) |
+ 不在 channel 端构造——按钮 actionId / params 都在模板里固化(D15) |
+ 不需要 channel 端更新 |
+
+
+ show_approve_btns |
+ Boolean |
+ approval 按钮组的显隐条件 |
+ pending:"true";resolved/expired:"false" |
+
+
+ hasAction(既有字段) |
+ Boolean |
+ btn_stop 的显隐(既有 AI Card v2) |
+ pending:"false"(D23 隐藏 stop);resolved/expired:card 还 active ? "true" : "false" |
+
+
+ | approval 终态指示位 |
+ — |
+ 显示"✅ 已批准 by @user · allow-once" / "ℹ️ 已处理或已过期" |
+ v1 不实现——用户感知"按钮消失=已处理"(详见下方限制) |
+
+
+
+
+
+
v3.5 v1 终态展示限制(坦率描述)
+ 用户实配 schema 没有专门给 approval 终态文字的字段位:
+
+ statusLine:已被 taskInfo 占用(agent 显示 model/tokens/effort/taskTime/dapiUsage/agent 名)
+ content:与 agent stream 写冲突,patch append 不安全
+ blockList:未确认模板支持 system/notification block 类型
+ quoteContent:群聊引用上下文用,语义不匹配
+
+
v1 策略:不主动写终态文字。show_approve_btns 由 true → false 让按钮消失,用户感知"我点的按钮成功了"即可。
+
v1.x 升级路径:让维护者在模板加
approval_status 变量(小工作量)后再 PUT 写入"✅ 已批准 by @<name> · <decision>"
+
+
+ 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" },
+// { name: "approveId", type: 变量, value: "approveId" } // ← D24 绑定到变量
+// ]
+//
+// 按钮 2 · 总是允许(color: blue)
+// ActionId: "allow-always"
+// 回传参数: [
+// { name: "action", type: 静态值, value: "allow-always" },
+// { name: "approveId", type: 变量, value: "approveId" }
+// ]
+//
+// 按钮 3 · 拒绝(color: red)
+// ActionId: "deny"
+// 回传参数: [
+// { name: "action", type: 静态值, value: "deny" },
+// { name: "approveId", type: 变量, value: "approveId" }
+// ]
+//
+// 显示控制 → 条件计算:show_approve_btns 的值为 true → 整组可见
+// ─────────────────────────────────────────────────────────────
+
+// 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)
+// fallback:markCardRunPendingApproval(outTrackId, approvalId) 写到 card-run-registry
+// 仅当 callback 不带 params.approveId 时反查(D24 兜底)
+
+ v3.5 实证:actionId 唯一命名时 DingTalk callback 原样回传,无 index 后缀。
+ 回调解析主链路读 params.approveId,params.action 取 decision,详见 §1.X 单一事实表 + §6.3。
+
+
+ 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 ───┤
+ ├─ /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 按钮)
+
+──────────────────────────────────────────────────────
+
+两状态机的协同(D22 关键不变量):
+• approval 只对 cardParamMap 字段做 patch,不触碰 card 的 state
+• card 自然走到 FINISHED 后,approval 按钮已隐藏(show_approve_btns=false 永久态)
+• 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 · 群聊 · v3.2 旧版示意)
+
+
+
+
+
U
+
小赵
@OpenClaw 帮我把 720 小时之前的 docker image 清理掉
+
+
+
O
+
+
OpenClaw
+
+
+
+
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
+
+
+
+
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 私聊)—— v2 future
+ v1 不支持 approver-DM 投递(D4 v3 修订);本 mockup 保留作为 v2 future 设计参考。v1 中 plugin approval 卡片同样发到 origin 会话(不是 DM);DM 私聊场景仅在用户跟 agent 1:1 触发时出现(origin == DM[user])。
+
+
+
+
+
O
+
+
OpenClaw
+
+
+
+
Agent 请求授权使用工具:postgres-mcp
+
将对你的生产 PG 库执行:
+
tool: query_database
+description: 对 production.orders 表查询近 7 天订单
+
+ - severity: 🔴 high(涉及生产数据)
+ - approval id:
plugin-xyz789
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 8. 错误处理矩阵
+
+ | 失败点 | 触发条件 | 处理 | 用户体验 |
+
+
+ | 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 命令模板,可手动完成审批 |
+
+
+ | 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 失败 |
+ PUT /v1.0/card/instances 5xx |
+ WARN 日志,不重试(避免 race);卡片视觉短暂不一致,下次相关事件会再覆盖 |
+ 偶发不一致;上游 store 仍是真相 |
+
+
+ | parseApprovalFromCardPrivateData 失败 |
+ cardPrivateData 缺失 / actionIds[0] 不在 {"allow-once","allow-always","deny"} / params.action 缺失或值非法(v3.5 实配 schema,已对齐 D15 修订) |
+ WARN 日志 + ack 平台 + 卡片不变(保留按钮);continue 走原 callback 既有路径(feedback / btn_stop 等) |
+ 非 approval 按钮回调不受影响;approval 按钮异常时可通过 /approve 命令兜底 |
+
+
+ | 权限校验失败 |
+ clicker.staffId 不在 approver 名单 |
+ 调 sendProactiveTextOrMarkdown 给点击者私聊"⛔ 无权批准";卡片不变;日志 |
+ 点击者私聊收到拒绝提示,原卡片对其他人仍可用 |
+
+
+ | 上游 resolve 返回 already-resolved |
+ 用户重复点击 / 多端同时点 |
+ callback-handler 静默成功,强制 update 本机卡片到终态 |
+ 第二次点击看起来等同生效,无打扰 |
+
+
+ | 上游 resolve 返回 not-found |
+ approvalId 在上游已过期/清除(如 channel 长时间下线) |
+ callback-handler 调 applyExpiredPatch:隐藏 approval 按钮组 + 清 approveId 变量(字段集见 §1.X 单一事实表;v1 不写"已处理或已过期"等终态文字,§7.1 已说明 schema 无字段位) |
+ 按钮自动消失,用户感知"已是终态" |
+
+
+ | turnSourceChannel 不是 dingtalk(CLI 触发) |
+ v1 origin-only,无 DM 兜底 |
+ availability.shouldHandle 返回 false → 上游 approval-runtime 不调用 DingTalk 投递路径;用户在钉钉里用 /approve 命令完成(§6.8) |
+ 需用户主动复制 approval id;v2 future 自动 DM 给 approver |
+
+
+ | multi-approver 在 origin 群里竞争点击 |
+ 正常场景(v1 仅 1 张卡片,多 approver 同群) |
+ 第一个点击的成功 → 上游标记 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 未继续"的卡死 |
+
+
+
+
+
+
+
+ 9. 测试策略
+
+ 9.1 测试文件布局
+
+ | 文件 | 覆盖目标 | 预计 case 数 |
+
+ tests/unit/approval-config.test.ts | schema 解析、normalize、enabled=auto、fallback chain | ~12 |
+
+
+ tests/unit/approval-target-resolver.test.ts | origin 解析(含 turnSourceChannel=null)、DM 列表构造 | ~10 |
+ tests/unit/approval-resolver.test.ts ★ v3.2 / v3.12 补 invalid-decision | D20/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.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 改动:analyzeCardCallback 抽 cardPrivateData 含 actionIds + params;既有 feedback / btn_stop 用例不受影响 | +6 |
+ 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;同一卡片已有不同 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.test | buildExecApprovalMarkdown / buildPluginApprovalMarkdown:含 /approve 三种 decision 命令模板、过期 hint、id 显示 | ~8 |
+ tests/unit/approval-capability.test.ts | SDK 工厂参数装配正确、capability 单例 | ~6 |
+ tests/unit/approval-native-runtime.test.ts | 4 子 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 拒绝。当前功能分支尚未创建该 integration 文件,若作为合并门禁需补齐;若先合并,应转为后续验证 TODO。 | ~10 |
+
+
+
+ v3 删除:approval-store.test.ts(无本地 store)、approval-cancel.test.ts(无 finalize-on-stop)、approval-channel-stop.test.ts(同上)。
+ 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 策略
+
+ - 所有 DingTalk HTTP(createAndDeliver / updateCardVariables / sendProactiveTextOrMarkdown):
vi.mock("../../src/http-client") 与 vi.mock("../../src/auth"),不打真实 API
+ - 上游 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 全局开启)
+
+
+ 9.3 关键 integration 场景(v3:仅 origin)
+
+ - 群里 multi-approver 点击:发起 → 群里发 1 卡 → approverA 点 approve → 卡片刷成终态;approverB 第二次点击 → already-resolved → 卡片再刷一次(幂等)
+ - self-approval 在 DM:approver 自己 DM 发起 exec → 投 1 卡到自己私聊 → 自己点 → 通过
+ - 非 approver 点击:投 1 卡 → 非 approver 用户点 → 收私聊拒绝 → 卡片不变(按钮保留)
+ - 过期:投卡 → mock 上游 expired event → core 调 updateEntry → 卡片刷成过期
+ - 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 投递;钉钉里无 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 覆盖率目标
+
+ src/approval/*:line ≥ 90%,branch ≥ 85%
+ - 整体仓库 coverage 不下降(pnpm test:coverage)
+
+
+
+
+
+ 10. 实施阶段
+ 按"先上链路、再上 UX、最后部署模板"分 3 阶段。每阶段独立可合并,前一阶段不阻塞后一阶段对应的 PR review。
+
+ 阶段 0 · 前置依赖(必须先满足)
+
+
D17:阶段 1 PR 提交前必须完成的事
+
+ - SDK 基线三件套同时到位(v3.2 关键补充——peerDep 只是冰山一角):
+
+ package.json 的 peerDependencies.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)
+ 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 模板的
sendCardRequest 回调格式已在当前 main 可用。v3.6 本设计在此基础上发布 v3 模板(v2 字段超集,按钮在模板内置,channel 不构造按钮);详见 §10 阶段 0
+ - 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):
+
+ - 用户重新 export 完整 schema 到 docs/assets/card-template-v3.json(含 approveId 变量 + 三按钮 params 绑定的最终状态)
+ - 把 v3 schema 发布为预置卡片模板(用户已完成 ✓,正式 templateId =
58f73932-fc3b-46ae-8e90-93313e405061.schema)
+ - 必须替换
src/card/card-template.ts:6 的 BUILTIN_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 的超集向后兼容,并存会让"哪个场景用哪个模板"的决策复杂化)
+ - 真机回归确认:(a) v3 模板下既有 AI Card 流式行为不变;(b)
show_approve_btns toggle 行为符合 §6.2 / §6.4;(c) callback payload 含 cardPrivateData.params.approveId 字符串(与 patch 时设的值一致)
+
+
+
+
+ 阶段 1 · 接口骨架 + 统一 resolver + 命令链路(PR-1)
+
+ - 新增
src/approval/ 目录骨架(v3.3:10 个文件,含 card-locator)
+ - 实现核心收敛点(D20):
approval-resolver.ts、approval-command-parser.ts
+ - 实现 v3.3 新增的
approval-card-locator.ts(D22 落地)—— PR-1 就要做,因为 markdown 路径下 locator 也会被调(返回 null 是触发 markdown 的信号)
+ - 实现支撑模块:
approval-config.ts、approval-target-resolver.ts(仅 origin)、approval-capability.ts(不含 nativeRuntime 完整实现)、approval-command-intercept.ts(薄壳,调 parser + resolver)
+ - 在
src/channel.ts 挂上 approvalCapability,nativeRuntime 暂留 undefined;capability 仅生效工厂吐出的部分(authorizeActorAction / getActionAvailabilityState / delivery / native.describeDeliveryCapabilities)。
v3.4 修订:不引入 resolveApproveCommandBehavior——上游工厂 createApproverRestrictedNativeApprovalCapability(openclaw/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、配置文档(草稿)
+ - 测试:
approval-resolver(含 §D21 kind 推导 / allowPluginFallback / 错误分类全分支)、approval-command-parser(含上游 alias 对照断言)、approval-config、approval-target-resolver、approval-capability、approval-command-intercept、inbound-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)
+
+ - 实现 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;v3.9 改动面收紧):当前 src/card-callback-service.ts:6 的 CardCallbackAnalysis 接口只暴露 actionId,approval handler 拿不到 params.action 和 params.approveId。v3.9 实测改动面 ~5 行——核对 src/card-callback-service.ts:100-110,analyzeCardCallback 内部已经在解析三层 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 分支(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 按钮(show_approve_btns 切 true)+ 点按钮后按钮消失(show_approve_btns 切 false + approveId 清空)+ agent 继续 stream(hasAction 按 cardStillActive 恢复 btn_stop);v1 不验证终态文字(schema 无字段位)
+ - 这阶段交付后:完整 v3.3 双路由 UX 在真机可用
+
+
+ 阶段 3 · 用户文档与回归收尾(PR-3)
+
+ - 更新用户文档
docs/user/features/exec-approval.md(配置示例、approver 名单、UX 截图)
+ - 更新 README 与 release notes(特别说明 peerDependency BREAKING)
+ - 补真机回归记录到
docs/artifacts/(与现有 v2 卡片真机回归同模式)
+ - 这阶段交付后:feature 正式宣告 production-ready
+
+
+
+
分阶段的好处
+
+ - PR-1 可独立 merge:DingTalk 端 resolve 通道与 approver 权限校验生效;用户从外部界面拿到 approval id 后可在钉钉里手敲 /approve 完成(与 Feishu approval-auth 同档;完整 UX 在 PR-2)
+ - PR-2 引入大量代码但全部聚焦 native runtime;阶段 0 模板已发布,可直接进真机回归
+ - PR-3 只动文档与回归记录,code-review 量极小,便于聚焦写作质量
+ - 任何阶段回滚都不破坏前阶段已交付能力
+
+
+
+ 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 真机回归
+
+
+
+
+
+ 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):v1 仅为 approval 实现 pending/resolved/expired 状态机;将之普适到其它交互需要更多用例验证,留待 #12 message tool action surface
+ - 主动 rebind on restart(D12 B 选项):留待 v2
+ - interactions sub-adapter:v1 不实现,仅在 DM 启用后才有实际收益
+ - respect 上游 allowedDecisions 字段(v3.11 策略重新设计):
+
上游 ExecApprovalRequest.allowedDecisions(openclaw/src/infra/exec-approvals.ts:1241)与 PluginApprovalRequest.allowedDecisions(openclaw/src/infra/plugin-approvals.ts:54)允许 per-request 限制可选 decision。例如 ask="always" 时 exec 只允许 allow-once + deny(不允许 allow-always)。
+
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 已知风险
+
+ | 风险 | 影响 | 缓解 |
+
+
+ | approval-card 模板需在 DingTalk 侧发布为跨租户预置 |
+ 若发布失败 → 所有用户都走 markdown 兜底 |
+ 阶段 3 在发布后做真机回归;fallback 路径保证不出现 approval 静默消失 |
+
+
+ | 上游 commands-approve.ts 扩展 decision alias / 命令顺序 |
+ 钉钉 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 修订相关) |
+ 按 approvalId.startsWith("plugin:") 派发 exec/plugin;若上游未来引入 unprefixed plugin id 或新 kind,本派发会把 plugin 当成 exec(权限校验仍正确,但 metrics/log/未来 plugin-specific authorization 可能出错) |
+ CI 加一个上游 id 命名约定的契约测试;若上游新增 kind,本派发函数 + capability.authorizeActorAction 调用方都需同步更新;考虑设计 allowPluginFallback 配置位以兜底 |
+
+
+ | DingTalk 平台未来变更 cardPrivateData 字段命名 |
+ 所有 approval 按钮点击解析失败 → 卡片按钮点了无反应 |
+ 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 失败(企业权限) |
+ 该 approver 收不到 DM;origin 仍可见 |
+ transport 内部 WARN 日志;用户文档明示需配置工作通知权限 |
+
+
+ | updateCardVariables 在频繁 approve/expire 时遇上 race |
+ 卡片视觉短暂不一致 |
+ "upstream store 是真相"原则;不重试 update;下次相关事件会覆盖 |
+
+
+ | 多账号场景下 entry 隔离 |
+ 潜在跨账号污染(core activeEntries 由 SDK 管,channel 不直接干预) |
+ transport.deliverPending 返回的 entry 强制带 accountId;updateEntry 调用方也按 accountId 路由;测试覆盖跨账号场景 |
+
+
+ | v1 origin-only 在 CLI 触发场景下无 DM 兜底 |
+ 用户必须**先从外部界面获取 approval id**(CLI 终端 / WebUI / OpenClaw 日志),再到钉钉敲 /approve <id> <decision>。**不是天然兜底**——v1 没有"钉钉端被动看见 id"的路径 |
+ v1:在 OpenClaw CLI 输出里清晰打印 approval id 与命令模板;v2:启用 DM 投递自动到达 approver |
+
+
+
+
+
+
+
+ 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 端 §6.8 regex 需保持与此处 alias 集合对齐)
+ - openclaw/plugin-sdk/approval-gateway-runtime —
resolveApprovalOverGateway 公开 API(v2026.4.7+ 引入;本设计的按钮 / 命令两条路径都调它)
+
+
+ 本设计的对照实现(必读)
+
+ - 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 实现
+
+ - 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
+
+
+
+
+
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..3d202d2c
--- /dev/null
+++ b/docs/plans/2026-05-19-gap-01-approval-native.md
@@ -0,0 +1,4954 @@
+# 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(DONE)
+
+**Files:**
+- 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/*` 内部)。
+> **实施偏差:** 原 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 都依赖)
+
+```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/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..99d6e0bb
--- /dev/null
+++ b/docs/user/features/exec-approval.md
@@ -0,0 +1,102 @@
+# 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.
+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
+
+| 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/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."
diff --git a/package.json b/package.json
index a13619f7..b63e7c04 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": {
@@ -68,7 +73,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 +87,7 @@
"vitest": "^3.2.4"
},
"peerDependencies": {
- "openclaw": ">=2026.3.28"
+ "openclaw": ">=2026.5.7"
},
"peerDependenciesMeta": {
"openclaw": {
@@ -91,10 +96,10 @@
},
"openclaw": {
"compat": {
- "pluginApi": ">=2026.3.28"
+ "pluginApi": ">=2026.5.7"
},
"build": {
- "openclawVersion": "2026.3.28"
+ "openclawVersion": "2026.5.7"
},
"extensions": [
"./index.ts"
@@ -120,7 +125,7 @@
]
},
"install": {
- "minHostVersion": ">=2026.3.28",
+ "minHostVersion": ">=2026.5.7",
"npmSpec": "@soimy/dingtalk",
"localPath": ".",
"defaultChoice": "npm"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index d695341a..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,21 +24,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.5.7
+ version: 2026.5.7(@types/express@5.0.6)
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 +56,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.0':
+ resolution: {integrity: sha512-ONj+Q8qOdNQp5XbH5jnMwzT9IKZJsSN0p0lkceS4GtUtNOPVLpNzSS8gqQdGMKfBvA0ESbkL8BTaSN1Rc9miEw==}
peerDependencies:
zod: ^3.25.0 || ^4.0.0
@@ -145,8 +148,17 @@ 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
+ peerDependenciesMeta:
+ 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
@@ -154,8 +166,8 @@ packages:
zod:
optional: true
- '@anthropic-ai/vertex-sdk@0.14.4':
- resolution: {integrity: sha512-BZUPRWghZxfSFtAxU563wH+jfWBPoedAwsVxG35FhmNsjeV8tyfN+lFriWhCpcZApxA4NdT6Soov+PzfnxxD5g==}
+ '@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==}
@@ -174,130 +186,149 @@ 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==}
+ '@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/client-bedrock@3.1020.0':
- resolution: {integrity: sha512-OIM38upZjWsi62070cOm2nZAJsIeZC26KhOFDt8T6gmzbfcoz7XgkJ6eK9/JFfFagoFykUvXX0nfbcqtryWY0A==}
+ '@aws-sdk/credential-provider-cognito-identity@3.972.35':
+ resolution: {integrity: sha512-mMQsBJv40oi5QdqRj4Xbc9jTlWMxqWfs5zWu+RhbOuF5F0AxxWXT70hm0abOmLbF2M/Tkuygs01H4eWIQMfoMw==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/core@3.973.26':
- resolution: {integrity: sha512-A/E6n2W42ruU+sfWk+mMUOyVXbsSgGrY3MJ9/0Az5qUdG67y8I6HYzzoAa+e/lzxxl1uCYmEL6BTMi9ZiZnplQ==}
+ '@aws-sdk/credential-provider-env@3.972.38':
+ resolution: {integrity: sha512-m3WjZEgPtioMhPmwqUt+DhlTJ2i9ufR6DhfkyXojb9puEvfR+ur2U5shavu5/Cc9WHHsDCvALi6UFHgcqjhQ5w==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-env@3.972.24':
- resolution: {integrity: sha512-FWg8uFmT6vQM7VuzELzwVo5bzExGaKHdubn0StjgrcU5FvuLExUe+k06kn/40uKv59rYzhez8eFNM4yYE/Yb/w==}
+ '@aws-sdk/credential-provider-http@3.972.40':
+ resolution: {integrity: sha512-D78L/m2Dr6cJnnSvWoAudPhQmCwmJ7j6APXsPYmFpPaKfQTfCSu0rdm8j14Np+VmXF9z8Aj8HE3xFpsrwtfgeg==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-http@3.972.26':
- resolution: {integrity: sha512-CY4ppZ+qHYqcXqBVi//sdHST1QK3KzOEiLtpLsc9W2k2vfZPKExGaQIsOwcyvjpjUEolotitmd3mUNY56IwDEA==}
+ '@aws-sdk/credential-provider-ini@3.972.42':
+ resolution: {integrity: sha512-Mu5ESvFXeinafVM8jTIvRqcvK2Ehj4kz3auT39yUcHwu1Vfxo6xRlmUafdKLW4tusjAJukQwK09sCSMgOm7OKg==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-ini@3.972.28':
- resolution: {integrity: sha512-wXYvq3+uQcZV7k+bE4yDXCTBdzWTU9x/nMiKBfzInmv6yYK1veMK0AKvRfRBd72nGWYKcL6AxwiPg9z/pYlgpw==}
+ '@aws-sdk/credential-provider-login@3.972.42':
+ resolution: {integrity: sha512-O6WkZga3kf0yqyJYd1dbeJqVhEgJx/x1UaLgtbR+XuL/YP+K5y6QTxQKL7ka9z3jnQASESKGAPnRyt4D5hQrxA==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-login@3.972.28':
- resolution: {integrity: sha512-ZSTfO6jqUTCysbdBPtEX5OUR//3rbD0lN7jO3sQeS2Gjr/Y+DT6SbIJ0oT2cemNw3UzKu97sNONd1CwNMthuZQ==}
+ '@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.29':
- resolution: {integrity: sha512-clSzDcvndpFJAggLDnDb36sPdlZYyEs5Zm6zgZjjUhwsJgSWiWKwFIXUVBcbruidNyBdbpOv2tNDL9sX8y3/0g==}
+ '@aws-sdk/credential-provider-node@3.972.43':
+ resolution: {integrity: sha512-D/DJmbrWRP5BXEO3FH+ar4el+2n6OlGofiud7dQun2jES+AQEJjczenp1jBb4MBN7CpGpS8nsWGQLtuzc9tQbA==}
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-process@3.972.38':
+ resolution: {integrity: sha512-EnbYVajGgbkb24s0K1eo4VNAPV5mHIET7LSvirTaFCwkfrfaOJxtSE+wY/tJdKDS21cEYkZs2ruCaAm+W4iblg==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/credential-provider-sso@3.972.28':
- resolution: {integrity: sha512-IoUlmKMLEITFn1SiCTjPfR6KrE799FBo5baWyk/5Ppar2yXZoUdaRqZzJzK6TcJxx450M8m8DbpddRVYlp5R/A==}
+ '@aws-sdk/credential-provider-sso@3.972.42':
+ resolution: {integrity: sha512-RVV/9NbFwI8ZHEH5dn39lGyFmSbSVj1+orZdr6QsOe1mW9DCglmlen0cFaNZmCcqkqc7erNRHNBduxbeZuHAnw==}
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-web-identity@3.972.42':
+ resolution: {integrity: sha512-/67fXX0ddllD4u2Nujc5PvT4byHgpMUfz6+RxIKi/0nFIckeorm7JvXgzBuDyVKw0s58EbofmETDWUf9vTEuHQ==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/eventstream-handler-node@3.972.11':
- resolution: {integrity: sha512-2IrLrOruRr1NhTK0vguBL1gCWv1pu4bf4KaqpsA+/vCJpFEbvXFawn71GvCzk1wyjnDUsemtKypqoKGv4cSGbA==}
+ '@aws-sdk/credential-providers@3.1049.0':
+ resolution: {integrity: sha512-2B0ljqENrXKHlPg50kCV12RA35jcAfgZLLB38NW9qv5gUtOHFS8/wW7AkbyVAFYQySl/+2a3x1MAjY2d+Ed71g==}
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/eventstream-handler-node@3.972.16':
+ resolution: {integrity: sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/middleware-host-header@3.972.8':
- resolution: {integrity: sha512-wAr2REfKsqoKQ+OkNqvOShnBoh+nkPurDKW7uAeVSu6kUECnWlSJiPvnoqxGlfousEY/v9LfS9sNc46hjSYDIQ==}
+ '@aws-sdk/middleware-eventstream@3.972.12':
+ resolution: {integrity: sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/middleware-logger@3.972.8':
- resolution: {integrity: sha512-CWl5UCM57WUFaFi5kB7IBY1UmOeLvNZAZ2/OZ5l20ldiJ3TiIz1pC65gYj8X0BCPWkeR1E32mpsCk1L1I4n+lA==}
+ '@aws-sdk/middleware-host-header@3.972.13':
+ resolution: {integrity: sha512-EA3+u2LD3kGcfRNmCSjyJuzX4XvG4zYv57i4ZksH+1IEciuSyHQGvzivEz7vZ+jbRPdAAe7WWKy/4M8InCKDcw==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/middleware-recursion-detection@3.972.9':
- resolution: {integrity: sha512-/Wt5+CT8dpTFQxEJ9iGy/UGrXr7p2wlIOEHvIr/YcHYByzoLjrqkYqXdJjd9UIgWjv7eqV2HnFJen93UTuwfTQ==}
+ '@aws-sdk/middleware-logger@3.972.12':
+ resolution: {integrity: sha512-NxB2dS4/mV3380hNkC72TkhMaLLjWGGBeTAEucqlJptVVovTbNmQWZLwaMC74ICo9NZHmFiBVVTHzDfAh/3y6Q==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/middleware-user-agent@3.972.28':
- resolution: {integrity: sha512-cfWZFlVh7Va9lRay4PN2A9ARFzaBYcA097InT5M2CdRS05ECF5yaz86jET8Wsl2WcyKYEvVr/QNmKtYtafUHtQ==}
+ '@aws-sdk/middleware-recursion-detection@3.972.14':
+ resolution: {integrity: sha512-bqL+upATpOJ/7px4IVfMVxcM6Lyt9uRizmEx3mNg4N6+IQlnOaYXXOJ4TNX6P0mKPPW0lwn9ZW8QEhXwQuCH9A==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/middleware-websocket@3.972.13':
- resolution: {integrity: sha512-Gp6EWIqHX5wmsOR5ZxWyyzEU8P0xBdSxkm6VHEwXwBqScKZ7QWRoj6ZmHpr+S44EYb5tuzGya4ottsogSu2W3A==}
+ '@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'}
- '@aws-sdk/nested-clients@3.996.18':
- resolution: {integrity: sha512-c7ZSIXrESxHKx2Mcopgd8AlzZgoXMr20fkx5ViPWPOLBvmyhw9VwJx/Govg8Ef/IhEon5R9l53Z8fdYSEmp6VA==}
+ '@aws-sdk/nested-clients@3.997.10':
+ resolution: {integrity: sha512-FtQ/Bt327peZJuyo4WZSOLVUTw9ujRxntepiC7L65FxA2P82Xlq0g14T22BuqBUeMjDoxa9nvwiMHjLIfP3eUg==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/region-config-resolver@3.972.10':
- resolution: {integrity: sha512-1dq9ToC6e070QvnVhhbAs3bb5r6cQ10gTVc6cyRV5uvQe7P138TV2uG2i6+Yok4bAkVAcx5AqkTEBUvWEtBlsQ==}
+ '@aws-sdk/region-config-resolver@3.972.16':
+ resolution: {integrity: sha512-/YaivCvKUkEeMN9VTKBSvBn5w/4osAM1YboM58DKaLF/vqFGf/FdJCLmppqiPPJWZaXcASqByVjc3evE7KHKdA==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/token-providers@3.1014.0':
- resolution: {integrity: sha512-gHTHNUoaOGNrSWkl32A7wFsU78jlNTlqMccLu0byUk5CysYYXaxNMIonIVr4YcykC7vgtDS5ABuz83giy6fzJA==}
+ '@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.1020.0':
- resolution: {integrity: sha512-T61KA/VKl0zVUubdxigr1ut7SEpwE1/4CIKb14JDLyTAOne2yWKtQE1dDCSHl0UqrZNwW/bTt+EBHfQbslZJdw==}
+ '@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.1021.0':
- resolution: {integrity: sha512-TKY6h9spUk3OLs5v1oAgW9mAeBE3LAGNBwJokLy96wwmd4W2v/tYlXseProyed9ValDj2u1jK/4Rg1T+1NXyJA==}
+ '@aws-sdk/token-providers@3.1049.0':
+ resolution: {integrity: sha512-r7+d0lQMTHKypkmaF5jRTBYLYHCUHzt3gaVoN9SidLhQeWhCmHk3AKrboDTpPF5b7Pt7vKu3+oeMjznM2Eu1ow==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/types@3.973.6':
- resolution: {integrity: sha512-Atfcy4E++beKtwJHiDln2Nby8W/mam64opFPTiHEqgsthqeydFS1pY+OUlN1ouNOmf8ArPU/6cDS65anOP3KQw==}
+ '@aws-sdk/types@3.973.8':
+ resolution: {integrity: sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==}
engines: {node: '>=20.0.0'}
- '@aws-sdk/util-endpoints@3.996.5':
- resolution: {integrity: sha512-Uh93L5sXFNbyR5sEPMzUU8tJ++Ku97EY4udmC01nB8Zu+xfBPwpIwJ6F7snqQeq8h2pf+8SGN5/NoytfKgYPIw==}
+ '@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.8':
- resolution: {integrity: sha512-J6DS9oocrgxM8xlUTTmQOuwRF6rnAGEujAN9SAzllcrQmwn5iJ58ogxy3SEhD0Q7JZvlA5jvIXBkpQRqEqlE9A==}
+ '@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.8':
- resolution: {integrity: sha512-B3KGXJviV2u6Cdw2SDY2aDhoJkVfY/Q/Trwk2CMSkikE1Oi6gRzxhvhIfiRpHfmIsAhV4EA54TVEX8K6CbHbkA==}
+ '@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.14':
- resolution: {integrity: sha512-vNSB/DYaPOyujVZBg/zUznH9QC142MaTHVmaFlF7uzzfg3CgT9f/l4C0Yi+vU/tbBhxVcXVB90Oohk5+o+ZbWw==}
+ '@aws-sdk/util-user-agent-node@3.973.28':
+ resolution: {integrity: sha512-A2l/PTRzsOS9L8dmZbXtDyJQgeeX+qjqLJ+fr0UU5Dz0AUQMuxgZCPSLKZgUDlHAmLFuk34owdMEvJxmDTBgRg==}
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/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'}
@@ -335,11 +366,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,9 +397,6 @@ packages:
search-insights:
optional: true
- '@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'}
@@ -661,8 +691,8 @@ packages:
cpu: [x64]
os: [win32]
- '@google/genai@1.46.0':
- resolution: {integrity: sha512-ewPMN5JkKfgU5/kdco9ZhXBHDPhVqZpMQqIFQhwsHLf8kyZfx1cNpw1pHo1eV6PGEW7EhIBFi3aYZraFndAXqg==}
+ '@google/genai@1.52.0':
+ resolution: {integrity: sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==}
engines: {node: '>=20.0.0'}
peerDependencies:
'@modelcontextprotocol/sdk': ^1.25.2
@@ -670,8 +700,23 @@ packages:
'@modelcontextprotocol/sdk':
optional: true
- '@homebridge/ciao@1.3.6':
- resolution: {integrity: sha512-2F9N/15Q/GnoBXimr8PFg7fb1QrAQBvuZpaW2kseWOOy14Lzc3yZB1mT9N1Ju/4hlkboU33uHxtOxZkvkPoE/w==}
+ '@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.27.3':
+ resolution: {integrity: sha512-yUKMLliGsGbnxu96YUJ7km7B0zy4PzeH/Jvti5705R/LeKDMqkDV4DckMSt+OrliWQpTwQljHE0QLol5zgxBkg==}
+
+ '@homebridge/ciao@1.3.8':
+ resolution: {integrity: sha512-lNhpCsZVbdbjz2trFjQdzQ3cUIMZQMIMksi7wd3ntTIYgdaGLqT1Ms97DfVIJYHzRuduf56ISvgU8RRLTpK/ng==}
hasBin: true
'@hono/node-server@1.19.11':
@@ -686,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'}
@@ -864,146 +756,138 @@ 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==}
+ '@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.63.1':
- resolution: {integrity: sha512-wjgwY+yfrFO6a9QdAfjWpH7iSrDean6GsKDDMohNcLCy6PreMxHOZvNM0NwJARL1tZoZovr7ikAQfLGFZbnjsw==}
+ '@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.63.1':
- resolution: {integrity: sha512-XSoMyLtuMA7ePK1UBWqSJ/BBdtBdJUHY9nbtnNyG6GeW7Gbgd+iqljIuwmAUf8wlYL981UIfYM/WIPQ6t/dIxw==}
+ '@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.63.1':
- resolution: {integrity: sha512-G5p+eh1EPkFCNaaggX6vRrqttnDscK6npgmEOknoCQXZtch8XNgh9Lf3VJ0A2lZXSgR7IntG5dfXHPH/Ki64wA==}
+ '@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
- '@matrix-org/matrix-sdk-crypto-nodejs@0.4.0':
- resolution: {integrity: sha512-+qqgpn39XFSbsD0dFjssGO9vHEP7sTyfs8yTpt8vuqWpUpF20QMwpCZi0jpYw7GxjErNTsMshopuo8677DfGEA==}
- engines: {node: '>= 22'}
+ '@mistralai/mistralai@2.2.1':
+ resolution: {integrity: sha512-uKU8CZmL2RzYKmplsU01hii4p3pe4HqJefpWNRWXm1Tcm0Sm4xXfwSLIy4k7ZCPlbETCGcp69E7hZs+WOJ5itQ==}
- '@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 +900,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 +924,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 +936,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 +948,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 +962,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 +976,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 +990,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 +1004,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 +1018,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 +1036,16 @@ 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==}
'@oxfmt/binding-android-arm-eabi@0.34.0':
resolution: {integrity: sha512-sqkqjh/Z38l+duOb1HtVqJTAj1grt2ttkobCopC/72+a4Xxz4xUgZPFyQ4HxrYMvyqO/YA0tvM1QbfOu70Gk1Q==}
@@ -1633,198 +1520,174 @@ packages:
'@silvia-odwyer/photon-node@0.3.4':
resolution: {integrity: sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==}
- '@sinclair/typebox@0.34.48':
- resolution: {integrity: sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==}
+ '@slack/bolt@4.7.2':
+ resolution: {integrity: sha512-ALHtaS2iaP2WAWgX08yXsoCxEDitC6AqZs26ot6smXJQzBFMM4slVP+w3blLwzUV551xZ/+9RlBmWHsZDJJ5HA==}
+ engines: {node: '>=18', npm: '>=8.6.0'}
+ peerDependencies:
+ '@types/express': ^5.0.0
- '@sinclair/typebox@0.34.49':
- resolution: {integrity: sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==}
+ '@slack/logger@4.0.1':
+ resolution: {integrity: sha512-6cmdPrV/RYfd2U0mDGiMK8S7OJqpCTm7enMLRR3edccsPX8j7zXTLnaEF4fhxxJJTAIOil6+qZrnUPTuaLvwrQ==}
+ engines: {node: '>= 18', npm: '>= 8.6.0'}
- '@smithy/config-resolver@4.4.13':
- resolution: {integrity: sha512-iIzMC5NmOUP6WL6o8iPBjFhUhBZ9pPjpUpQYWMUFQqKyXXzOftbfK8zcQCz/jFV1Psmf05BK5ypx4K2r4Tnwdg==}
- engines: {node: '>=18.0.0'}
+ '@slack/oauth@3.0.5':
+ resolution: {integrity: sha512-exqFQySKhNDptWYSWhvRUJ4/+ndu2gayIy7vg/JfmJq3wGtGdHk531P96fAZyBm5c1Le3yaPYqv92rL4COlU3A==}
+ engines: {node: '>=18', npm: '>=8.6.0'}
- '@smithy/core@3.23.13':
- resolution: {integrity: sha512-J+2TT9D6oGsUVXVEMvz8h2EmdVnkBiy2auCie4aSJMvKlzUtO5hqjEzXhoCUkIMo7gAYjbQcN0g/MMSXEhDs1Q==}
- engines: {node: '>=18.0.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'}
- '@smithy/credential-provider-imds@4.2.12':
- resolution: {integrity: sha512-cr2lR792vNZcYMriSIj+Um3x9KWrjcu98kn234xA6reOAFMmbRpQMOv8KPgEmLLtx3eldU6c5wALKFqNOhugmg==}
+ '@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/eventstream-codec@4.2.12':
- resolution: {integrity: sha512-FE3bZdEl62ojmy8x4FHqxq2+BuOHlcxiH5vaZ6aqHJr3AIZzwF5jfx8dEiU/X0a8RboyNDjmXjlbr8AdEyLgiA==}
+ '@smithy/core@3.24.3':
+ resolution: {integrity: sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==}
engines: {node: '>=18.0.0'}
- '@smithy/eventstream-serde-browser@4.2.12':
- resolution: {integrity: sha512-XUSuMxlTxV5pp4VpqZf6Sa3vT/Q75FVkLSpSSE3KkWBvAQWeuWt1msTv8fJfgA4/jcJhrbrbMzN1AC/hvPmm5A==}
+ '@smithy/credential-provider-imds@4.3.3':
+ resolution: {integrity: sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==}
engines: {node: '>=18.0.0'}
- '@smithy/eventstream-serde-config-resolver@4.3.12':
- resolution: {integrity: sha512-7epsAZ3QvfHkngz6RXQYseyZYHlmWXSTPOfPmXkiS+zA6TBNo1awUaMFL9vxyXlGdoELmCZyZe1nQE+imbmV+Q==}
+ '@smithy/eventstream-serde-browser@4.3.3':
+ resolution: {integrity: sha512-LXg5yYJPYnVSrpa6LOZ+/wqpI2OlIccy7j5F16EFNYDbXWmnhry/PFRRPyM30H+hJeqfVgckFuvNGnAGCt56cA==}
engines: {node: '>=18.0.0'}
- '@smithy/eventstream-serde-node@4.2.12':
- resolution: {integrity: sha512-D1pFuExo31854eAvg89KMn9Oab/wEeJR6Buy32B49A9Ogdtx5fwZPqBHUlDzaCDpycTFk2+fSQgX689Qsk7UGA==}
+ '@smithy/eventstream-serde-config-resolver@4.4.3':
+ resolution: {integrity: sha512-MdQxEX5SFNc3QmpiLXtcZXsWk4imCfGVN7Ikz9I/XvavypvHT4mqxwo5JHdr/LBKCfAv89+8193ZWlUwDp8YXQ==}
engines: {node: '>=18.0.0'}
- '@smithy/eventstream-serde-universal@4.2.12':
- resolution: {integrity: sha512-+yNuTiyBACxOJUTvbsNsSOfH9G9oKbaJE1lNL3YHpGcuucl6rPZMi3nrpehpVOVR2E07YqFFmtwpImtpzlouHQ==}
+ '@smithy/eventstream-serde-node@4.3.3':
+ resolution: {integrity: sha512-54RbRsw9eVaVnqYUXi3F6nMAPgUyKsBvAKBY2lf+81mIgM7N+yS9V5LYk7yUGbrM789b2e1qBuyDSjX1/Axxcw==}
engines: {node: '>=18.0.0'}
- '@smithy/fetch-http-handler@5.3.15':
- resolution: {integrity: sha512-T4jFU5N/yiIfrtrsb9uOQn7RdELdM/7HbyLNr6uO/mpkj1ctiVs7CihVr51w4LyQlXWDpXFn4BElf1WmQvZu/A==}
+ '@smithy/fetch-http-handler@5.4.3':
+ resolution: {integrity: sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==}
engines: {node: '>=18.0.0'}
- '@smithy/hash-node@4.2.12':
- resolution: {integrity: sha512-QhBYbGrbxTkZ43QoTPrK72DoYviDeg6YKDrHTMJbbC+A0sml3kSjzFtXP7BtbyJnXojLfTQldGdUR0RGD8dA3w==}
+ '@smithy/hash-node@4.3.3':
+ resolution: {integrity: sha512-tSUA38sM7kzMoLhqQ2aCGTwLXovjurz3jjG+a0sxqD4qT/4FhQr/wxMdhCumT70giM+axC1pPjimAHLlEQCfzw==}
engines: {node: '>=18.0.0'}
- '@smithy/invalid-dependency@4.2.12':
- resolution: {integrity: sha512-/4F1zb7Z8LOu1PalTdESFHR0RbPwHd3FcaG1sI3UEIriQTWakysgJr65lc1jj6QY5ye7aFsisajotH6UhWfm/g==}
+ '@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/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==}
+ '@smithy/middleware-content-length@4.3.3':
+ resolution: {integrity: sha512-Up1XAYnj6oxFBypWpkhNpgX+yReQxkKAV/iLaeP0KVLb2oTkmA9X+UJuGBVvEA9uZIN06y0irDi7sBMuTZMVJg==}
engines: {node: '>=18.0.0'}
- '@smithy/middleware-stack@4.2.12':
- resolution: {integrity: sha512-kruC5gRHwsCOuyCd4ouQxYjgRAym2uDlCvQ5acuMtRrcdfg7mFBg6blaxcJ09STpt3ziEkis6bhg1uwrWU7txw==}
+ '@smithy/middleware-endpoint@4.5.3':
+ resolution: {integrity: sha512-p60HGFflWsJC6V9GAYeFgbfORn+9ILx8FqgMa/8PzA0rhIUxF57EKoOR4Irs6oe1oy8RLzhjhcGS8CBtPv/t+Q==}
engines: {node: '>=18.0.0'}
- '@smithy/node-config-provider@4.3.12':
- resolution: {integrity: sha512-tr2oKX2xMcO+rBOjobSwVAkV05SIfUKz8iI53rzxEmgW3GOOPOv0UioSDk+J8OpRQnpnhsO3Af6IEBabQBVmiw==}
+ '@smithy/middleware-retry@4.6.3':
+ resolution: {integrity: sha512-MnfYnJs3cBXK3ZBqbPzXRPHIp+QtgpkX5NogcUOWHPU5GbgTAQSIfPLi91lTcEbkFDcH2YbgjLPQjWeyQ689rA==}
engines: {node: '>=18.0.0'}
- '@smithy/node-http-handler@4.5.1':
- resolution: {integrity: sha512-ejjxdAXjkPIs9lyYyVutOGNOraqUE9v/NjGMKwwFrfOM354wfSD8lmlj8hVwUzQmlLLF4+udhfCX9Exnbmvfzw==}
+ '@smithy/middleware-serde@4.3.3':
+ resolution: {integrity: sha512-RUVCZgn92izDAARs5OJSM2+KWSfTRvQWwN9t0MmiybT3pquRgDx9vD9t/YZjd/5lwcFbsNuPojJSddYQEZGeWw==}
engines: {node: '>=18.0.0'}
- '@smithy/property-provider@4.2.12':
- resolution: {integrity: sha512-jqve46eYU1v7pZ5BM+fmkbq3DerkSluPr5EhvOcHxygxzD05ByDRppRwRPPpFrsFo5yDtCYLKu+kreHKVrvc7A==}
+ '@smithy/middleware-stack@4.3.3':
+ resolution: {integrity: sha512-+BPabWluqxo3EfMMvOgnAmPtWnCSzj+gf5mJ27wTZUbvS0hpdUIU1g80R01bEGKZx4JCi8P58jAXD9FUGMjhwA==}
engines: {node: '>=18.0.0'}
- '@smithy/protocol-http@5.3.12':
- resolution: {integrity: sha512-fit0GZK9I1xoRlR4jXmbLhoN0OdEpa96ul8M65XdmXnxXkuMxM0Y8HDT0Fh0Xb4I85MBvBClOzgSrV1X2s1Hxw==}
+ '@smithy/node-config-provider@4.4.3':
+ resolution: {integrity: sha512-vDtz5OuytrjP4o9GtAOz1JloN003p94utJIQeO0WAjorhpafFFjpbDOrP6btPoCN3UxaU/U84OIEt5dM7ZRRLA==}
engines: {node: '>=18.0.0'}
- '@smithy/querystring-builder@4.2.12':
- resolution: {integrity: sha512-6wTZjGABQufekycfDGMEB84BgtdOE/rCVTov+EDXQ8NHKTUNIp/j27IliwP7tjIU9LR+sSzyGBOXjeEtVgzCHg==}
+ '@smithy/node-http-handler@4.7.3':
+ resolution: {integrity: sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==}
engines: {node: '>=18.0.0'}
- '@smithy/querystring-parser@4.2.12':
- resolution: {integrity: sha512-P2OdvrgiAKpkPNKlKUtWbNZKB1XjPxM086NeVhK+W+wI46pIKdWBe5QyXvhUm3MEcyS/rkLvY8rZzyUdmyDZBw==}
+ '@smithy/property-provider@4.3.3':
+ resolution: {integrity: sha512-nmeVi9Ww/RMyttqj1Dh0PA+iVieKm4dxDlnT6tNP118O/5U/Qqb9b3DV5A3RX+slR/m4/MABSZ2zNfSkpVV8dw==}
engines: {node: '>=18.0.0'}
- '@smithy/service-error-classification@4.2.12':
- resolution: {integrity: sha512-LlP29oSQN0Tw0b6D0Xo6BIikBswuIiGYbRACy5ujw/JgWSzTdYj46U83ssf6Ux0GyNJVivs2uReU8pt7Eu9okQ==}
+ '@smithy/protocol-http@5.4.3':
+ resolution: {integrity: sha512-P16TBD/d8ZcD9MHQ0ubQ9BbOYSd5HZKbHOLsyFWxKk2oBEoghbRFPfGOoqToZX1yrfLITXRylL16EyPP4IzLPg==}
engines: {node: '>=18.0.0'}
- '@smithy/shared-ini-file-loader@4.4.7':
- resolution: {integrity: sha512-HrOKWsUb+otTeo1HxVWeEb99t5ER1XrBi/xka2Wv6NVmTbuCUC1dvlrksdvxFtODLBjsC+PHK+fuy2x/7Ynyiw==}
+ '@smithy/shared-ini-file-loader@4.5.3':
+ resolution: {integrity: sha512-9fgVSJBB1k79oZkT5eLHaPx289LZg8wDi2xNEDKlD2Wy2GpPQfvUhnzJCXEWQxIJ5hhj+peI/todWUFBXhi86w==}
engines: {node: '>=18.0.0'}
- '@smithy/signature-v4@5.3.12':
- resolution: {integrity: sha512-B/FBwO3MVOL00DaRSXfXfa/TRXRheagt/q5A2NM13u7q+sHS59EOVGQNfG7DkmVtdQm5m3vOosoKAXSqn/OEgw==}
+ '@smithy/signature-v4@5.4.3':
+ resolution: {integrity: sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==}
engines: {node: '>=18.0.0'}
- '@smithy/smithy-client@4.12.8':
- resolution: {integrity: sha512-aJaAX7vHe5i66smoSSID7t4rKY08PbD8EBU7DOloixvhOozfYWdcSYE4l6/tjkZ0vBZhGjheWzB2mh31sLgCMA==}
+ '@smithy/smithy-client@4.13.3':
+ resolution: {integrity: sha512-Z8mQ+YryjP5krDadV6unnp5035L4S1brafXpTiRmjPweKSaQ6X9CYDYWvmEggXjDIa1oufX/2a/bdwu8EIz/lw==}
engines: {node: '>=18.0.0'}
- '@smithy/types@4.13.1':
- resolution: {integrity: sha512-787F3yzE2UiJIQ+wYW1CVg2odHjmaWLGksnKQHUrK/lYZSEcy1msuLVvxaR/sI2/aDe9U+TBuLsXnr3vod1g0g==}
+ '@smithy/types@4.14.2':
+ resolution: {integrity: sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==}
engines: {node: '>=18.0.0'}
- '@smithy/url-parser@4.2.12':
- resolution: {integrity: sha512-wOPKPEpso+doCZGIlr+e1lVI6+9VAKfL4kZWFgzVgGWY2hZxshNKod4l2LXS3PRC9otH/JRSjtEHqQ/7eLciRA==}
+ '@smithy/url-parser@4.3.3':
+ resolution: {integrity: sha512-TsMTAOnjuMOv1zJBw8cfYGWhopyc3og8tZX/KuyCPjg7V3ji3f4YjFOVu843UjBmrfS/+X6kwFv5ZKg7sSm1bQ==}
engines: {node: '>=18.0.0'}
- '@smithy/util-base64@4.3.2':
- resolution: {integrity: sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==}
+ '@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.2.2':
- resolution: {integrity: sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==}
+ '@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.2.3':
- resolution: {integrity: sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==}
+ '@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-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==}
+ '@smithy/util-defaults-mode-browser@4.4.3':
+ resolution: {integrity: sha512-Q60hxKkMEkmBsOEzxlMWEymBWov0dtWGgoJhOUs6mE8k2FDPjK8NlsRdMkmO80n2pwzreHtrYcX5jiRP7ZkP3w==}
engines: {node: '>=18.0.0'}
- '@smithy/util-hex-encoding@4.2.2':
- resolution: {integrity: sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==}
+ '@smithy/util-defaults-mode-node@4.3.3':
+ resolution: {integrity: sha512-RYj+8gr95WiiBqvVghoRvL12NS9ryvLyufp7FOs7EzKwGX0W5gOVlXdCrFkJScSf8gxdjQMRyIZ3Y82/MvXQ3Q==}
engines: {node: '>=18.0.0'}
- '@smithy/util-middleware@4.2.12':
- resolution: {integrity: sha512-Er805uFUOvgc0l8nv0e0su0VFISoxhJ/AwOn3gL2NWNY2LUEldP5WtVcRYSQBcjg0y9NfG8JYrCJaYDpupBHJQ==}
+ '@smithy/util-endpoints@3.5.3':
+ resolution: {integrity: sha512-2JqSmzQtKDKqBckLl/9NXTL1fY+zQBU5fNGMpud7AT65vql0tVFhb2UEZNZmLSHayLeD+X/Qzn84oXw5KS+KSQ==}
engines: {node: '>=18.0.0'}
- '@smithy/util-retry@4.2.13':
- resolution: {integrity: sha512-qQQsIvL0MGIbUjeSrg0/VlQ3jGNKyM3/2iU3FPNgy01z+Sp4OvcaxbgIoFOTvB61ZoohtutuOvOcgmhbD0katQ==}
+ '@smithy/util-middleware@4.3.3':
+ resolution: {integrity: sha512-8NZwlQ+nyAIWn9YZxH14FC8ca0i6ZGW1aJyPjD+zMZz3k9jOhXXKhdCSRvjmcSYLW42uhbrxavXqMkrTKHyY3A==}
engines: {node: '>=18.0.0'}
- '@smithy/util-stream@4.5.21':
- resolution: {integrity: sha512-KzSg+7KKywLnkoKejRtIBXDmwBfjGvg1U1i/etkC7XSWUyFCoLno1IohV2c74IzQqdhX5y3uE44r/8/wuK+A7Q==}
+ '@smithy/util-retry@4.4.3':
+ resolution: {integrity: sha512-8RJXeU5lEhdNfXm4XAuHlf6VtNzd279Z2FJZSR7VaELYCR46ffgjJBSjc+3UAy7V1YqBOLV0G9gWhLB/nA44nA==}
engines: {node: '>=18.0.0'}
- '@smithy/util-uri-escape@4.2.2':
- resolution: {integrity: sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==}
+ '@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.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==}
+ '@smithy/util-utf8@4.3.3':
+ resolution: {integrity: sha512-c1QpRBn3aMsoqE64dd4Imgjy8Pynfw+eR7GkjElquxUFSnezwYVaOFm8JcYa+Bo/5ssbEyPKcT3+4bmrWYh6eQ==}
engines: {node: '>=18.0.0'}
'@telegraf/types@7.1.0':
@@ -1840,21 +1703,36 @@ packages:
'@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/events@3.0.3':
- resolution: {integrity: sha512-trOc4AAUThEz9hapPtSd7wf5tiQKvTtu5b371UxXdTuqzIh0ArcRspRP0i0Viu+LXstIQ1z96t1nsPxT9ol01g==}
+ '@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==}
@@ -1870,21 +1748,36 @@ packages:
'@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/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==}
@@ -2036,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:
@@ -2048,16 +1949,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'}
@@ -2083,6 +1981,9 @@ packages:
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'}
@@ -2100,18 +2001,18 @@ 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}
- 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==}
+ basic-ftp@5.3.1:
+ resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==}
engines: {node: '>=10.0.0'}
bignumber.js@9.3.1:
@@ -2123,6 +2024,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 +2034,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,8 +2044,9 @@ packages:
resolution: {integrity: sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw==}
engines: {node: 20 || >=22}
- bs58@6.0.0:
- resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==}
+ 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==}
@@ -2174,6 +2082,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==}
@@ -2212,6 +2124,9 @@ packages:
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==}
@@ -2293,6 +2208,10 @@ packages:
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'}
@@ -2302,14 +2221,32 @@ 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'}
+ 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'}
@@ -2322,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==}
@@ -2333,6 +2266,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==}
@@ -2356,8 +2292,8 @@ packages:
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:
@@ -2436,6 +2372,10 @@ 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'}
@@ -2468,9 +2408,11 @@ 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'}
+ 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==}
@@ -2505,14 +2447,23 @@ packages:
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==}
+ fast-xml-builder@1.2.0:
+ resolution: {integrity: sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==}
+
+ fast-xml-parser@5.7.3:
+ resolution: {integrity: sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==}
hasBin: true
fd-slicer@1.1.0:
@@ -2535,14 +2486,18 @@ packages:
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==}
@@ -2555,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'}
@@ -2623,6 +2587,10 @@ packages:
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
@@ -2632,6 +2600,14 @@ 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'}
@@ -2655,6 +2631,10 @@ packages:
graceful-fs@4.2.11:
resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==}
+ 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'}
@@ -2663,6 +2643,9 @@ packages:
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'}
@@ -2684,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:
@@ -2715,10 +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'}
@@ -2740,22 +2739,25 @@ 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'}
- 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-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'}
- is-network-error@1.3.1:
- resolution: {integrity: sha512-6QCxa49rQbmUWLfk0nuGqzql9U8uaV2H6279bRErPBHe/109hCzsLUBUHfbEtvLIHBd6hyXbgedBSHevm43Edw==}
- engines: {node: '>=16'}
-
is-promise@4.0.0:
resolution: {integrity: sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==}
@@ -2792,8 +2794,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:
@@ -2823,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==}
@@ -2854,18 +2860,39 @@ 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==}
+ lodash.includes@4.3.0:
+ resolution: {integrity: sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==}
- lop@0.4.2:
- resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==}
+ lodash.isboolean@3.0.3:
+ resolution: {integrity: sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==}
- loupe@3.2.1:
- resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
+ 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==}
+
+ lop@0.4.2:
+ resolution: {integrity: sha512-RefILVDQ4DKoRZsJ4Pj22TxE3omDO47yFpkIBoDKzkqPRISs5U1cnAdg/5583YPkWPaLIYHOKRMQSvjFsO26cw==}
+
+ loupe@3.2.1:
+ resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==}
lru-cache@10.4.3:
resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==}
@@ -2905,20 +2932,14 @@ 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'}
- 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,14 +2985,20 @@ packages:
resolution: {integrity: sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==}
engines: {node: '>=18'}
- minimatch@10.2.4:
- resolution: {integrity: sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==}
+ minimalistic-assert@1.0.1:
+ resolution: {integrity: sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==}
+
+ minimatch@10.2.5:
+ resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==}
engines: {node: 18 || 20 || >=22}
minimatch@9.0.6:
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'}
@@ -3005,20 +3032,19 @@ 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==}
+ 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}
+
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 +3062,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,9 +3077,9 @@ 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'}
+ 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==}
@@ -3076,17 +3103,23 @@ 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
+ 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'}
@@ -3095,10 +3128,6 @@ packages:
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,26 +3147,56 @@ 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'}
+
+ p-locate@4.1.0:
+ 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-retry@7.1.1:
- resolution: {integrity: sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w==}
- engines: {node: '>=20'}
+ 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==}
@@ -3160,8 +3219,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,9 +3262,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'}
+ pdfjs-dist@5.7.284:
+ resolution: {integrity: sha512-h4EdYQczmGhbOlqc3PPZwxevn7ApdWPbovAuWXOB/DjIyigSnwfy2oze7c6mRcSr9XgLp3eN3EeL4DyySTPMFw==}
+ engines: {node: '>=22.13.0 || >=24'}
pend@1.2.0:
resolution: {integrity: sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==}
@@ -3220,11 +3283,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.59.1:
+ resolution: {integrity: sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==}
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}
@@ -3257,9 +3324,17 @@ packages:
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==}
@@ -3267,14 +3342,18 @@ packages:
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 +3386,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'}
@@ -3340,10 +3422,6 @@ packages:
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==}
@@ -3356,20 +3434,23 @@ 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'}
+ 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==}
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'}
@@ -3414,12 +3495,16 @@ packages:
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.7:
- resolution: {integrity: sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==}
+ 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:
@@ -3443,33 +3528,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 +3591,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==}
@@ -3578,9 +3663,22 @@ packages:
resolution: {integrity: sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==}
engines: {node: '>=14.16'}
+ tokenjuice@0.7.0:
+ resolution: {integrity: sha512-RZIyFmzztf/8V4q1cUS5L+q8UISMSfsjzh4UoWVxQbE7/zX91SfNmHpNqopqyB4oc5hwH4XqC9O/yakVzJCu8g==}
+ 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==}
@@ -3594,10 +3692,21 @@ 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.37:
+ resolution: {integrity: sha512-jb7jp6KvOvvy5sd+11AfJ0/e0F0AS9RcOXd55oGi2ZnRHIGmFvrTaNF+ZidRmGBmmNTkM5KKl0Z37KzxJ+owEQ==}
+
typescript@5.9.3:
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
engines: {node: '>=14.17'}
@@ -3619,12 +3728,13 @@ 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==}
+ undici@7.25.0:
+ resolution: {integrity: sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==}
engines: {node: '>=20.18.1'}
- unhomoglyph@1.0.6:
- resolution: {integrity: sha512-7uvcWI3hWshSADBu4JpnyYbTVc7YlhF5GDW/oPD5AxIxl34k4wXR3WDkPnzLxkN32LiTCTKMQLtKVZiwki3zGg==}
+ undici@8.2.0:
+ resolution: {integrity: sha512-Z+4Hx9GE26Lh9Upwfnc8C7SsrpBPGaM/Gm6kMFtiG7c+5IvQKlXi/t+9x9DrrCh29cww5TSP9YdVaBcnLDs5fQ==}
+ engines: {node: '>=22.19.0'}
unist-util-is@6.0.1:
resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==}
@@ -3648,12 +3758,13 @@ packages:
util-deprecate@1.0.2:
resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==}
- uuid@13.0.0:
- resolution: {integrity: sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==}
+ 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:
@@ -3790,16 +3901,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 +3932,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 +3959,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 +3971,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,11 +3990,15 @@ 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@18.1.3:
+ resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+ engines: {node: '>=6'}
+
yargs-parser@20.2.9:
resolution: {integrity: sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==}
engines: {node: '>=10'}
@@ -3870,6 +4007,10 @@ packages:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
+ yargs@15.4.1:
+ 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'}
@@ -3890,17 +4031,17 @@ packages:
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.0(zod@4.4.3)':
dependencies:
- zod: 4.3.6
+ zod: 4.4.3
'@algolia/abtesting@1.16.0':
dependencies:
@@ -4019,15 +4160,21 @@ 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.4.3
+
+ '@anthropic-ai/sdk@0.93.0(zod@4.4.3)':
dependencies:
json-schema-to-ts: 3.1.1
optionalDependencies:
- zod: 4.3.6
+ zod: 4.4.3
- '@anthropic-ai/vertex-sdk@0.14.4(zod@4.3.6)':
+ '@anthropic-ai/vertex-sdk@0.16.1(zod@4.4.3)':
dependencies:
- '@anthropic-ai/sdk': 0.73.0(zod@4.3.6)
+ '@anthropic-ai/sdk': 0.93.0(zod@4.4.3)
google-auth-library: 9.15.1
transitivePeerDependencies:
- encoding
@@ -4037,7 +4184,7 @@ snapshots:
'@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 +4192,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 +4200,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,421 +4209,409 @@ 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.1042.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-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
- transitivePeerDependencies:
- - aws-crt
- '@aws-sdk/client-bedrock@3.1020.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/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
+ '@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/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/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/credential-provider-env@3.972.24':
+ '@aws-sdk/client-cognito-identity@3.1049.0':
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-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/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
+ '@aws-sdk/core@3.974.12':
+ dependencies:
+ '@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-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
+ '@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
- transitivePeerDependencies:
- - aws-crt
- '@aws-sdk/credential-provider-login@3.972.28':
+ '@aws-sdk/credential-provider-env@3.972.38':
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
+ '@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
- 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/credential-provider-http@3.972.40':
+ dependencies:
+ '@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':
+ '@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-login@3.972.42':
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/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/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-sso@3.972.28':
+ '@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
+
+ '@aws-sdk/credential-provider-process@3.972.38':
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/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':
+ '@aws-sdk/credential-provider-sso@3.972.42':
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/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
- transitivePeerDependencies:
- - aws-crt
- '@aws-sdk/eventstream-handler-node@3.972.11':
+ '@aws-sdk/credential-provider-web-identity@3.972.42':
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/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-eventstream@3.972.8':
+ '@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.6
- '@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-host-header@3.972.8':
+ '@aws-sdk/middleware-eventstream@3.972.12':
dependencies:
- '@aws-sdk/types': 3.973.6
- '@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-logger@3.972.8':
+ '@aws-sdk/middleware-host-header@3.972.13':
dependencies:
- '@aws-sdk/types': 3.973.6
- '@smithy/types': 4.13.1
+ '@aws-sdk/core': 3.974.12
tslib: 2.8.1
- '@aws-sdk/middleware-recursion-detection@3.972.9':
+ '@aws-sdk/middleware-logger@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/core': 3.974.12
tslib: 2.8.1
- '@aws-sdk/middleware-user-agent@3.972.28':
+ '@aws-sdk/middleware-recursion-detection@3.972.14':
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
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
+ '@aws-sdk/middleware-user-agent@3.972.42':
+ dependencies:
+ '@aws-sdk/core': 3.974.12
tslib: 2.8.1
- '@aws-sdk/nested-clients@3.996.18':
+ '@aws-sdk/middleware-websocket@3.972.20':
+ dependencies:
+ '@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/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
+ '@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/region-config-resolver@3.972.10':
+ '@aws-sdk/region-config-resolver@3.972.16':
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
+ '@aws-sdk/core': 3.974.12
tslib: 2.8.1
- '@aws-sdk/token-providers@3.1014.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.1020.0':
+ '@aws-sdk/token-providers@3.1042.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/property-provider': 4.3.3
+ '@smithy/shared-ini-file-loader': 4.5.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
+ '@smithy/types': 4.14.2
tslib: 2.8.1
- '@aws-sdk/util-endpoints@3.996.5':
+ '@aws-sdk/util-endpoints@3.996.11':
dependencies:
- '@aws-sdk/types': 3.973.6
- '@smithy/types': 4.13.1
- '@smithy/url-parser': 4.2.12
- '@smithy/util-endpoints': 3.3.3
+ '@aws-sdk/core': 3.974.12
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@aws-sdk/util-format-url@3.972.8':
+ '@aws-sdk/util-format-url@3.972.14':
dependencies:
- '@aws-sdk/types': 3.973.6
- '@smithy/querystring-builder': 4.2.12
- '@smithy/types': 4.13.1
+ '@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.8':
+ '@aws-sdk/util-user-agent-browser@3.972.13':
dependencies:
- '@aws-sdk/types': 3.973.6
- '@smithy/types': 4.13.1
- bowser: 2.14.1
+ '@aws-sdk/core': 3.974.12
tslib: 2.8.1
- '@aws-sdk/util-user-agent-node@3.973.14':
+ '@aws-sdk/util-user-agent-node@3.973.28':
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
+ '@aws-sdk/core': 3.974.12
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/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': {}
@@ -4502,13 +4637,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,11 +4673,6 @@ snapshots:
transitivePeerDependencies:
- '@algolia/client-search'
- '@emnapi/runtime@1.9.1':
- dependencies:
- tslib: 2.8.1
- optional: true
-
'@esbuild/aix-ppc64@0.21.5':
optional: true
@@ -4687,20 +4820,32 @@ 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.52.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':
+ '@grammyjs/runner@2.0.3(grammy@1.43.0)':
+ dependencies:
+ abort-controller: 3.0.0
+ grammy: 1.43.0
+
+ '@grammyjs/transformer-throttler@1.2.1(grammy@1.43.0)':
+ dependencies:
+ bottleneck: 2.19.5
+ grammy: 1.43.0
+
+ '@grammyjs/types@3.27.3': {}
+
+ '@homebridge/ciao@1.3.8':
dependencies:
debug: 4.4.3
fast-deep-equal: 3.1.3
@@ -4709,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:
@@ -4719,102 +4864,6 @@ snapshots:
'@iconify/types@2.0.0': {}
- '@img/colour@1.1.0': {}
-
- '@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
@@ -4844,83 +4893,75 @@ 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':
+ '@mariozechner/clipboard-win32-x64-msvc@0.3.6':
optional: true
- '@mariozechner/clipboard@0.3.2':
+ '@mariozechner/clipboard@0.3.6':
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-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
'@mariozechner/jiti@2.6.5':
@@ -4928,50 +4969,46 @@ snapshots:
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)':
+ '@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.63.1(@modelcontextprotocol/sdk@1.28.0(zod@4.3.6))(ws@8.20.0)(zod@4.3.6)
+ '@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'
- - 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)':
+ '@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.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)
+ '@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.0)(zod@4.3.6)
+ openai: 6.26.0(ws@8.20.1)(zod@4.4.3)
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)
+ typebox: 1.1.37
+ undici: 7.25.0
+ 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-coding-agent@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.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.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
+ '@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
- ajv: 8.18.0
chalk: 5.6.2
cli-highlight: 2.1.11
diff: 8.0.3
@@ -4981,23 +5018,24 @@ snapshots:
hosted-git-info: 9.0.2
ignore: 7.0.5
marked: 15.0.12
- minimatch: 10.2.4
+ minimatch: 10.2.5
proper-lockfile: 4.1.2
strip-ansi: 7.1.2
- undici: 7.24.7
- yaml: 2.8.3
+ typebox: 1.1.37
+ undici: 7.25.0
+ uuid: 14.0.0
+ yaml: 2.9.0
optionalDependencies:
- '@mariozechner/clipboard': 0.3.2
+ '@mariozechner/clipboard': 0.3.6
transitivePeerDependencies:
- '@modelcontextprotocol/sdk'
- - aws-crt
- bufferutil
- supports-color
- utf-8-validate
- ws
- zod
- '@mariozechner/pi-tui@0.63.1':
+ '@mariozechner/pi-tui@0.73.0':
dependencies:
'@types/mime-types': 2.1.4
chalk: 5.6.2
@@ -5007,30 +5045,20 @@ snapshots:
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':
+ '@mistralai/mistralai@2.2.1':
dependencies:
- ws: 8.20.0
- zod: 4.3.6
- zod-to-json-schema: 3.25.1(zod@4.3.6)
+ ws: 8.20.1
+ zod: 4.4.3
+ zod-to-json-schema: 3.25.1(zod@4.4.3)
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)
+ '@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
cors: 2.8.6
cross-spawn: 7.0.6
@@ -5038,79 +5066,94 @@ 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
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 +5169,7 @@ 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':
- 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
+ '@nodable/entities@2.1.0': {}
'@oxfmt/binding-android-arm-eabi@0.34.0':
optional: true
@@ -5415,293 +5446,246 @@ snapshots:
'@silvia-odwyer/photon-node@0.3.4': {}
- '@sinclair/typebox@0.34.48': {}
-
- '@sinclair/typebox@0.34.49': {}
-
- '@smithy/config-resolver@4.4.13':
+ '@slack/bolt@4.7.2(@types/express@5.0.6)':
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':
- dependencies:
- '@aws-crypto/crc32': 5.2.0
- '@smithy/types': 4.13.1
- '@smithy/util-hex-encoding': 4.2.2
- tslib: 2.8.1
+ '@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
- '@smithy/eventstream-serde-browser@4.2.12':
+ '@slack/logger@4.0.1':
dependencies:
- '@smithy/eventstream-serde-universal': 4.2.12
- '@smithy/types': 4.13.1
- tslib: 2.8.1
+ '@types/node': 25.2.0
- '@smithy/eventstream-serde-config-resolver@4.3.12':
+ '@slack/oauth@3.0.5':
dependencies:
- '@smithy/types': 4.13.1
- tslib: 2.8.1
+ '@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
- '@smithy/eventstream-serde-node@4.2.12':
+ '@slack/socket-mode@2.0.7':
dependencies:
- '@smithy/eventstream-serde-universal': 4.2.12
- '@smithy/types': 4.13.1
- tslib: 2.8.1
+ '@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
- '@smithy/eventstream-serde-universal@4.2.12':
- dependencies:
- '@smithy/eventstream-codec': 4.2.12
- '@smithy/types': 4.13.1
- tslib: 2.8.1
+ '@slack/types@2.21.1': {}
- '@smithy/fetch-http-handler@5.3.15':
+ '@slack/web-api@7.16.0':
dependencies:
- '@smithy/protocol-http': 5.3.12
- '@smithy/querystring-builder': 4.2.12
- '@smithy/types': 4.13.1
- '@smithy/util-base64': 4.3.2
- tslib: 2.8.1
+ '@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/hash-node@4.2.12':
+ '@smithy/config-resolver@4.5.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
tslib: 2.8.1
- '@smithy/invalid-dependency@4.2.12':
+ '@smithy/core@3.24.3':
dependencies:
- '@smithy/types': 4.13.1
+ '@aws-crypto/crc32': 5.2.0
+ '@smithy/types': 4.14.2
tslib: 2.8.1
- '@smithy/is-array-buffer@2.2.0':
+ '@smithy/credential-provider-imds@4.3.3':
dependencies:
+ '@smithy/core': 3.24.3
+ '@smithy/types': 4.14.2
tslib: 2.8.1
- '@smithy/is-array-buffer@4.2.2':
+ '@smithy/eventstream-serde-browser@4.3.3':
dependencies:
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/middleware-content-length@4.2.12':
+ '@smithy/eventstream-serde-config-resolver@4.4.3':
dependencies:
- '@smithy/protocol-http': 5.3.12
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/middleware-endpoint@4.4.28':
+ '@smithy/eventstream-serde-node@4.3.3':
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
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/middleware-retry@4.4.46':
+ '@smithy/fetch-http-handler@5.4.3':
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
+ '@smithy/core': 3.24.3
+ '@smithy/types': 4.14.2
tslib: 2.8.1
- '@smithy/middleware-serde@4.2.16':
+ '@smithy/hash-node@4.3.3':
dependencies:
- '@smithy/core': 3.23.13
- '@smithy/protocol-http': 5.3.12
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/middleware-stack@4.2.12':
+ '@smithy/invalid-dependency@4.3.3':
dependencies:
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/node-config-provider@4.3.12':
+ '@smithy/is-array-buffer@2.2.0':
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':
+ '@smithy/middleware-content-length@4.3.3':
dependencies:
- '@smithy/protocol-http': 5.3.12
- '@smithy/querystring-builder': 4.2.12
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/property-provider@4.2.12':
+ '@smithy/middleware-endpoint@4.5.3':
dependencies:
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/protocol-http@5.3.12':
+ '@smithy/middleware-retry@4.6.3':
dependencies:
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/querystring-builder@4.2.12':
+ '@smithy/middleware-serde@4.3.3':
dependencies:
- '@smithy/types': 4.13.1
- '@smithy/util-uri-escape': 4.2.2
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/querystring-parser@4.2.12':
+ '@smithy/middleware-stack@4.3.3':
dependencies:
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/service-error-classification@4.2.12':
+ '@smithy/node-config-provider@4.4.3':
dependencies:
- '@smithy/types': 4.13.1
-
- '@smithy/shared-ini-file-loader@4.4.7':
- dependencies:
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
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/property-provider@4.3.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
tslib: 2.8.1
- '@smithy/types@4.13.1':
+ '@smithy/protocol-http@5.4.3':
dependencies:
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/url-parser@4.2.12':
+ '@smithy/shared-ini-file-loader@4.5.3':
dependencies:
- '@smithy/querystring-parser': 4.2.12
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/util-base64@4.3.2':
+ '@smithy/signature-v4@5.4.3':
dependencies:
- '@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/util-body-length-browser@4.2.2':
+ '@smithy/smithy-client@4.13.3':
dependencies:
+ '@smithy/core': 3.24.3
+ '@smithy/types': 4.14.2
tslib: 2.8.1
- '@smithy/util-body-length-node@4.2.3':
+ '@smithy/types@4.14.2':
dependencies:
tslib: 2.8.1
- '@smithy/util-buffer-from@2.2.0':
+ '@smithy/url-parser@4.3.3':
dependencies:
- '@smithy/is-array-buffer': 2.2.0
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/util-buffer-from@4.2.2':
+ '@smithy/util-base64@4.4.3':
dependencies:
- '@smithy/is-array-buffer': 4.2.2
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/util-config-provider@4.2.2':
+ '@smithy/util-body-length-browser@4.3.3':
dependencies:
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/util-defaults-mode-browser@4.3.44':
+ '@smithy/util-body-length-node@4.3.3':
dependencies:
- '@smithy/property-provider': 4.2.12
- '@smithy/smithy-client': 4.12.8
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/util-defaults-mode-node@4.2.48':
+ '@smithy/util-buffer-from@2.2.0':
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
+ '@smithy/is-array-buffer': 2.2.0
tslib: 2.8.1
- '@smithy/util-endpoints@3.3.3':
+ '@smithy/util-defaults-mode-browser@4.4.3':
dependencies:
- '@smithy/node-config-provider': 4.3.12
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/util-hex-encoding@4.2.2':
+ '@smithy/util-defaults-mode-node@4.3.3':
dependencies:
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/util-middleware@4.2.12':
+ '@smithy/util-endpoints@3.5.3':
dependencies:
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/util-retry@4.2.13':
+ '@smithy/util-middleware@4.3.3':
dependencies:
- '@smithy/service-error-classification': 4.2.12
- '@smithy/types': 4.13.1
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/util-stream@4.5.21':
+ '@smithy/util-retry@4.4.3':
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
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@smithy/util-uri-escape@4.2.2':
+ '@smithy/util-stream@4.6.3':
dependencies:
+ '@smithy/core': 3.24.3
tslib: 2.8.1
'@smithy/util-utf8@2.3.0':
@@ -5709,17 +5693,12 @@ snapshots:
'@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':
+ '@smithy/util-utf8@4.3.3':
dependencies:
+ '@smithy/core': 3.24.3
tslib: 2.8.1
- '@telegraf/types@7.1.0':
- optional: true
+ '@telegraf/types@7.1.0': {}
'@tokenizer/inflate@0.4.1':
dependencies:
@@ -5732,21 +5711,48 @@ snapshots:
'@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/events@3.0.3': {}
+ '@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':
@@ -5762,20 +5768,35 @@ snapshots:
'@types/mime-types@2.1.4': {}
- '@types/node@24.12.0':
- dependencies:
- undici-types: 7.16.0
+ '@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
@@ -5788,7 +5809,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 +5824,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 +5836,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 +5951,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 +5960,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,20 +5977,27 @@ snapshots:
abort-controller@3.0.0:
dependencies:
event-target-shim: 5.0.1
- optional: true
accepts@2.0.0:
dependencies:
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: {}
- ajv-formats@3.0.1(ajv@8.18.0):
+ agent-base@9.0.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 +6021,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: {}
@@ -6012,6 +6039,13 @@ snapshots:
argparse@2.0.1: {}
+ asn1.js@5.4.1:
+ dependencies:
+ 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-types@0.13.4:
@@ -6034,13 +6068,21 @@ snapshots:
transitivePeerDependencies:
- debug
- balanced-match@4.0.3: {}
+ 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
- base-x@5.0.1: {}
+ balanced-match@4.0.3: {}
base64-js@1.5.1: {}
- basic-ftp@5.2.0: {}
+ basic-ftp@5.3.1: {}
bignumber.js@9.3.1: {}
@@ -6048,6 +6090,8 @@ snapshots:
bluebird@3.4.7: {}
+ bn.js@4.12.3: {}
+
body-parser@2.2.2:
dependencies:
bytes: 3.1.2
@@ -6064,31 +6108,30 @@ 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:
+ brace-expansion@5.0.6:
dependencies:
- base-x: 5.0.1
+ balanced-match: 4.0.3
- buffer-alloc-unsafe@1.1.0:
- optional: true
+ buffer-alloc-unsafe@1.1.0: {}
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-fill@1.0.0: {}
buffer-from@1.1.2: {}
@@ -6106,6 +6149,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:
@@ -6144,6 +6189,12 @@ snapshots:
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
@@ -6215,32 +6266,55 @@ snapshots:
data-uri-to-buffer@6.0.2: {}
+ data-uri-to-buffer@8.0.0: {}
+
debug@4.4.3:
dependencies:
ms: 2.1.3
+ decamelize@1.2.0: {}
+
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: {}
-
devlop@1.1.0:
dependencies:
dequal: 2.0.3
diff@8.0.3: {}
+ dijkstrajs@1.0.3: {}
+
dingbat-to-unicode@1.0.1: {}
dingtalk-stream@2.1.4:
@@ -6271,10 +6345,9 @@ snapshots:
domelementtype: 2.3.0
domhandler: 5.0.3
- dotenv@16.6.1:
- optional: true
+ dotenv@16.6.1: {}
- dotenv@17.3.1: {}
+ dotenv@17.4.2: {}
duck@0.1.12:
dependencies:
@@ -6386,6 +6459,8 @@ snapshots:
escape-html@1.0.3: {}
+ escape-string-regexp@4.0.0: {}
+
escodegen@2.1.0:
dependencies:
esprima: 4.0.1
@@ -6408,10 +6483,11 @@ snapshots:
etag@1.8.1: {}
- event-target-shim@5.0.1:
- optional: true
+ event-target-shim@5.0.1: {}
+
+ eventemitter3@4.0.7: {}
- events@3.3.0: {}
+ eventemitter3@5.0.4: {}
eventsource-parser@3.0.6: {}
@@ -6473,17 +6549,29 @@ snapshots:
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:
+ fast-string-width: 3.0.2
+
+ fast-xml-builder@1.2.0:
dependencies:
- path-expression-matcher: 1.2.0
+ path-expression-matcher: 1.5.0
+ xml-naming: 0.1.0
- fast-xml-parser@5.5.8:
+ fast-xml-parser@5.7.3:
dependencies:
- fast-xml-builder: 1.1.4
- path-expression-matcher: 1.2.0
- strnum: 2.2.1
+ '@nodable/entities': 2.1.0
+ fast-xml-builder: 1.2.0
+ path-expression-matcher: 1.5.0
+ strnum: 2.3.0
fd-slicer@1.1.0:
dependencies:
@@ -6507,7 +6595,7 @@ snapshots:
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 +6615,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
@@ -6535,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
@@ -6625,12 +6720,20 @@ snapshots:
get-uri@6.0.5:
dependencies:
- basic-ftp: 5.2.0
+ 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
@@ -6642,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
@@ -6677,6 +6792,16 @@ snapshots:
graceful-fs@4.2.11: {}
+ grammy@1.43.0:
+ dependencies:
+ '@grammyjs/types': 3.27.3
+ abort-controller: 3.0.0
+ debug: 4.4.3
+ node-fetch: 2.7.0
+ transitivePeerDependencies:
+ - encoding
+ - supports-color
+
gtoken@7.1.0:
dependencies:
gaxios: 6.7.1
@@ -6687,6 +6812,10 @@ snapshots:
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:
@@ -6717,7 +6846,7 @@ snapshots:
highlight.js@10.7.3: {}
- hono@4.12.9: {}
+ hono@4.12.10: {}
hookable@5.5.3: {}
@@ -6753,6 +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
@@ -6760,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
@@ -6774,13 +6926,15 @@ snapshots:
ip-address@10.1.0: {}
+ ip-address@10.2.0: {}
+
ipaddr.js@1.9.1: {}
- ipaddr.js@2.3.0: {}
+ ipaddr.js@2.4.0: {}
- is-fullwidth-code-point@3.0.0: {}
+ is-electron@2.2.2: {}
- is-network-error@1.3.1: {}
+ is-fullwidth-code-point@3.0.0: {}
is-promise@4.0.0: {}
@@ -6819,7 +6973,7 @@ snapshots:
optionalDependencies:
'@pkgjs/parseargs': 0.11.0
- jiti@2.6.1: {}
+ jiti@2.7.0: {}
jose@6.2.2: {}
@@ -6842,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
@@ -6860,7 +7027,8 @@ 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
@@ -6881,7 +7049,23 @@ snapshots:
dependencies:
uc.micro: 2.1.0
- loglevel@1.9.2: {}
+ locate-path@5.0.0:
+ 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: {}
@@ -6939,31 +7123,11 @@ snapshots:
marked@15.0.12: {}
- math-intrinsics@1.1.0: {}
-
- matrix-events-sdk@0.0.1: {}
-
- matrix-js-sdk@41.2.0:
+ matcher@4.0.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
+ escape-string-regexp: 4.0.0
- matrix-widget-api@1.17.0:
- dependencies:
- '@types/events': 3.0.3
- events: 3.3.0
+ math-intrinsics@1.1.0: {}
mdast-util-to-hast@13.2.1:
dependencies:
@@ -7012,14 +7176,18 @@ snapshots:
dependencies:
mime-db: 1.54.0
- minimatch@10.2.4:
+ minimalistic-assert@1.0.1: {}
+
+ minimatch@10.2.5:
dependencies:
- brace-expansion: 5.0.2
+ brace-expansion: 5.0.6
minimatch@9.0.6:
dependencies:
brace-expansion: 5.0.2
+ minimist@1.2.8: {}
+
minipass@7.1.3: {}
minisearch@7.2.0: {}
@@ -7030,8 +7198,7 @@ snapshots:
mitt@3.0.1: {}
- mri@1.2.0:
- optional: true
+ mri@1.2.0: {}
ms@2.1.3: {}
@@ -7045,17 +7212,16 @@ snapshots:
negotiator@1.0.0: {}
- netmask@2.0.2: {}
+ netmask@2.1.1: {}
- node-domexception@1.0.0: {}
+ node-addon-api@8.7.0: {}
- node-downloader-helper@2.1.11:
- optional: true
+ node-domexception@1.0.0: {}
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 +7238,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,9 +7248,7 @@ snapshots:
object-inspect@1.13.4: {}
- oidc-client-ts@3.5.0:
- dependencies:
- jwt-decode: 4.0.0
+ object-keys@1.1.1: {}
on-finished@2.4.1:
dependencies:
@@ -7101,72 +7264,87 @@ 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.7(@types/express@5.0.6):
+ dependencies:
+ '@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
+ '@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
- '@napi-rs/canvas': 0.1.97
- '@sinclair/typebox': 0.34.48
- ajv: 8.18.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
- 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
+ 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
linkedom: 0.18.12
- long: 5.3.2
markdown-it: 14.1.1
- matrix-js-sdk: 41.2.0
+ minimatch: 10.2.5
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
+ openai: 6.38.0(ws@8.20.1)(zod@4.4.3)
+ openshell: 0.1.0
+ pdfjs-dist: 5.7.284
+ playwright-core: 1.59.1
+ proxy-agent: 8.0.1
+ qrcode: 1.5.4
tar: 7.5.13
+ tokenjuice: 0.7.0
+ 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.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:
- '@matrix-org/matrix-sdk-crypto-nodejs': 0.4.0
- openshell: 0.1.0
+ sqlite-vec: 0.1.9
transitivePeerDependencies:
- '@cfworker/json-schema'
- - aws-crt
+ - '@types/express'
- bufferutil
- canvas
- debug
- encoding
- supports-color
+ - tree-sitter
- utf-8-validate
openshell@0.1.0:
@@ -7176,12 +7354,9 @@ snapshots:
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,17 +7413,33 @@ 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
+
+ p-locate@4.1.0:
+ 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-retry@7.1.1:
+ p-timeout@3.2.0:
dependencies:
- is-network-error: 1.3.1
+ p-finally: 1.0.0
- p-timeout@4.1.0:
- optional: true
+ p-timeout@4.1.0: {}
+
+ p-try@2.2.0: {}
pac-proxy-agent@7.2.0:
dependencies:
@@ -7263,10 +7454,29 @@ snapshots:
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.0.2
+ 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: {}
@@ -7284,7 +7494,9 @@ snapshots:
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,10 +7527,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
+ '@napi-rs/canvas': 0.1.100
pend@1.2.0: {}
@@ -7330,7 +7541,9 @@ snapshots:
pkce-challenge@5.0.1: {}
- playwright-core@1.58.2: {}
+ playwright-core@1.59.1: {}
+
+ pngjs@5.0.0: {}
postcss@8.5.6:
dependencies:
@@ -7389,8 +7602,23 @@ snapshots:
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
@@ -7398,12 +7626,18 @@ snapshots:
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 +7673,8 @@ snapshots:
require-from-string@2.0.2: {}
+ require-main-filename@2.0.0: {}
+
retry@0.12.0: {}
retry@0.13.1: {}
@@ -7491,14 +7727,10 @@ snapshots:
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: {}
+ sandwich-stream@2.0.2: {}
search-insights@2.17.3: {}
@@ -7520,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
@@ -7529,41 +7765,12 @@ snapshots:
transitivePeerDependencies:
- supports-color
+ set-blocking@2.0.0: {}
+
setimmediate@1.0.5: {}
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
-
shebang-command@2.0.0:
dependencies:
shebang-regex: 3.0.0
@@ -7619,17 +7826,25 @@ snapshots:
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.7
+ socks: 2.8.9
transitivePeerDependencies:
- supports-color
- socks@2.8.7:
+ socks@2.8.9:
dependencies:
- ip-address: 10.1.0
+ ip-address: 10.2.0
smart-buffer: 4.2.0
source-map-js@1.2.1: {}
@@ -7647,28 +7862,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 +7925,7 @@ snapshots:
dependencies:
js-tokens: 9.0.1
- strnum@2.2.1: {}
+ strnum@2.3.0: {}
strtok3@10.3.5:
dependencies:
@@ -7746,7 +7962,6 @@ snapshots:
transitivePeerDependencies:
- encoding
- supports-color
- optional: true
test-exclude@7.0.1:
dependencies:
@@ -7787,8 +8002,15 @@ snapshots:
'@tokenizer/token': 0.3.0
ieee754: 1.2.1
+ tokenjuice@0.7.0: {}
+
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: {}
@@ -7797,12 +8019,18 @@ 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.37: {}
+
typescript@5.9.3: {}
uc.micro@2.1.0: {}
@@ -7815,9 +8043,9 @@ snapshots:
undici-types@7.16.0: {}
- undici@7.24.7: {}
+ undici@7.25.0: {}
- unhomoglyph@1.0.6: {}
+ undici@8.2.0: {}
unist-util-is@6.0.1:
dependencies:
@@ -7846,7 +8074,7 @@ snapshots:
util-deprecate@1.0.2: {}
- uuid@13.0.0: {}
+ uuid@14.0.0: {}
uuid@9.0.1: {}
@@ -7862,13 +8090,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 +8120,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 +8131,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 +8147,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 +8183,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 +8205,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 +8234,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 +8255,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 +8266,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,20 +8288,43 @@ 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@18.1.3:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
yargs-parser@20.2.9: {}
yargs-parser@21.1.1: {}
+ yargs@15.4.1:
+ dependencies:
+ 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
+ which-module: 2.0.1
+ y18n: 4.0.3
+ yargs-parser: 18.1.3
+
yargs@16.2.0:
dependencies:
cliui: 7.0.4
@@ -8081,10 +8352,10 @@ snapshots:
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/approval/approval-callback-handler.ts b/src/approval/approval-callback-handler.ts
new file mode 100644
index 00000000..68181ed5
--- /dev/null
+++ b/src/approval/approval-callback-handler.ts
@@ -0,0 +1,191 @@
+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 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;
+ 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 run = resolveCardRun(input.analysis.outTrackId);
+ const cardStillActive = run ? isActiveCardRun(run) : false;
+ const approvalId = resolveApprovalId(input.analysis);
+
+ if (!approvalId) {
+ 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" };
+ }
+
+ const result = await resolveApproval({
+ cfg: input.cfg,
+ accountId: input.accountId,
+ approvalId,
+ decision,
+ senderId: input.analysis.userId ?? "",
+ log: input.log,
+ });
+
+ if (result.ok) {
+ await patchCardBestEffort({
+ dtConfig,
+ 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" };
+ }
+
+ 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 };
+ }
+
+ 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-capability.ts b/src/approval/approval-capability.ts
new file mode 100644
index 00000000..79570725
--- /dev/null
+++ b/src/approval/approval-capability.ts
@@ -0,0 +1,49 @@
+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 { createDingTalkApprovalNativeRuntime } from "./approval-native-runtime";
+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,
+ nativeRuntime:
+ createDingTalkApprovalNativeRuntime() as unknown as ChannelApprovalCapability["nativeRuntime"],
+ describeExecApprovalSetup: () => EXEC_APPROVAL_SETUP_TEXT,
+ });
+}
diff --git a/src/approval/approval-card-locator.ts b/src/approval/approval-card-locator.ts
new file mode 100644
index 00000000..f799a683
--- /dev/null
+++ b/src/approval/approval-card-locator.ts
@@ -0,0 +1,32 @@
+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;
+ 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;
+ }
+ 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-card-patcher.ts b/src/approval/approval-card-patcher.ts
new file mode 100644
index 00000000..4dc1f649
--- /dev/null
+++ b/src/approval/approval-card-patcher.ts
@@ -0,0 +1,116 @@
+import { updateCardVariables } from "../card-callback-service";
+import { completeDeferredAICardFinalize } from "../card-service";
+import { DINGTALK_CARD_TEMPLATE } from "../card/card-template";
+import {
+ buildApprovalClearedCardParams,
+ buildApprovalPendingCardParams,
+} from "./approval-card-state";
+import {
+ clearCardRunPendingApproval,
+ markCardRunPendingApproval,
+ resolveCardRun,
+} from "../card/card-run-registry";
+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
+ // 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,
+ buildPendingCardPutVariables(approvalId, cardBodyMarkdown),
+ token,
+ config,
+ );
+ } catch (err) {
+ clearCardRunPendingApproval(outTrackId);
+ if (resolveCardRun(outTrackId)?.deferredFinalize) {
+ await applyExpiredPatch(outTrackId, token, false, config);
+ }
+ throw err;
+ }
+}
+
+/**
+ * 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,
+ cardStillActive: boolean,
+): Record {
+ const params: Record = {
+ ...buildApprovalClearedCardParams(cardStillActive),
+ };
+ if (resolveCardRun(outTrackId)?.deferredFinalize) {
+ params.flowStatus = 3;
+ }
+ return params;
+}
+
+export async function applyResolvedPatch(
+ outTrackId: string,
+ _decision: string,
+ token: string,
+ cardStillActive: boolean,
+ config?: Pick,
+): Promise {
+ await updateCardVariables(
+ outTrackId,
+ buildTerminalPatchParams(outTrackId, cardStillActive),
+ token,
+ config,
+ );
+ clearCardRunPendingApproval(outTrackId);
+ await completeDeferredAICardFinalize(outTrackId);
+}
+
+export async function applyExpiredPatch(
+ outTrackId: string,
+ token: string,
+ cardStillActive: boolean,
+ config?: Pick,
+): Promise {
+ await updateCardVariables(
+ outTrackId,
+ buildTerminalPatchParams(outTrackId, cardStillActive),
+ token,
+ config,
+ );
+ clearCardRunPendingApproval(outTrackId);
+ await completeDeferredAICardFinalize(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-command-intercept.ts b/src/approval/approval-command-intercept.ts
new file mode 100644
index 00000000..0aa480e7
--- /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 { APPROVE_COMMAND_RE, parseApproveCommand } from "./approval-command-parser";
+import { resolveApproval } from "./approval-resolver";
+
+export interface ApproveCommandInterceptInput {
+ cfg: OpenClawConfig;
+ accountId: string;
+ text: string;
+ senderId: string;
+ log?: Logger;
+}
+
+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} 已处理或已过期,无需再次操作。`);
+ } else if (result.reason === "gateway-error") {
+ await sendDirectHint(input, `ℹ️ 审批 ${parsed.approvalId} 暂时处理失败,请稍后重试。`);
+ }
+
+ input.log?.info?.(`[DingTalk][Approval] /approve resolver returned ${result.reason}`);
+ return true;
+}
diff --git a/src/approval/approval-command-parser.ts b/src/approval/approval-command-parser.ts
new file mode 100644
index 00000000..f95654fc
--- /dev/null
+++ b/src/approval/approval-command-parser.ts
@@ -0,0 +1,45 @@
+import type { ApprovalDecision } from "../types";
+
+export 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..e23014a6
--- /dev/null
+++ b/src/approval/approval-config.ts
@@ -0,0 +1,75 @@
+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 {
+ // v1 intentionally shares the exec approver list for plugin approvals.
+ return isExecAuthorizedSender(query);
+}
+
+export function resolveNativeDeliveryMode(_query: ApprovalConfigQuery): "channel" {
+ return "channel";
+}
diff --git a/src/approval/approval-markdown-render.ts b/src/approval/approval-markdown-render.ts
new file mode 100644
index 00000000..d98c19c3
--- /dev/null
+++ b/src/approval/approval-markdown-render.ts
@@ -0,0 +1,116 @@
+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 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");
+}
+
+/**
+ * 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);
+ 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..1ef31c80
--- /dev/null
+++ b/src/approval/approval-native-runtime.ts
@@ -0,0 +1,273 @@
+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 {
+ 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 = {
+ 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 shouldFallbackToMarkdown(error: unknown): boolean {
+ return isExplicitHttpFailure(error);
+}
+
+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";
+ 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 (!tst) {
+ log?.info?.(
+ `[DingTalk][Approval][shouldHandle] skip approval=${reqId} reason=missing-turnSourceTo`,
+ );
+ return false;
+ }
+ 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;
+ }
+ log?.info?.(
+ `[DingTalk][Approval][shouldHandle] accept approval=${reqId} account=${resolvedAccountId} approvers=${approvers}`,
+ );
+ return true;
+ },
+ },
+ presentation: {
+ buildPendingPayload: ({ request, approvalKind, nowMs }) => {
+ 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: buildExecApprovalMarkdown(request as never, nowMs),
+ cardBodyMarkdown: buildExecApprovalCardBody(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 log = getLogger(resolvedAccountId);
+ const to = normalizeApprovalTargetTo(target.to);
+ const activeCard = findActiveAgentCard({
+ cfg,
+ accountId: resolvedAccountId,
+ sessionKey: request.request.sessionKey ?? "",
+ 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: {
+ route: "card",
+ to,
+ accountId: resolvedAccountId,
+ activeCardOutTrackId: activeCard.outTrackId,
+ },
+ };
+ }
+ 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: {
+ 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,
+ pendingPayload.cardBodyMarkdown,
+ );
+ return {
+ mode: "card",
+ approvalId: pendingPayload.approvalId,
+ accountId: preparedTarget.accountId,
+ outTrackId: preparedTarget.activeCardOutTrackId,
+ };
+ } catch (error) {
+ if (!shouldFallbackToMarkdown(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/approval/approval-resolver.ts b/src/approval/approval-resolver.ts
new file mode 100644
index 00000000..084fed78
--- /dev/null
+++ b/src/approval/approval-resolver.ts
@@ -0,0 +1,146 @@
+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";
+
+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.execAuthorized && !params.pluginAuthorized) {
+ return null;
+ }
+ if (params.approvalId.startsWith("plugin:")) {
+ return { resolveMethod: "plugin" };
+ }
+ 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" };
+ }
+ return { allowPluginFallback: false };
+}
+
+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;
+ // Upstream not-found surfaces three ways: gatewayCode=APPROVAL_NOT_FOUND,
+ // gatewayCode=INVALID_REQUEST with details.reason=APPROVAL_NOT_FOUND, or
+ // gatewayCode=INVALID_REQUEST with message "unknown or expired approval id".
+ // Use the upstream helper so we catch all three (real-device repro showed
+ // the third form misclassified as gateway-error and surfacing a misleading
+ // "稍后重试" hint to the operator).
+ if (isApprovalNotFoundError(error)) {
+ 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/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/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-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 79e90708..10130fad 100644
--- a/src/card-service.ts
+++ b/src/card-service.ts
@@ -5,6 +5,14 @@ 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 {
+ 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";
import {
@@ -46,6 +54,17 @@ 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 {
+ 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(
outTrackId: string,
token: string,
@@ -805,6 +824,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),
@@ -1070,7 +1090,7 @@ export async function clearAICardStreamingContent(
}
}
-async function finalizeAICardStreamingLifecycleIfNeeded(
+export async function finalizeAICardStreamingLifecycleIfNeeded(
card: AICardInstance,
log?: Logger,
): Promise {
@@ -1090,6 +1110,39 @@ 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.
@@ -1125,14 +1178,28 @@ export async function commitAICardBlocks(
}
await ensureFreshToken(card, log);
- await finalizeAICardStreamingLifecycleIfNeeded(card, log);
+ 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. 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
[template.copyContentKey]: options.content, // same markdown as String type for card copy action
- flowStatus: 3, // completed state - V2 template hides stop button automatically
+ ...(approvalPending ? {} : { flowStatus: 3, ...APPROVAL_CARD_INITIAL }),
};
// Optional fields
@@ -1144,14 +1211,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,
@@ -1180,11 +1247,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`);
}
@@ -1428,6 +1503,7 @@ export async function finalizeStoppedAICard(
[template.streamingKey]: payload.content,
[template.copyContentKey]: payload.content,
flowStatus: 3,
+ ...approvalParamsForTerminal(card.outTrackId || card.cardInstanceId),
},
card.accessToken,
card.config,
diff --git a/src/card/card-run-registry.ts b/src/card/card-run-registry.ts
index 0af7acf2..242bcfb7 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;
@@ -20,6 +20,13 @@ export interface CardRunRecord {
ownerUserId?: string;
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;
}
@@ -92,6 +99,56 @@ 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;
+}
+
+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;
+ }
+}
+
+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/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/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/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/gateway/channel-gateway.ts b/src/gateway/channel-gateway.ts
index 73390a04..d11031d9 100644
--- a/src/gateway/channel-gateway.ts
+++ b/src/gateway/channel-gateway.ts
@@ -1,6 +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,
@@ -181,6 +185,33 @@ export function createDingTalkGateway(): NonNullable dynamicAckReactionController.dispose(MIN_THINKING_REACTION_VISIBLE_MS),
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/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/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 }),
+ );
+ });
+});
diff --git a/tests/integration/gateway-inbound-flow.test.ts b/tests/integration/gateway-inbound-flow.test.ts
index 6dc01dea..98f7d82c 100644
--- a/tests/integration/gateway-inbound-flow.test.ts
+++ b/tests/integration/gateway-inbound-flow.test.ts
@@ -11,6 +11,7 @@ const shared = vi.hoisted(() => ({
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..3d573d14
--- /dev/null
+++ b/tests/unit/approval-callback-handler.test.ts
@@ -0,0 +1,230 @@
+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("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,
+ 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"] as const)(
+ "expires the card for %s",
+ async (reason) => {
+ mockResolve.mockResolvedValue({ ok: false, reason });
+
+ await tryHandleApprovalCallback({ ...base, analysis: analysis() });
+
+ 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-capability.test.ts b/tests/unit/approval-capability.test.ts
new file mode 100644
index 00000000..a1130d89
--- /dev/null
+++ b/tests/unit/approval-capability.test.ts
@@ -0,0 +1,65 @@
+import { describe, expect, it, vi } from "vitest";
+
+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"
+);
+
+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("attaches nativeRuntime for channel-native approval delivery", () => {
+ createDingTalkApprovalCapability();
+
+ expect(mockFactory.mock.calls.at(-1)?.[0].nativeRuntime).toEqual({ marker: "native-runtime" });
+ });
+
+ 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/);
+ });
+});
diff --git a/tests/unit/approval-card-locator.test.ts b/tests/unit/approval-card-locator.test.ts
new file mode 100644
index 00000000..b20ecfc4
--- /dev/null
+++ b/tests/unit/approval-card-locator.test.ts
@@ -0,0 +1,81 @@
+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("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);
+
+ findActiveAgentCard({ cfg: {} as never, accountId: "acme", sessionKey: "session-A" });
+
+ expect(mockResolveActiveCardRunBySession).toHaveBeenCalledWith("acme", "session-A");
+ });
+});
diff --git a/tests/unit/approval-card-patcher.test.ts b/tests/unit/approval-card-patcher.test.ts
new file mode 100644
index 00000000..83b43c98
--- /dev/null
+++ b/tests/unit/approval-card-patcher.test.ts
@@ -0,0 +1,183 @@
+import { beforeEach, describe, expect, it, vi } from "vitest";
+
+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(),
+ resolveCardRun: vi.fn(),
+}));
+
+const { applyExpiredPatch, applyPendingPatch, applyResolvedPatch } = await import(
+ "../../src/approval/approval-card-patcher"
+);
+const { updateCardVariables } = await import("../../src/card-callback-service");
+const { completeDeferredAICardFinalize } = await import("../../src/card-service");
+const {
+ clearCardRunPendingApproval,
+ markCardRunPendingApproval,
+ resolveCardRun,
+} = await import("../../src/card/card-run-registry");
+
+const mockUpdate = vi.mocked(updateCardVariables);
+const mockComplete = vi.mocked(completeDeferredAICardFinalize);
+const mockMark = vi.mocked(markCardRunPendingApproval);
+const mockClear = vi.mocked(clearCardRunPendingApproval);
+const mockResolveRun = vi.mocked(resolveCardRun);
+
+describe("approval-card-patcher", () => {
+ beforeEach(() => {
+ mockUpdate.mockReset().mockResolvedValue(200);
+ mockComplete.mockReset().mockResolvedValue(undefined);
+ mockMark.mockReset();
+ mockClear.mockReset();
+ 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(() => {
+ 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 delegates terminal completion", async () => {
+ await applyResolvedPatch("ot1", "allow-once", "tok", true, {});
+
+ expect(mockUpdate).toHaveBeenCalledWith(
+ "ot1",
+ { show_approve_btns: "false", approveId: "", hasAction: "true" },
+ "tok",
+ {},
+ );
+ expect(mockClear).toHaveBeenCalledWith("ot1");
+ expect(mockComplete).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("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,
+ });
+
+ await applyResolvedPatch("ot1", "allow-once", "tok", true, {});
+
+ expect(mockUpdate).toHaveBeenCalledWith(
+ "ot1",
+ { show_approve_btns: "false", approveId: "", hasAction: "true", flowStatus: 3 },
+ "tok",
+ {},
+ );
+ expect(mockComplete).toHaveBeenCalledWith("ot1");
+ });
+
+ it("applies expired variables and delegates terminal completion", async () => {
+ await applyExpiredPatch("ot1", "tok", false, {});
+
+ expect(mockUpdate).toHaveBeenCalledWith(
+ "ot1",
+ { show_approve_btns: "false", approveId: "", hasAction: "false" },
+ "tok",
+ {},
+ );
+ expect(mockClear).toHaveBeenCalledWith("ot1");
+ expect(mockComplete).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-command-intercept.test.ts b/tests/unit/approval-command-intercept.test.ts
new file mode 100644
index 00000000..96942626
--- /dev/null
+++ b/tests/unit/approval-command-intercept.test.ts
@@ -0,0 +1,156 @@
+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("sends a retry DM for gateway errors", async () => {
+ mockResolveApproval.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("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,
+ );
+ });
+});
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-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/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");
+ });
+});
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..f1b1dbc3
--- /dev/null
+++ b/tests/unit/approval-native-runtime.test.ts
@@ -0,0 +1,275 @@
+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"),
+ buildExecApprovalCardBody: vi.fn(() => "exec-card-body"),
+ buildPluginApprovalCardBody: vi.fn(() => "plugin-card-body"),
+}));
+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",
+ cardBodyMarkdown: "exec-card-body",
+ });
+ });
+
+ 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:abc123",
+ target: {
+ route: "card",
+ to: "group:cid_xxx",
+ accountId: "default",
+ 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 () => {
+ 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",
+ cardBodyMarkdown: "card-body",
+ },
+ view: {} as never,
+ } as never);
+
+ expect(mockPending).toHaveBeenCalledWith(
+ "ot1",
+ "abc123",
+ "tok",
+ expect.objectContaining({ clientId: "x" }),
+ "card-body",
+ );
+ 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/approval-resolver.test.ts b/tests/unit/approval-resolver.test.ts
new file mode 100644
index 00000000..07082199
--- /dev/null
+++ b/tests/unit/approval-resolver.test.ts
@@ -0,0 +1,257 @@
+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();
+ });
+
+ 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", () => {
+ 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 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"));
+
+ 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);
+ });
+});
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",
+ }),
+ );
+ });
+});
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-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-run-registry-approval.test.ts b/tests/unit/card-run-registry-approval.test.ts
new file mode 100644
index 00000000..8f2c2c47
--- /dev/null
+++ b/tests/unit/card-run-registry-approval.test.ts
@@ -0,0 +1,115 @@
+import { beforeEach, describe, expect, it } from "vitest";
+import {
+ clearCardRunRegistryForTest,
+ clearCardRunPendingApproval,
+ isActiveCardRun,
+ markCardRunPendingApproval,
+ registerCardRun,
+ resolveCardRun,
+ 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");
+ });
+
+ 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..072fdbf6 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: '',
@@ -1195,3 +1197,237 @@ 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();
+ });
+
+ 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();
+ });
+});
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/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/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();
+ });
+});
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");
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");
+ });
+});
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..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.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.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 9e74be1e..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.3.28");
- expect(packageJson.openclaw?.install?.minHostVersion).toBe(">=2026.3.28");
+ expect(packageJson.peerDependencies?.openclaw).toBe(">=2026.5.7");
+ expect(packageJson.openclaw?.install?.minHostVersion).toBe(">=2026.5.7");
});
});