fix(im): address shortcut review regressions
This commit is contained in:
1 parent
9c297d0520
commit
d5c260c7c0
16 files changed
+259
-77
No files matched your search
@@ -388,6 +388,7 @@ func shortcutCustomConstraintEvidence(description string) []string {
|
||||
"绝对路径",
|
||||
"..",
|
||||
"最多 15 个字符",
|
||||
"能力矩阵",
|
||||
}
|
||||
evidence := make([]string, 0, len(probes))
|
||||
for _, probe := range probes {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": 1,
|
||||
"surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16",
|
||||
"source_hash": "sha256:0aec358a00376b48a2bf263dd413a439be2d19f5fc2dbc5f37ade862727f2b3c",
|
||||
"source_hash": "sha256:47e91a68c4dc191042873c29a3738e3de45633421dd75c9c7671f4d2d7f18e37",
|
||||
"catalog": {
|
||||
"agent_metadata": {
|
||||
"products_with_metadata": 26,
|
||||
@@ -10788,7 +10788,7 @@
|
||||
"cli_name": "+messages-send",
|
||||
"cli_path": "chat +messages-send",
|
||||
"confirmation": "user_required",
|
||||
"description": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 身份相关目标、内容和凭据不能为空,完整能力矩阵见命令说明",
|
||||
"description": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持",
|
||||
"effect": "write",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
|
||||
@@ -73972,7 +73972,7 @@
|
||||
]
|
||||
]
|
||||
},
|
||||
"description": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 身份相关目标、内容和凭据不能为空,完整能力矩阵见命令说明",
|
||||
"description": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持",
|
||||
"display": "群聊 / 消息 / 机器人",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
@@ -74077,13 +74077,13 @@
|
||||
"precedence": "cobra_help",
|
||||
"selected": true,
|
||||
"source": "cobra_help",
|
||||
"value": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 身份相关目标、内容和凭据不能为空,完整能力矩阵见命令说明"
|
||||
"value": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持"
|
||||
}
|
||||
],
|
||||
"precedence": "cobra_help",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_help",
|
||||
"value": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 身份相关目标、内容和凭据不能为空,完整能力矩阵见命令说明"
|
||||
"value": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持"
|
||||
},
|
||||
"effect": {
|
||||
"candidates": [
|
||||
@@ -74379,7 +74379,7 @@
|
||||
"type": "boolean"
|
||||
},
|
||||
"as": {
|
||||
"description": "--identity 的 lark-cli 对齐别名(可选值: user, bot, webhook)",
|
||||
"description": "--identity 的 lark-cli 对齐别名;受发送身份能力矩阵约束(可选值: user, bot, webhook)",
|
||||
"enum": [
|
||||
"user",
|
||||
"bot",
|
||||
@@ -74392,7 +74392,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "--identity 的 lark-cli 对齐别名(可选值: user, bot, webhook)"
|
||||
"value": "--identity 的 lark-cli 对齐别名;受发送身份能力矩阵约束(可选值: user, bot, webhook)"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -74404,7 +74404,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "--identity 的 lark-cli 对齐别名(可选值: user, bot, webhook)"
|
||||
"value": "--identity 的 lark-cli 对齐别名;受发送身份能力矩阵约束(可选值: user, bot, webhook)"
|
||||
},
|
||||
"enum": {
|
||||
"candidates": [
|
||||
@@ -74832,7 +74832,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"chat-id": {
|
||||
"description": "--group 的 lark-cli 对齐别名",
|
||||
"description": "--group 的 lark-cli 对齐别名;受发送身份能力矩阵约束",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
@@ -74840,7 +74840,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "--group 的 lark-cli 对齐别名"
|
||||
"value": "--group 的 lark-cli 对齐别名;受发送身份能力矩阵约束"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -74852,7 +74852,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "--group 的 lark-cli 对齐别名"
|
||||
"value": "--group 的 lark-cli 对齐别名;受发送身份能力矩阵约束"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
@@ -75096,7 +75096,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"group": {
|
||||
"description": "群 openConversationId(user/bot 群聊)",
|
||||
"description": "群 openConversationId(user/bot 群聊);受发送身份能力矩阵约束",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
@@ -75104,7 +75104,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "群 openConversationId(user/bot 群聊)"
|
||||
"value": "群 openConversationId(user/bot 群聊);受发送身份能力矩阵约束"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -75116,7 +75116,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "群 openConversationId(user/bot 群聊)"
|
||||
"value": "群 openConversationId(user/bot 群聊);受发送身份能力矩阵约束"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
@@ -75180,7 +75180,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"idempotency-key": {
|
||||
"description": "--uuid 的 lark-cli 对齐别名(仅 user)",
|
||||
"description": "--uuid 的 lark-cli 对齐别名(仅 user);受发送身份能力矩阵约束",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
@@ -75188,7 +75188,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "--uuid 的 lark-cli 对齐别名(仅 user)"
|
||||
"value": "--uuid 的 lark-cli 对齐别名(仅 user);受发送身份能力矩阵约束"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -75200,7 +75200,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "--uuid 的 lark-cli 对齐别名(仅 user)"
|
||||
"value": "--uuid 的 lark-cli 对齐别名(仅 user);受发送身份能力矩阵约束"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
@@ -75265,7 +75265,7 @@
|
||||
},
|
||||
"identity": {
|
||||
"default": "user",
|
||||
"description": "发送身份;身份相关目标、内容和凭据不能为空,完整能力矩阵见命令说明(可选值: user, bot, webhook)",
|
||||
"description": "发送身份;目标、凭据和幂等参数受发送身份能力矩阵约束(可选值: user, bot, webhook)",
|
||||
"enum": [
|
||||
"user",
|
||||
"bot",
|
||||
@@ -75278,7 +75278,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "发送身份;身份相关目标、内容和凭据不能为空,完整能力矩阵见命令说明(可选值: user, bot, webhook)"
|
||||
"value": "发送身份;目标、凭据和幂等参数受发送身份能力矩阵约束(可选值: user, bot, webhook)"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -75290,7 +75290,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "发送身份;身份相关目标、内容和凭据不能为空,完整能力矩阵见命令说明(可选值: user, bot, webhook)"
|
||||
"value": "发送身份;目标、凭据和幂等参数受发送身份能力矩阵约束(可选值: user, bot, webhook)"
|
||||
},
|
||||
"enum": {
|
||||
"candidates": [
|
||||
@@ -75694,7 +75694,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"open-dingtalk-id": {
|
||||
"description": "单聊接收者 openDingTalkId(user)",
|
||||
"description": "单聊接收者 openDingTalkId(user);受发送身份能力矩阵约束",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
@@ -75702,7 +75702,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "单聊接收者 openDingTalkId(user)"
|
||||
"value": "单聊接收者 openDingTalkId(user);受发送身份能力矩阵约束"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -75714,7 +75714,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "单聊接收者 openDingTalkId(user)"
|
||||
"value": "单聊接收者 openDingTalkId(user);受发送身份能力矩阵约束"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
@@ -75778,7 +75778,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"open-dingtalk-ids": {
|
||||
"description": "批量单聊接收者 openDingTalkId(bot)",
|
||||
"description": "批量单聊接收者 openDingTalkId(bot);受发送身份能力矩阵约束",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
@@ -75786,7 +75786,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "批量单聊接收者 openDingTalkId(bot)"
|
||||
"value": "批量单聊接收者 openDingTalkId(bot);受发送身份能力矩阵约束"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -75798,7 +75798,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "批量单聊接收者 openDingTalkId(bot)"
|
||||
"value": "批量单聊接收者 openDingTalkId(bot);受发送身份能力矩阵约束"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
@@ -75862,7 +75862,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"robot-code": {
|
||||
"description": "机器人 Code(identity=bot 时使用)",
|
||||
"description": "机器人 Code(identity=bot 时使用);受发送身份能力矩阵约束",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
@@ -75870,7 +75870,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "机器人 Code(identity=bot 时使用)"
|
||||
"value": "机器人 Code(identity=bot 时使用);受发送身份能力矩阵约束"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -75882,7 +75882,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "机器人 Code(identity=bot 时使用)"
|
||||
"value": "机器人 Code(identity=bot 时使用);受发送身份能力矩阵约束"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
@@ -76120,7 +76120,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"user": {
|
||||
"description": "单聊接收者 userId(user;自动解析 openDingTalkId)",
|
||||
"description": "单聊接收者 userId(user;自动解析 openDingTalkId);受发送身份能力矩阵约束",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
@@ -76128,7 +76128,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "单聊接收者 userId(user;自动解析 openDingTalkId)"
|
||||
"value": "单聊接收者 userId(user;自动解析 openDingTalkId);受发送身份能力矩阵约束"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -76140,7 +76140,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "单聊接收者 userId(user;自动解析 openDingTalkId)"
|
||||
"value": "单聊接收者 userId(user;自动解析 openDingTalkId);受发送身份能力矩阵约束"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
@@ -76204,7 +76204,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"users": {
|
||||
"description": "批量单聊接收者 userId(bot)",
|
||||
"description": "批量单聊接收者 userId(bot);受发送身份能力矩阵约束",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
@@ -76212,7 +76212,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "批量单聊接收者 userId(bot)"
|
||||
"value": "批量单聊接收者 userId(bot);受发送身份能力矩阵约束"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -76224,7 +76224,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "批量单聊接收者 userId(bot)"
|
||||
"value": "批量单聊接收者 userId(bot);受发送身份能力矩阵约束"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
@@ -76288,7 +76288,7 @@
|
||||
"type": "array"
|
||||
},
|
||||
"uuid": {
|
||||
"description": "幂等键(仅 user)",
|
||||
"description": "幂等键(仅 user);受发送身份能力矩阵约束",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
@@ -76296,7 +76296,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "幂等键(仅 user)"
|
||||
"value": "幂等键(仅 user);受发送身份能力矩阵约束"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -76308,7 +76308,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "幂等键(仅 user)"
|
||||
"value": "幂等键(仅 user);受发送身份能力矩阵约束"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
@@ -76372,7 +76372,7 @@
|
||||
"type": "string"
|
||||
},
|
||||
"webhook-token": {
|
||||
"description": "自定义机器人 Webhook token(identity=webhook 时使用)",
|
||||
"description": "自定义机器人 Webhook token(identity=webhook 时使用);受发送身份能力矩阵约束",
|
||||
"field_provenance": {
|
||||
"description": {
|
||||
"candidates": [
|
||||
@@ -76380,7 +76380,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"selected": true,
|
||||
"source": "cobra_usage",
|
||||
"value": "自定义机器人 Webhook token(identity=webhook 时使用)"
|
||||
"value": "自定义机器人 Webhook token(identity=webhook 时使用);受发送身份能力矩阵约束"
|
||||
},
|
||||
{
|
||||
"precedence": "default",
|
||||
@@ -76392,7 +76392,7 @@
|
||||
"precedence": "cobra_contract",
|
||||
"resolution": "highest_precedence",
|
||||
"source": "cobra_usage",
|
||||
"value": "自定义机器人 Webhook token(identity=webhook 时使用)"
|
||||
"value": "自定义机器人 Webhook token(identity=webhook 时使用);受发送身份能力矩阵约束"
|
||||
},
|
||||
"property": {
|
||||
"candidates": [
|
||||
|
||||
@@ -320,7 +320,7 @@ func listMessageProjectOneWithReactions(m map[string]any, includeReactions bool)
|
||||
if quoted := chatmsg.QuotedMessage(m); len(quoted) > 0 {
|
||||
row["quotedMessage"] = quoted
|
||||
}
|
||||
if resources := chatmsg.Resources(m); len(resources) > 0 {
|
||||
if resources := chatmsg.ResourcesDeep(m); len(resources) > 0 {
|
||||
row["resourceRefs"] = resources
|
||||
}
|
||||
projectForwarded := func(item map[string]any) map[string]any {
|
||||
@@ -638,6 +638,9 @@ func DownloadMessageResources(
|
||||
seen := map[string]bool{}
|
||||
for _, resource := range resources {
|
||||
resourceType := strings.TrimSpace(fmt.Sprint(resource["type"]))
|
||||
if canonicalType, ok := canonicalMessageResourceType(resourceType); ok {
|
||||
resourceType = canonicalType
|
||||
}
|
||||
resourceID := strings.TrimSpace(fmt.Sprint(resource["resourceId"]))
|
||||
download, _ := resource["download"].(map[string]any)
|
||||
arguments, _ := download["arguments"].(map[string]any)
|
||||
@@ -692,6 +695,9 @@ func DownloadMessageResources(
|
||||
plannedNames := map[string]int{}
|
||||
for _, resource := range resources {
|
||||
resourceType := strings.TrimSpace(fmt.Sprint(resource["type"]))
|
||||
if canonicalType, ok := canonicalMessageResourceType(resourceType); ok {
|
||||
resourceType = canonicalType
|
||||
}
|
||||
resourceID := strings.TrimSpace(fmt.Sprint(resource["resourceId"]))
|
||||
download, _ := resource["download"].(map[string]any)
|
||||
arguments, _ := download["arguments"].(map[string]any)
|
||||
|
||||
@@ -24,7 +24,7 @@ func TestCrossPlatformCoverageListMessageRichProjection(t *testing.T) {
|
||||
"content": `{"mediaId":"@image"}`,
|
||||
"quotedMessage": map[string]any{
|
||||
"openMessageId": "quoted",
|
||||
"content": "quoted",
|
||||
"content": `{"mediaId":"@quoted-image"}`,
|
||||
},
|
||||
},
|
||||
}}})
|
||||
@@ -36,6 +36,16 @@ func TestCrossPlatformCoverageListMessageRichProjection(t *testing.T) {
|
||||
t.Errorf("projection missing %s: %#v", key, rows[0])
|
||||
}
|
||||
}
|
||||
resources := rows[0]["resourceRefs"].([]map[string]any)
|
||||
if len(resources) != 2 {
|
||||
t.Fatalf("projected resources = %#v", resources)
|
||||
}
|
||||
quotedArgs := resources[1]["download"].(map[string]any)["arguments"].(map[string]any)
|
||||
if resources[1]["resourceId"] != "@quoted-image" ||
|
||||
quotedArgs["message-id"] != "quoted" ||
|
||||
quotedArgs["open-conversation-id"] != "cid" {
|
||||
t.Fatalf("quoted resource context = %#v", resources[1])
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageMgetResourceDownloadOutcomes(t *testing.T) {
|
||||
|
||||
@@ -20,6 +20,36 @@ import (
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
|
||||
)
|
||||
|
||||
func TestMessagesSendPublishesCompleteIdentityConstraintInputs(t *testing.T) {
|
||||
want := []string{
|
||||
"identity", "as", "group", "chat-id", "user", "open-dingtalk-id",
|
||||
"users", "open-dingtalk-ids", "robot-code", "webhook-token",
|
||||
"uuid", "idempotency-key",
|
||||
}
|
||||
var got []string
|
||||
for _, constraint := range MessagesSend.Constraints {
|
||||
if constraint.Kind == shortcut.ConstraintCustom {
|
||||
got = constraint.Flags
|
||||
break
|
||||
}
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("identity constraint flags = %#v, want %#v", got, want)
|
||||
}
|
||||
wantSet := make(map[string]bool, len(want))
|
||||
for _, name := range want {
|
||||
wantSet[name] = true
|
||||
}
|
||||
for _, flag := range MessagesSend.Flags {
|
||||
if !wantSet[flag.Name] {
|
||||
continue
|
||||
}
|
||||
if !strings.Contains(flag.Desc, "能力矩阵") {
|
||||
t.Errorf("--%s does not publish identity matrix semantics: %q", flag.Name, flag.Desc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageMessagesSendCurrentUserImageAndUserResolution(t *testing.T) {
|
||||
fake := &larkAlignmentCaller{}
|
||||
helpers.InitDeps(fake)
|
||||
|
||||
@@ -89,7 +89,8 @@ var MessagesResourceDownload = shortcut.Shortcut{
|
||||
if err := validateResourceDownloadOutput(rt.Str("output")); err != nil {
|
||||
return err
|
||||
}
|
||||
if rt.Str("type") == "mediaId" &&
|
||||
resourceType, _ := canonicalMessageResourceType(rt.Str("type"))
|
||||
if resourceType == "mediaId" &&
|
||||
(strings.TrimSpace(rt.Str("message-id")) == "" ||
|
||||
strings.TrimSpace(rt.Str("open-conversation-id")) == "") {
|
||||
return apperrors.NewValidation(
|
||||
@@ -98,8 +99,9 @@ var MessagesResourceDownload = shortcut.Shortcut{
|
||||
return nil
|
||||
},
|
||||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||||
resourceType, _ := canonicalMessageResourceType(rt.Str("type"))
|
||||
plan := map[string]any{
|
||||
"resourceType": rt.Str("type"),
|
||||
"resourceType": resourceType,
|
||||
"resourceId": rt.Str("resource-id"),
|
||||
"messageId": rt.Str("message-id"),
|
||||
"openConversationId": rt.Str("open-conversation-id"),
|
||||
@@ -119,7 +121,7 @@ var MessagesResourceDownload = shortcut.Shortcut{
|
||||
|
||||
data, err := resolveMessageResourceDownloadData(
|
||||
rt,
|
||||
rt.Str("type"),
|
||||
resourceType,
|
||||
rt.Str("resource-id"),
|
||||
rt.Str("message-id"),
|
||||
rt.Str("open-conversation-id"),
|
||||
@@ -153,7 +155,7 @@ var MessagesResourceDownload = shortcut.Shortcut{
|
||||
return rt.Output(map[string]any{
|
||||
"messageId": rt.Str("message-id"),
|
||||
"resourceId": rt.Str("resource-id"),
|
||||
"resourceType": rt.Str("type"),
|
||||
"resourceType": resourceType,
|
||||
"localPath": filepath.ToSlash(relativePath),
|
||||
"sizeBytes": size,
|
||||
})
|
||||
@@ -164,6 +166,11 @@ func resolveMessageResourceDownloadData(
|
||||
rt *shortcut.RuntimeContext,
|
||||
resourceType, resourceID, messageID, conversationID string,
|
||||
) (map[string]any, error) {
|
||||
resourceType, ok := canonicalMessageResourceType(resourceType)
|
||||
if !ok {
|
||||
return nil, apperrors.NewValidation(fmt.Sprintf(
|
||||
"不支持的消息资源类型 %q;仅支持 mediaId 或 fileId", resourceType))
|
||||
}
|
||||
if resourceType == "fileId" {
|
||||
return rt.CallMCPData("drive", "download_file", map[string]any{
|
||||
"fileId": resourceID,
|
||||
@@ -177,6 +184,17 @@ func resolveMessageResourceDownloadData(
|
||||
})
|
||||
}
|
||||
|
||||
func canonicalMessageResourceType(resourceType string) (string, bool) {
|
||||
switch {
|
||||
case strings.EqualFold(strings.TrimSpace(resourceType), "mediaId"):
|
||||
return "mediaId", true
|
||||
case strings.EqualFold(strings.TrimSpace(resourceType), "fileId"):
|
||||
return "fileId", true
|
||||
default:
|
||||
return strings.TrimSpace(resourceType), false
|
||||
}
|
||||
}
|
||||
|
||||
func validateResourceDownloadOutput(output string) error {
|
||||
return validateResourceDownloadOutputFlag(output, "--output")
|
||||
}
|
||||
@@ -486,6 +504,8 @@ func downloadResourceAtomically(
|
||||
clientCopy := *client
|
||||
client = &clientCopy
|
||||
originalRedirect := client.CheckRedirect
|
||||
initialHost := strings.ToLower(parsedResourceURL.Hostname())
|
||||
headersConfinedToInitialHost := true
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
if _, redirectErr := validateResourceDownloadURL(req.URL.String()); redirectErr != nil {
|
||||
return apperrors.NewValidation(fmt.Sprintf(
|
||||
@@ -494,8 +514,14 @@ func downloadResourceAtomically(
|
||||
if len(via) >= 10 {
|
||||
return apperrors.NewAPI("资源下载重定向次数过多")
|
||||
}
|
||||
if len(via) > 0 &&
|
||||
!strings.EqualFold(req.URL.Hostname(), via[len(via)-1].URL.Hostname()) {
|
||||
if !strings.EqualFold(req.URL.Hostname(), initialHost) {
|
||||
headersConfinedToInitialHost = false
|
||||
}
|
||||
// net/http rebuilds redirect headers from the initial request on every
|
||||
// hop. Once a chain leaves the original host, strip lower-service
|
||||
// headers on every later hop so a same-host redirect on the new origin
|
||||
// cannot silently restore them.
|
||||
if !headersConfinedToInitialHost {
|
||||
for key := range headers {
|
||||
req.Header.Del(key)
|
||||
}
|
||||
|
||||
@@ -469,18 +469,29 @@ func TestCrossPlatformCoverageDownloadResourceRedirectGuards(t *testing.T) {
|
||||
redirectCount := 0
|
||||
crossHost := &http.Client{Transport: resourceRoundTripper(func(request *http.Request) (*http.Response, error) {
|
||||
redirectCount++
|
||||
if redirectCount == 1 {
|
||||
switch redirectCount {
|
||||
case 1:
|
||||
if request.Header.Get("X-Resource-Token") != "secret" {
|
||||
t.Fatal("initial signed header missing")
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusFound,
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Header: http.Header{"Location": []string{"https://bucket.oss-cn-hangzhou.aliyuncs.com/intermediate"}},
|
||||
}, nil
|
||||
case 2:
|
||||
if request.Header.Get("X-Resource-Token") != "" {
|
||||
t.Fatal("server-supplied header leaked on first cross-host hop")
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusFound,
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Header: http.Header{"Location": []string{"https://bucket.oss-cn-hangzhou.aliyuncs.com/final"}},
|
||||
}, nil
|
||||
}
|
||||
if request.Header.Get("X-Resource-Token") != "" {
|
||||
t.Fatal("server-supplied header leaked across download domains")
|
||||
default:
|
||||
if request.Header.Get("X-Resource-Token") != "" {
|
||||
t.Fatal("server-supplied header was restored on later same-host hop")
|
||||
}
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
|
||||
@@ -23,6 +23,29 @@ import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCrossPlatformCoverageCanonicalMessageResourceType(t *testing.T) {
|
||||
for input, want := range map[string]string{
|
||||
"mediaId": "mediaId",
|
||||
"MEDIAID": "mediaId",
|
||||
" fileid ": "fileId",
|
||||
"FileId": "fileId",
|
||||
} {
|
||||
got, ok := canonicalMessageResourceType(input)
|
||||
if !ok || got != want {
|
||||
t.Errorf("canonicalMessageResourceType(%q) = %q, %v; want %q, true",
|
||||
input, got, ok, want)
|
||||
}
|
||||
}
|
||||
if got, ok := canonicalMessageResourceType("attachment"); ok || got != "attachment" {
|
||||
t.Errorf("unsupported resource type = %q, %v", got, ok)
|
||||
}
|
||||
if _, err := resolveMessageResourceDownloadData(
|
||||
nil, "attachment", "resource", "message", "conversation",
|
||||
); err == nil || !strings.Contains(err.Error(), "不支持的消息资源类型") {
|
||||
t.Fatalf("unsupported resolver type error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageResourceDownloadInfo(t *testing.T) {
|
||||
url, headers, err := resourceDownloadInfo(map[string]any{
|
||||
"result": map[string]any{
|
||||
|
||||
@@ -31,16 +31,16 @@ var MessagesSend = shortcut.Shortcut{
|
||||
Intent: "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。",
|
||||
Risk: shortcut.RiskWrite,
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "identity", Type: shortcut.FlagString, Default: "user", Enum: []string{"user", "bot", "webhook"}, Desc: "发送身份;身份相关目标、内容和凭据不能为空,完整能力矩阵见命令说明"},
|
||||
{Name: "as", Type: shortcut.FlagString, Enum: []string{"user", "bot", "webhook"}, Desc: "--identity 的 lark-cli 对齐别名"},
|
||||
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(user/bot 群聊)"},
|
||||
{Name: "chat-id", Type: shortcut.FlagString, Desc: "--group 的 lark-cli 对齐别名"},
|
||||
{Name: "user", Type: shortcut.FlagString, Desc: "单聊接收者 userId(user;自动解析 openDingTalkId)"},
|
||||
{Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊接收者 openDingTalkId(user)"},
|
||||
{Name: "users", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 userId(bot)"},
|
||||
{Name: "open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 openDingTalkId(bot)"},
|
||||
{Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code(identity=bot 时使用)"},
|
||||
{Name: "webhook-token", Type: shortcut.FlagString, Desc: "自定义机器人 Webhook token(identity=webhook 时使用)"},
|
||||
{Name: "identity", Type: shortcut.FlagString, Default: "user", Enum: []string{"user", "bot", "webhook"}, Desc: "发送身份;目标、凭据和幂等参数受发送身份能力矩阵约束"},
|
||||
{Name: "as", Type: shortcut.FlagString, Enum: []string{"user", "bot", "webhook"}, Desc: "--identity 的 lark-cli 对齐别名;受发送身份能力矩阵约束"},
|
||||
{Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(user/bot 群聊);受发送身份能力矩阵约束"},
|
||||
{Name: "chat-id", Type: shortcut.FlagString, Desc: "--group 的 lark-cli 对齐别名;受发送身份能力矩阵约束"},
|
||||
{Name: "user", Type: shortcut.FlagString, Desc: "单聊接收者 userId(user;自动解析 openDingTalkId);受发送身份能力矩阵约束"},
|
||||
{Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊接收者 openDingTalkId(user);受发送身份能力矩阵约束"},
|
||||
{Name: "users", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 userId(bot);受发送身份能力矩阵约束"},
|
||||
{Name: "open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 openDingTalkId(bot);受发送身份能力矩阵约束"},
|
||||
{Name: "robot-code", Type: shortcut.FlagString, Desc: "机器人 Code(identity=bot 时使用);受发送身份能力矩阵约束"},
|
||||
{Name: "webhook-token", Type: shortcut.FlagString, Desc: "自定义机器人 Webhook token(identity=webhook 时使用);受发送身份能力矩阵约束"},
|
||||
{Name: "title", Type: shortcut.FlagString, Desc: "消息标题(不传则从正文生成)"},
|
||||
{Name: "text", Type: shortcut.FlagString, Desc: "纯文本正文"},
|
||||
{Name: "markdown", Type: shortcut.FlagString, Desc: "Markdown 正文"},
|
||||
@@ -48,8 +48,8 @@ var MessagesSend = shortcut.Shortcut{
|
||||
{Name: "media-id", Type: shortcut.FlagString, Desc: "已有图片 mediaId(仅 user 的 image)"},
|
||||
{Name: "file", Type: shortcut.FlagString, Desc: "工作目录内安全相对文件路径(仅 user 的 file/audio/video)"},
|
||||
{Name: "file-path", Type: shortcut.FlagString, Desc: "--file 的兼容别名"},
|
||||
{Name: "uuid", Type: shortcut.FlagString, Desc: "幂等键(仅 user)"},
|
||||
{Name: "idempotency-key", Type: shortcut.FlagString, Desc: "--uuid 的 lark-cli 对齐别名(仅 user)"},
|
||||
{Name: "uuid", Type: shortcut.FlagString, Desc: "幂等键(仅 user);受发送身份能力矩阵约束"},
|
||||
{Name: "idempotency-key", Type: shortcut.FlagString, Desc: "--uuid 的 lark-cli 对齐别名(仅 user);受发送身份能力矩阵约束"},
|
||||
{Name: "at-open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "@ 的 openDingTalkId(user/bot 群聊)"},
|
||||
{Name: "at-user-ids", Type: shortcut.FlagStringSlice, Desc: "@ 的 userId(bot/webhook)"},
|
||||
{Name: "at-mobiles", Type: shortcut.FlagStringSlice, Desc: "@ 的手机号(webhook)"},
|
||||
@@ -65,8 +65,8 @@ var MessagesSend = shortcut.Shortcut{
|
||||
{Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"uuid", "idempotency-key"}},
|
||||
{
|
||||
Kind: shortcut.ConstraintCustom,
|
||||
Flags: []string{"identity"},
|
||||
Description: "身份相关目标、内容和凭据不能为空,完整能力矩阵见命令说明",
|
||||
Flags: []string{"identity", "as", "group", "chat-id", "user", "open-dingtalk-id", "users", "open-dingtalk-ids", "robot-code", "webhook-token", "uuid", "idempotency-key"},
|
||||
Description: "目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持",
|
||||
},
|
||||
},
|
||||
Tips: []string{
|
||||
|
||||
@@ -135,6 +135,46 @@ func TestCrossPlatformCoverageConfirmRiskUsesEffectiveRisk(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfirmRiskDoesNotReadStdinForDynamicReadInvocation(t *testing.T) {
|
||||
cmd := &cobra.Command{}
|
||||
cmd.Flags().Bool("download-resources", false, "")
|
||||
var stderr bytes.Buffer
|
||||
cmd.SetErr(&stderr)
|
||||
rt := &RuntimeContext{cmd: cmd}
|
||||
s := Shortcut{
|
||||
Service: "chat",
|
||||
Command: "+at-me",
|
||||
Risk: RiskRead,
|
||||
RiskWhen: func(rt *RuntimeContext) Risk {
|
||||
if rt.Bool("download-resources") {
|
||||
return RiskWrite
|
||||
}
|
||||
return RiskRead
|
||||
},
|
||||
}
|
||||
|
||||
reader, writer, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := writer.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
previousStdin := os.Stdin
|
||||
os.Stdin = reader
|
||||
t.Cleanup(func() {
|
||||
os.Stdin = previousStdin
|
||||
_ = reader.Close()
|
||||
})
|
||||
|
||||
if !confirmRisk(rt, s) {
|
||||
t.Fatal("dynamic read invocation was treated as a declined write")
|
||||
}
|
||||
if stderr.Len() != 0 {
|
||||
t.Fatalf("dynamic read invocation prompted: %q", stderr.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageMountRegistersFlagsAndUse(t *testing.T) {
|
||||
s := Shortcut{
|
||||
Service: "contact",
|
||||
|
||||
@@ -240,7 +240,7 @@ func atMeProjectWithReactions(m map[string]any, includeReactions bool) map[strin
|
||||
if quoted := chatmsg.QuotedMessage(m); len(quoted) > 0 {
|
||||
row["quotedMessage"] = quoted
|
||||
}
|
||||
if resources := chatmsg.Resources(m); len(resources) > 0 {
|
||||
if resources := chatmsg.ResourcesDeep(m); len(resources) > 0 {
|
||||
row["resourceRefs"] = resources
|
||||
}
|
||||
projectForwarded := func(item map[string]any) map[string]any {
|
||||
|
||||
@@ -22,6 +22,12 @@ import (
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
|
||||
)
|
||||
|
||||
var dingTalkMessageLocation = time.FixedZone("CST", 8*60*60)
|
||||
|
||||
func formatDingTalkMessageBoundary(now time.Time) string {
|
||||
return now.In(dingTalkMessageLocation).Format("2006-01-02 15:04:05")
|
||||
}
|
||||
|
||||
// ChatMessages: fetch the message list of one conversation (group OR single
|
||||
// chat) and print a clean projected list (speaker / text / time) instead of a
|
||||
// raw dump.
|
||||
@@ -86,7 +92,7 @@ var ChatMessages = shortcut.Shortcut{
|
||||
if rt.Changed("time") && rt.Str("time") != "" {
|
||||
params["time"] = rt.Str("time")
|
||||
} else {
|
||||
params["time"] = time.Now().Format("2006-01-02 15:04:05")
|
||||
params["time"] = formatDingTalkMessageBoundary(time.Now())
|
||||
}
|
||||
if limit := rt.IntFirst("limit", "size"); limit > 0 {
|
||||
params["limit"] = limit
|
||||
@@ -211,7 +217,7 @@ func projectChatMessageWithReactions(m map[string]any, includeReactions bool) ma
|
||||
if quoted := chatmsg.QuotedMessage(m); len(quoted) > 0 {
|
||||
row["quotedMessage"] = quoted
|
||||
}
|
||||
if resources := chatmsg.Resources(m); len(resources) > 0 {
|
||||
if resources := chatmsg.ResourcesDeep(m); len(resources) > 0 {
|
||||
row["resourceRefs"] = resources
|
||||
}
|
||||
projectForwarded := func(item map[string]any) map[string]any {
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestCrossPlatformCoverageRichMessageProjections(t *testing.T) {
|
||||
"emotionReplyList": []any{map[string]any{"emojiName": "赞", "replyCount": 1}},
|
||||
"quotedMessage": map[string]any{
|
||||
"openMessageId": "quoted",
|
||||
"content": "quoted body",
|
||||
"content": `{"mediaId":"@quoted-image"}`,
|
||||
},
|
||||
}
|
||||
for name, row := range map[string]map[string]any{
|
||||
@@ -80,6 +80,16 @@ func TestCrossPlatformCoverageRichMessageProjections(t *testing.T) {
|
||||
t.Errorf("%s projection missing %s: %#v", name, key, row)
|
||||
}
|
||||
}
|
||||
resources := row["resourceRefs"].([]map[string]any)
|
||||
if len(resources) != 2 {
|
||||
t.Fatalf("%s projected resources = %#v", name, resources)
|
||||
}
|
||||
quotedArgs := resources[1]["download"].(map[string]any)["arguments"].(map[string]any)
|
||||
if resources[1]["resourceId"] != "@quoted-image" ||
|
||||
quotedArgs["message-id"] != "quoted" ||
|
||||
quotedArgs["open-conversation-id"] != "cid" {
|
||||
t.Fatalf("%s quoted resource context = %#v", name, resources[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
)
|
||||
|
||||
func TestCrossPlatformCoverageMessageReadShortcutsPublishResourceDownloadPlans(t *testing.T) {
|
||||
message := `{"openMessageId":"msg","openConversationId":"cid","content":"{\"mediaId\":\"@image\"}"}`
|
||||
message := `{"openMessageId":"msg","openConversationId":"cid","content":"{\"mediaId\":\"@image\"}","quotedMessage":{"openMessageId":"quoted","content":"{\"fileId\":\"@quoted-file\"}"}}`
|
||||
tests := []struct {
|
||||
name string
|
||||
tool string
|
||||
@@ -72,11 +72,19 @@ func TestCrossPlatformCoverageMessageReadShortcutsPublishResourceDownloadPlans(t
|
||||
if err := json.Unmarshal(output.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("decode output: %v\n%s", err, output.String())
|
||||
}
|
||||
if _, ok := payload[tc.resultKey]; !ok {
|
||||
rows, ok := payload[tc.resultKey].([]any)
|
||||
if !ok || len(rows) != 1 {
|
||||
t.Fatalf("payload missing %s: %#v", tc.resultKey, payload)
|
||||
}
|
||||
row, _ := rows[0].(map[string]any)
|
||||
resources, _ := row["resourceRefs"].([]any)
|
||||
if len(resources) != 2 {
|
||||
t.Fatalf("visible resources = %#v", resources)
|
||||
}
|
||||
ledger, _ := payload["resourceDownloads"].(map[string]any)
|
||||
if ledger["dryRun"] != true || ledger["requestedCount"] != float64(1) {
|
||||
if ledger["dryRun"] != true ||
|
||||
ledger["discoveredCount"] != float64(2) ||
|
||||
ledger["requestedCount"] != float64(2) {
|
||||
t.Fatalf("resource plan = %#v", ledger)
|
||||
}
|
||||
})
|
||||
@@ -104,7 +112,7 @@ func TestCrossPlatformCoverageChatMessagesDefaultsToRecentHistory(t *testing.T)
|
||||
helpers.InitDeps(caller)
|
||||
root := newPlatformCoverageRoot()
|
||||
before := time.Now().Add(-2 * time.Second)
|
||||
root.SetArgs([]string{"chat", "+chat-messages", "--group", "cid", "--limit", "5", "--yes"})
|
||||
root.SetArgs([]string{"chat", "+chat-messages", "--group", "cid", "--limit", "5"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -116,7 +124,11 @@ func TestCrossPlatformCoverageChatMessagesDefaultsToRecentHistory(t *testing.T)
|
||||
if call.args["forward"] != false {
|
||||
t.Fatalf("default forward = %#v, want false", call.args["forward"])
|
||||
}
|
||||
boundary, err := time.ParseInLocation("2006-01-02 15:04:05", call.args["time"].(string), time.Local)
|
||||
boundary, err := time.ParseInLocation(
|
||||
"2006-01-02 15:04:05",
|
||||
call.args["time"].(string),
|
||||
dingTalkMessageLocation,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -125,6 +137,13 @@ func TestCrossPlatformCoverageChatMessagesDefaultsToRecentHistory(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatDingTalkMessageBoundaryDoesNotDependOnProcessTimezone(t *testing.T) {
|
||||
now := time.Date(2026, time.July, 29, 1, 2, 3, 0, time.UTC)
|
||||
if got := formatDingTalkMessageBoundary(now); got != "2026-07-29 09:02:03" {
|
||||
t.Fatalf("UTC process boundary = %q, want DingTalk UTC+8 wall time", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageChatMessagesPreservesExplicitTime(t *testing.T) {
|
||||
caller := &platformCoverageCaller{}
|
||||
helpers.InitDeps(caller)
|
||||
|
||||
@@ -526,7 +526,7 @@ func searchMsgProjectWithReactions(m map[string]any, includeReactions bool) map[
|
||||
if quoted := chatmsg.QuotedMessage(m); len(quoted) > 0 {
|
||||
row["quotedMessage"] = quoted
|
||||
}
|
||||
if resources := chatmsg.Resources(m); len(resources) > 0 {
|
||||
if resources := chatmsg.ResourcesDeep(m); len(resources) > 0 {
|
||||
row["resourceRefs"] = resources
|
||||
}
|
||||
projectForwarded := func(item map[string]any) map[string]any {
|
||||
|
||||
Reference in new issue
Block a user