fix(im): harden shortcut downloads and message context
This commit is contained in:
1 parent
41e0fb381a
commit
11a7ab8b7c
28 files changed
+959
-504
No files matched your search
@@ -8,8 +8,8 @@
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
"confirmation": "user_required",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws chat +at-me",
|
||||
@@ -37,14 +37,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -69,34 +69,34 @@
|
||||
]
|
||||
},
|
||||
"confirmation": {
|
||||
"value": "not_required",
|
||||
"value": "user_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"value": "user_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"effect": {
|
||||
"value": "read",
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "read",
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -123,18 +123,18 @@
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"value": "idempotent",
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "idempotent",
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -143,14 +143,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -159,14 +159,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -191,14 +191,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
@@ -210,24 +210,24 @@
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"value": "low",
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "low",
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。这是纯只读操作,只做搜索与本地投影,不会发送、撤回或标记任何消息。"
|
||||
"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。默认只读且不会发送、撤回或标记任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -236,7 +236,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。这是纯只读操作,只做搜索与本地投影,不会发送、撤回或标记任何消息。"
|
||||
"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。默认只读且不会发送、撤回或标记任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -246,11 +246,11 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"idempotency": "idempotent",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"risk": "medium",
|
||||
"source_refs": [
|
||||
"ShortcutRegistry:chat +at-me",
|
||||
"cobra-help:dws chat +at-me",
|
||||
@@ -259,7 +259,7 @@
|
||||
"internal/cli/schema_hints/selection/chat.json"
|
||||
],
|
||||
"use_when": [
|
||||
"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。这是纯只读操作,只做搜索与本地投影,不会发送、撤回或标记任何消息。"
|
||||
"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。默认只读且不会发送、撤回或标记任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
]
|
||||
},
|
||||
"chat +bot-find": {
|
||||
@@ -3623,8 +3623,8 @@
|
||||
"avoid_when": [
|
||||
"要跨多个会话按关键词、发送者或消息类型检索时使用 +search-msg;已有一批精确消息 ID 时使用 +messages-mget"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
"confirmation": "user_required",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws chat +chat-messages --group \u003copenConversationId\u003e --direction older",
|
||||
@@ -3652,14 +3652,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3684,34 +3684,34 @@
|
||||
]
|
||||
},
|
||||
"confirmation": {
|
||||
"value": "not_required",
|
||||
"value": "user_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"value": "user_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"effect": {
|
||||
"value": "read",
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "read",
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3738,18 +3738,18 @@
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"value": "idempotent",
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "idempotent",
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3758,14 +3758,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3774,14 +3774,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -3806,14 +3806,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
@@ -3825,24 +3825,24 @@
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"value": "low",
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "low",
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。"
|
||||
"当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -3851,7 +3851,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。"
|
||||
"当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -3861,11 +3861,11 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"idempotency": "idempotent",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"risk": "medium",
|
||||
"source_refs": [
|
||||
"ShortcutRegistry:chat +chat-messages",
|
||||
"cobra-help:dws chat +chat-messages",
|
||||
@@ -3874,7 +3874,7 @@
|
||||
"internal/cli/schema_hints/selection/chat.json"
|
||||
],
|
||||
"use_when": [
|
||||
"当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。"
|
||||
"当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
]
|
||||
},
|
||||
"chat +chat-mute": {
|
||||
@@ -9047,8 +9047,8 @@
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
"confirmation": "user_required",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws chat +messages-mget --msg-ids msgId1,msgId2"
|
||||
@@ -9075,14 +9075,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -9107,34 +9107,34 @@
|
||||
]
|
||||
},
|
||||
"confirmation": {
|
||||
"value": "not_required",
|
||||
"value": "user_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"value": "user_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"effect": {
|
||||
"value": "read",
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "read",
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -9159,18 +9159,18 @@
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"value": "idempotent",
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "idempotent",
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -9179,14 +9179,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -9195,14 +9195,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -9227,14 +9227,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
@@ -9246,24 +9246,24 @@
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"value": "low",
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "low",
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity."
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。"
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9272,7 +9272,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。"
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -9282,11 +9282,11 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"idempotency": "idempotent",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"risk": "medium",
|
||||
"source_refs": [
|
||||
"ShortcutRegistry:chat +messages-mget",
|
||||
"cobra-help:dws chat +messages-mget",
|
||||
@@ -9295,7 +9295,7 @@
|
||||
"internal/cli/schema_hints/selection/chat.json"
|
||||
],
|
||||
"use_when": [
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。"
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
]
|
||||
},
|
||||
"chat +messages-query-send-status": {
|
||||
@@ -11120,8 +11120,8 @@
|
||||
"avoid_when": [
|
||||
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
"confirmation": "user_required",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws chat +search-msg --query \"周报\" --senders \u003copenDingTalkId\u003e --days 3 --page-all",
|
||||
@@ -11149,14 +11149,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger.",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger."
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11181,34 +11181,34 @@
|
||||
]
|
||||
},
|
||||
"confirmation": {
|
||||
"value": "not_required",
|
||||
"value": "user_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger.",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"value": "user_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger."
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"effect": {
|
||||
"value": "read",
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger.",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "read",
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger."
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11235,18 +11235,18 @@
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"value": "idempotent",
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger.",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "idempotent",
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger."
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11255,14 +11255,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger.",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger."
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11271,14 +11271,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger.",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger."
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11303,14 +11303,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger.",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger."
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
@@ -11322,24 +11322,24 @@
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"value": "low",
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger.",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "low",
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger."
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。"
|
||||
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11348,7 +11348,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。"
|
||||
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11358,11 +11358,11 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"idempotency": "idempotent",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"risk": "medium",
|
||||
"source_refs": [
|
||||
"ShortcutRegistry:chat +search-msg",
|
||||
"cobra-help:dws chat +search-msg",
|
||||
@@ -11371,7 +11371,7 @@
|
||||
"internal/cli/schema_hints/selection/chat.json"
|
||||
],
|
||||
"use_when": [
|
||||
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。"
|
||||
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
]
|
||||
},
|
||||
"chat +send-to-group": {
|
||||
@@ -11637,10 +11637,10 @@
|
||||
"agent_summary_source": "dws-agent-selection/chat",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"要回复 Thread 或发送新回复时不要使用此只读入口;当前没有经过验证的 thread writer Shortcut"
|
||||
"要回复 Thread 或发送新回复时不要使用此读取入口;当前没有经过验证的 thread writer Shortcut"
|
||||
],
|
||||
"confirmation": "not_required",
|
||||
"effect": "read",
|
||||
"confirmation": "user_required",
|
||||
"effect": "write",
|
||||
"effect_source": "agent-hint",
|
||||
"examples": [
|
||||
"dws chat +thread-replies --group \u003copenConversationId\u003e --thread-id \u003cthreadId\u003e",
|
||||
@@ -11668,20 +11668,20 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "available",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"avoid_when": {
|
||||
"value": [
|
||||
"要回复 Thread 或发送新回复时不要使用此只读入口;当前没有经过验证的 thread writer Shortcut"
|
||||
"要回复 Thread 或发送新回复时不要使用此读取入口;当前没有经过验证的 thread writer Shortcut"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11690,7 +11690,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"要回复 Thread 或发送新回复时不要使用此只读入口;当前没有经过验证的 thread writer Shortcut"
|
||||
"要回复 Thread 或发送新回复时不要使用此读取入口;当前没有经过验证的 thread writer Shortcut"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11700,34 +11700,34 @@
|
||||
]
|
||||
},
|
||||
"confirmation": {
|
||||
"value": "not_required",
|
||||
"value": "user_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "not_required",
|
||||
"value": "user_required",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"effect": {
|
||||
"value": "read",
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "read",
|
||||
"value": "write",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11754,18 +11754,18 @@
|
||||
]
|
||||
},
|
||||
"idempotency": {
|
||||
"value": "idempotent",
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "idempotent",
|
||||
"value": "unknown",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11774,14 +11774,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "composite",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11790,14 +11790,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely.",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -11822,14 +11822,14 @@
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": true,
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
},
|
||||
{
|
||||
"value": true,
|
||||
@@ -11841,24 +11841,24 @@
|
||||
]
|
||||
},
|
||||
"risk": {
|
||||
"value": "low",
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"resolution": "highest_precedence",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"candidates": [
|
||||
{
|
||||
"value": "low",
|
||||
"value": "medium",
|
||||
"source": "internal/cli/schema_hints/metadata/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
"selected": true,
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger."
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files."
|
||||
}
|
||||
]
|
||||
},
|
||||
"use_when": {
|
||||
"value": [
|
||||
"当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。"
|
||||
"当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11867,7 +11867,7 @@
|
||||
"candidates": [
|
||||
{
|
||||
"value": [
|
||||
"当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。"
|
||||
"当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"source": "internal/cli/schema_hints/selection/chat.json",
|
||||
"precedence": "reviewed_explicit",
|
||||
@@ -11877,11 +11877,11 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"idempotency": "idempotent",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely.",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"risk": "medium",
|
||||
"source_refs": [
|
||||
"ShortcutRegistry:chat +thread-replies",
|
||||
"cobra-help:dws chat +thread-replies",
|
||||
@@ -11890,7 +11890,7 @@
|
||||
"internal/cli/schema_hints/selection/chat.json"
|
||||
],
|
||||
"use_when": [
|
||||
"当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。"
|
||||
"当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
]
|
||||
},
|
||||
"chat +unread-chats": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:ee1c5251720a1cefb87e97f98ee15bca1f929179c9d9a1c598ea5e0c7e281f95",
|
||||
"source_hash": "sha256:179a67a3fd47e2ef1786aaec197d52429a6522bf61a883df70f83febf334387b",
|
||||
"surface_hash": "sha256:698ccdc961c0b745e6524e33ae13d87ef4fcac012a2e833c7a727a79d35004db",
|
||||
"coverage": {
|
||||
"surface_products": 26,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"version": 1,
|
||||
"source_hash": "sha256:ee1c5251720a1cefb87e97f98ee15bca1f929179c9d9a1c598ea5e0c7e281f95",
|
||||
"source_hash": "sha256:179a67a3fd47e2ef1786aaec197d52429a6522bf61a883df70f83febf334387b",
|
||||
"surface_hash": "sha256:698ccdc961c0b745e6524e33ae13d87ef4fcac012a2e833c7a727a79d35004db",
|
||||
"source_files": 160,
|
||||
"hint_files": 54,
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"version": 1,
|
||||
"surface_hash": "sha256:698ccdc961c0b745e6524e33ae13d87ef4fcac012a2e833c7a727a79d35004db",
|
||||
"source_hash": "sha256:7e52ceb70627216cc13cdbad03cb5daec57c03d03cc7f854ed06cadfa984e015",
|
||||
"source_hash": "sha256:2336db101f882586f78322e88d8351ed8b80c9c66c6cb6268cfb7605c04077ed",
|
||||
"catalog": {
|
||||
"agent_metadata": {
|
||||
"products_with_metadata": 26,
|
||||
"source": "embedded-skill-metadata",
|
||||
"source_hash": "sha256:ee1c5251720a1cefb87e97f98ee15bca1f929179c9d9a1c598ea5e0c7e281f95",
|
||||
"source_hash": "sha256:179a67a3fd47e2ef1786aaec197d52429a6522bf61a883df70f83febf334387b",
|
||||
"surface_hash": "sha256:698ccdc961c0b745e6524e33ae13d87ef4fcac012a2e833c7a727a79d35004db",
|
||||
"surface_products": 26,
|
||||
"surface_tools": 818,
|
||||
@@ -9718,19 +9718,19 @@
|
||||
"canonical_path": "chat.shortcut_at_me",
|
||||
"cli_name": "+at-me",
|
||||
"cli_path": "chat +at-me",
|
||||
"confirmation": "not_required",
|
||||
"description": "当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。这是纯只读操作,只做搜索与本地投影,不会发送、撤回或标记任何消息。\n\n参数约束:\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
|
||||
"effect": "read",
|
||||
"idempotency": "idempotent",
|
||||
"confirmation": "user_required",
|
||||
"description": "当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。默认只读且不会发送、撤回或标记任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。\n\n参数约束:\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
|
||||
"effect": "write",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||||
"name": "shortcut_at_me",
|
||||
"primary_cli_path": "chat +at-me",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"risk": "medium",
|
||||
"title": "查最近 @我 的消息(自动算时间窗,投影发送人/时间/内容/会话)",
|
||||
"use_when": [
|
||||
"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。这是纯只读操作,只做搜索与本地投影,不会发送、撤回或标记任何消息。"
|
||||
"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。默认只读且不会发送、撤回或标记任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -10082,19 +10082,19 @@
|
||||
"canonical_path": "chat.shortcut_chat_messages",
|
||||
"cli_name": "+chat-messages",
|
||||
"cli_path": "chat +chat-messages",
|
||||
"confirmation": "not_required",
|
||||
"description": "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。\n\n参数约束:\n - --group、--conversation-id、--id、--user、--open-dingtalk-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
|
||||
"effect": "read",
|
||||
"idempotency": "idempotent",
|
||||
"confirmation": "user_required",
|
||||
"description": "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。\n\n参数约束:\n - --group、--conversation-id、--id、--user、--open-dingtalk-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
|
||||
"effect": "write",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.",
|
||||
"name": "shortcut_chat_messages",
|
||||
"primary_cli_path": "chat +chat-messages",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"risk": "medium",
|
||||
"title": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间",
|
||||
"use_when": [
|
||||
"当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。"
|
||||
"当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -10628,19 +10628,19 @@
|
||||
"canonical_path": "chat.shortcut_messages_mget",
|
||||
"cli_name": "+messages-mget",
|
||||
"cli_path": "chat +messages-mget",
|
||||
"confirmation": "not_required",
|
||||
"description": "当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。\n\n参数约束:\n - --msg-ids 去重后必须包含 1-50 条消息 ID\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
|
||||
"effect": "read",
|
||||
"idempotency": "idempotent",
|
||||
"confirmation": "user_required",
|
||||
"description": "当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。\n\n参数约束:\n - --msg-ids 去重后必须包含 1-50 条消息 ID\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
|
||||
"effect": "write",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||||
"name": "shortcut_messages_mget",
|
||||
"primary_cli_path": "chat +messages-mget",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"risk": "medium",
|
||||
"title": "根据消息 ID 批量查询消息(最多 50 条)",
|
||||
"use_when": [
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。"
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -10836,19 +10836,19 @@
|
||||
"canonical_path": "chat.shortcut_search_msg",
|
||||
"cli_name": "+search-msg",
|
||||
"cli_path": "chat +search-msg",
|
||||
"confirmation": "not_required",
|
||||
"description": "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。\n\n参数约束:\n - 至少指定一个内容、身份、会话或消息类型过滤条件\n - 需与 --end 一起传\n - 需与 --start 一起传\n - --groups、--chat-id 互斥,最多指定一个\n - --senders、--sender 互斥,最多指定一个\n - --at-me、--is-at-me 互斥,最多指定一个\n - --conversation-type、--chat-type 互斥,最多指定一个\n - --limit、--page-size 互斥,最多指定一个\n - --cursor、--page-token 互斥,最多指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
|
||||
"effect": "read",
|
||||
"idempotency": "idempotent",
|
||||
"confirmation": "user_required",
|
||||
"description": "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。\n\n参数约束:\n - 至少指定一个内容、身份、会话或消息类型过滤条件\n - 需与 --end 一起传\n - 需与 --start 一起传\n - --groups、--chat-id 互斥,最多指定一个\n - --senders、--sender 互斥,最多指定一个\n - --at-me、--is-at-me 互斥,最多指定一个\n - --conversation-type、--chat-type 互斥,最多指定一个\n - --limit、--page-size 互斥,最多指定一个\n - --cursor、--page-token 互斥,最多指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
|
||||
"effect": "write",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.",
|
||||
"name": "shortcut_search_msg",
|
||||
"primary_cli_path": "chat +search-msg",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"risk": "medium",
|
||||
"title": "多维搜索消息,可全量翻页并批量富化详情",
|
||||
"use_when": [
|
||||
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。"
|
||||
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
]
|
||||
},
|
||||
{
|
||||
@@ -10883,24 +10883,24 @@
|
||||
"agent_summary_source": "dws-agent-selection/chat",
|
||||
"availability": "available",
|
||||
"avoid_when": [
|
||||
"要回复 Thread 或发送新回复时不要使用此只读入口;当前没有经过验证的 thread writer Shortcut"
|
||||
"要回复 Thread 或发送新回复时不要使用此读取入口;当前没有经过验证的 thread writer Shortcut"
|
||||
],
|
||||
"canonical_path": "chat.shortcut_thread_replies",
|
||||
"cli_name": "+thread-replies",
|
||||
"cli_path": "chat +thread-replies",
|
||||
"confirmation": "not_required",
|
||||
"description": "当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。\n\n参数约束:\n - --thread-id 与兼容参数 --topic-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
|
||||
"effect": "read",
|
||||
"idempotency": "idempotent",
|
||||
"confirmation": "user_required",
|
||||
"description": "当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。\n\n参数约束:\n - --thread-id 与兼容参数 --topic-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸",
|
||||
"effect": "write",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely.",
|
||||
"name": "shortcut_thread_replies",
|
||||
"primary_cli_path": "chat +thread-replies",
|
||||
"reviewed": true,
|
||||
"risk": "low",
|
||||
"risk": "medium",
|
||||
"title": "拉取某条话题消息的全部回复并投影出发言人/文本/时间",
|
||||
"use_when": [
|
||||
"当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。"
|
||||
"当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1153,17 +1153,17 @@
|
||||
"runtime_gate": "none"
|
||||
},
|
||||
"chat.shortcut_chat_messages": {
|
||||
"effect": "read",
|
||||
"risk": "low",
|
||||
"confirmation": "not_required",
|
||||
"idempotency": "idempotent",
|
||||
"effect": "write",
|
||||
"risk": "medium",
|
||||
"confirmation": "user_required",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"availability": "available",
|
||||
"interface_reason": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.",
|
||||
"reviewed": true,
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, shared safe-output validation, resource de-duplication, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable Shortcut, group/direct lower read tools, optional local-file writes, shared safe-output validation, resource de-duplication, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"cli_path": "chat +chat-messages",
|
||||
"runtime_gate": "none"
|
||||
"runtime_gate": "typed_yes"
|
||||
},
|
||||
"chat.shortcut_messages_send": {
|
||||
"effect": "write",
|
||||
@@ -1231,17 +1231,17 @@
|
||||
"runtime_gate": "none"
|
||||
},
|
||||
"chat.shortcut_messages_mget": {
|
||||
"effect": "read",
|
||||
"risk": "low",
|
||||
"confirmation": "not_required",
|
||||
"idempotency": "idempotent",
|
||||
"effect": "write",
|
||||
"risk": "medium",
|
||||
"confirmation": "user_required",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"availability": "available",
|
||||
"interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||||
"reviewed": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"cli_path": "chat +messages-mget",
|
||||
"runtime_gate": "none"
|
||||
"runtime_gate": "typed_yes"
|
||||
},
|
||||
"chat.shortcut_messages_query_send_status": {
|
||||
"effect": "read",
|
||||
@@ -1296,17 +1296,17 @@
|
||||
"runtime_gate": "none"
|
||||
},
|
||||
"chat.shortcut_at_me": {
|
||||
"effect": "read",
|
||||
"risk": "low",
|
||||
"confirmation": "not_required",
|
||||
"idempotency": "idempotent",
|
||||
"effect": "write",
|
||||
"risk": "medium",
|
||||
"confirmation": "user_required",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"availability": "available",
|
||||
"interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.",
|
||||
"reviewed": true,
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command: publishes the executable risk gate and composite interface without inventing a direct MCP identity.",
|
||||
"review_reason": "Reviewed against the built-in Shortcut registry, mounted Cobra command, and optional local-file writes; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"cli_path": "chat +at-me",
|
||||
"runtime_gate": "none"
|
||||
"runtime_gate": "typed_yes"
|
||||
},
|
||||
"chat.shortcut_broadcast": {
|
||||
"effect": "write",
|
||||
@@ -1361,17 +1361,17 @@
|
||||
"runtime_gate": "none"
|
||||
},
|
||||
"chat.shortcut_search_msg": {
|
||||
"effect": "read",
|
||||
"risk": "low",
|
||||
"confirmation": "not_required",
|
||||
"idempotency": "idempotent",
|
||||
"effect": "write",
|
||||
"risk": "medium",
|
||||
"confirmation": "user_required",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"availability": "available",
|
||||
"interface_reason": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.",
|
||||
"reviewed": true,
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, shared safe-output validation, and optional per-resource download ledger.",
|
||||
"review_reason": "Reviewed against the executable advanced-search flags, page and enrichment failure ledgers, optional local-file writes, shared safe-output validation, and per-resource download ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"cli_path": "chat +search-msg",
|
||||
"runtime_gate": "none"
|
||||
"runtime_gate": "typed_yes"
|
||||
},
|
||||
"chat.shortcut_send_to_group": {
|
||||
"effect": "write",
|
||||
@@ -1387,17 +1387,17 @@
|
||||
"runtime_gate": "typed_yes"
|
||||
},
|
||||
"chat.shortcut_thread_replies": {
|
||||
"effect": "read",
|
||||
"risk": "low",
|
||||
"confirmation": "not_required",
|
||||
"idempotency": "idempotent",
|
||||
"effect": "write",
|
||||
"risk": "medium",
|
||||
"confirmation": "user_required",
|
||||
"idempotency": "unknown",
|
||||
"interface_mode": "composite",
|
||||
"availability": "available",
|
||||
"interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely.",
|
||||
"reviewed": true,
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, group fallback for resource context, and per-resource failure ledger.",
|
||||
"review_reason": "Reviewed against the executable thread/topic alias contract, real list_topic_replies mapping, optional local-file writes, group fallback for resource context, and per-resource failure ledger; the static runtime risk is conservatively write because --download-resources writes files.",
|
||||
"cli_path": "chat +thread-replies",
|
||||
"runtime_gate": "none"
|
||||
"runtime_gate": "typed_yes"
|
||||
},
|
||||
"chat.shortcut_unread_chats": {
|
||||
"effect": "read",
|
||||
|
||||
@@ -2113,7 +2113,7 @@
|
||||
"chat.shortcut_chat_messages": {
|
||||
"agent_summary": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间",
|
||||
"use_when": [
|
||||
"当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。"
|
||||
"当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"要跨多个会话按关键词、发送者或消息类型检索时使用 +search-msg;已有一批精确消息 ID 时使用 +messages-mget"
|
||||
@@ -2230,7 +2230,7 @@
|
||||
"chat.shortcut_messages_mget": {
|
||||
"agent_summary": "根据消息 ID 批量查询消息(最多 50 条)",
|
||||
"use_when": [
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。"
|
||||
"当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
@@ -2325,7 +2325,7 @@
|
||||
"chat.shortcut_at_me": {
|
||||
"agent_summary": "查最近 @我 的消息(自动算时间窗,投影发送人/时间/内容/会话)",
|
||||
"use_when": [
|
||||
"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。这是纯只读操作,只做搜索与本地投影,不会发送、撤回或标记任何消息。"
|
||||
"当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息,再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。默认只读且不会发送、撤回或标记任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
|
||||
@@ -2422,7 +2422,7 @@
|
||||
"chat.shortcut_search_msg": {
|
||||
"agent_summary": "多维搜索消息,可全量翻页并批量富化详情",
|
||||
"use_when": [
|
||||
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。"
|
||||
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"
|
||||
@@ -2461,10 +2461,10 @@
|
||||
"chat.shortcut_thread_replies": {
|
||||
"agent_summary": "拉取某条话题消息的全部回复并投影出发言人/文本/时间",
|
||||
"use_when": [
|
||||
"当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。"
|
||||
"当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么),而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表,可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。"
|
||||
],
|
||||
"avoid_when": [
|
||||
"要回复 Thread 或发送新回复时不要使用此只读入口;当前没有经过验证的 thread writer Shortcut"
|
||||
"要回复 Thread 或发送新回复时不要使用此读取入口;当前没有经过验证的 thread writer Shortcut"
|
||||
],
|
||||
"examples": [
|
||||
"dws chat +thread-replies --group <openConversationId> --thread-id <threadId>",
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
|
||||
@@ -289,6 +290,47 @@ func normalizeAtPlaceholders(text string, ids []string, wrapAngle bool) string {
|
||||
return text
|
||||
}
|
||||
|
||||
// NormalizeMessageMentions applies the placeholder convention required by the
|
||||
// selected sender identity and ensures a declared @all is present in the body.
|
||||
// Current-user messages use <@id>/<@all>; bot and webhook messages use
|
||||
// @id/@mobile/@all.
|
||||
func NormalizeMessageMentions(text string, ids []string, atAll, wrapAngle bool) string {
|
||||
text = normalizeAtPlaceholders(text, ids, wrapAngle)
|
||||
allPlaceholder := "@all"
|
||||
if wrapAngle {
|
||||
text = normalizeAtPlaceholders(text, []string{"all"}, true)
|
||||
allPlaceholder = "<@all>"
|
||||
} else {
|
||||
text = strings.ReplaceAll(text, "<@all>", "@all")
|
||||
}
|
||||
if atAll && !containsMessageMention(text, allPlaceholder) {
|
||||
text = allPlaceholder + " " + text
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func containsMessageMention(text, placeholder string) bool {
|
||||
if strings.HasPrefix(placeholder, "<") {
|
||||
return strings.Contains(text, placeholder)
|
||||
}
|
||||
for searchFrom := 0; ; {
|
||||
offset := strings.Index(text[searchFrom:], placeholder)
|
||||
if offset < 0 {
|
||||
return false
|
||||
}
|
||||
end := searchFrom + offset + len(placeholder)
|
||||
if end == len(text) {
|
||||
return true
|
||||
}
|
||||
next, _ := utf8.DecodeRuneInString(text[end:])
|
||||
if !unicode.IsLetter(next) && !unicode.IsDigit(next) &&
|
||||
next != '_' && next != '-' {
|
||||
return true
|
||||
}
|
||||
searchFrom = end
|
||||
}
|
||||
}
|
||||
|
||||
func resolveOpenDingTalkID(ctx context.Context, value string) (string, error) {
|
||||
ids, err := resolveOpenDingTalkIDs(ctx, []string{value})
|
||||
if err != nil {
|
||||
|
||||
@@ -114,6 +114,18 @@ func TestCrossPlatformCoverageChatDirectionAndScalarCoverage(t *testing.T) {
|
||||
for _, wrap := range []bool{true, false} {
|
||||
_ = normalizeAtPlaceholders("hello @u1 <@u2>", []string{"", "u1", "u2"}, wrap)
|
||||
}
|
||||
if got := NormalizeMessageMentions("hello @u1", []string{"u1"}, true, true); got != "<@all> hello <@u1>" {
|
||||
t.Fatalf("current-user mention normalization = %q", got)
|
||||
}
|
||||
if got := NormalizeMessageMentions("<@all> <@u1>", []string{"u1"}, true, false); got != "@all @u1" {
|
||||
t.Fatalf("bot mention normalization = %q", got)
|
||||
}
|
||||
if got := NormalizeMessageMentions("@alliance hello", nil, true, false); got != "@all @alliance hello" {
|
||||
t.Fatalf("bot @all token detection = %q", got)
|
||||
}
|
||||
if got := NormalizeMessageMentions("hello @all", nil, true, false); got != "hello @all" {
|
||||
t.Fatalf("trailing bot @all detection = %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageChatContactMappingCoverage(t *testing.T) {
|
||||
|
||||
@@ -526,8 +526,8 @@ var MessagesMget = shortcut.Shortcut{
|
||||
Command: "+messages-mget",
|
||||
Product: "im",
|
||||
Description: "根据消息 ID 批量查询消息(最多 50 条)",
|
||||
Intent: "当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。",
|
||||
Risk: shortcut.RiskRead,
|
||||
Intent: "当你已有一批消息 openMsgId、需要批量取回完整详情、reaction 和可执行资源引用时使用;一次最多 50 条。--download-resources 可把所有可识别 mediaId/fileId 安全下载到工作目录内,并逐资源返回成功/失败 ledger。默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。",
|
||||
Risk: shortcut.RiskWrite,
|
||||
Flags: append([]shortcut.Flag{
|
||||
{Name: "msg-ids", Type: shortcut.FlagStringSlice, Desc: "消息 openMsgId 列表;--msg-ids 去重后必须包含 1-50 条消息 ID", Required: true},
|
||||
{Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出消息 reaction(默认输出)"},
|
||||
@@ -620,7 +620,7 @@ func DownloadMessageResources(
|
||||
) map[string]any {
|
||||
resources := make([]map[string]any, 0)
|
||||
for _, message := range messages {
|
||||
resources = append(resources, chatmsg.Resources(message)...)
|
||||
resources = append(resources, chatmsg.ResourcesDeep(message)...)
|
||||
}
|
||||
discoveredCount := len(resources)
|
||||
uniqueResources := make([]map[string]any, 0, len(resources))
|
||||
@@ -635,6 +635,10 @@ func DownloadMessageResources(
|
||||
messageID = ""
|
||||
}
|
||||
key := strings.ToLower(resourceType) + "\x00" + resourceID
|
||||
if strings.EqualFold(resourceType, "mediaId") {
|
||||
conversationID := strings.TrimSpace(fmt.Sprint(arguments["open-conversation-id"]))
|
||||
key += "\x00" + messageID + "\x00" + conversationID
|
||||
}
|
||||
if resourceID != "" && resourceID != "<nil>" {
|
||||
if seen[key] {
|
||||
continue
|
||||
|
||||
@@ -39,10 +39,10 @@ func TestCrossPlatformCoverageListMessageRichProjection(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageMgetResourceDownloadOutcomes(t *testing.T) {
|
||||
baseArgs := []string{"chat", "+messages-mget", "--msg-ids", "msg", "--download-resources"}
|
||||
baseArgs := []string{"chat", "+messages-mget", "--msg-ids", "msg", "--download-resources", "--yes"}
|
||||
readyMget := `{"result":[{"openMessageId":"msg","openConversationId":"cid","content":"{\"mediaId\":\"@file\"}"}]}`
|
||||
missingContextMget := `{"result":[{"content":"{\"mediaId\":\"@file\"}"}]}`
|
||||
validInfo := `{"result":{"resourceUrl":"https://example.test/resource.bin"}}`
|
||||
validInfo := `{"result":{"resourceUrl":"https://download.dingtalk.com/resource.bin"}}`
|
||||
|
||||
t.Run("dry run", func(t *testing.T) {
|
||||
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{
|
||||
|
||||
@@ -234,7 +234,7 @@ func TestCrossPlatformCoverageFlagAndMgetValidation(t *testing.T) {
|
||||
fake := &larkAlignmentCaller{failProductTool: "im/list_messages_by_ids"}
|
||||
helpers.InitDeps(fake)
|
||||
root := newPlatformCoverageRoot()
|
||||
root.SetArgs([]string{"chat", "+messages-mget", "--msg-ids", "msg"})
|
||||
root.SetArgs([]string{"chat", "+messages-mget", "--msg-ids", "msg", "--yes"})
|
||||
if err := root.Execute(); err == nil {
|
||||
t.Fatal("mget lower error was swallowed")
|
||||
}
|
||||
|
||||
@@ -463,7 +463,7 @@ func TestCrossPlatformCoverageFindCardBizIDResponseShapes(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageMessageResourceDownloadDeduplicatesAndUsesFallbackConversation(t *testing.T) {
|
||||
func TestCrossPlatformCoverageMessageResourceDownloadKeepsNestedMessageContext(t *testing.T) {
|
||||
resetResourceDownloadHooks(t)
|
||||
t.Chdir(t.TempDir())
|
||||
resourceDownload = func(
|
||||
@@ -480,12 +480,16 @@ func TestCrossPlatformCoverageMessageResourceDownloadDeduplicatesAndUsesFallback
|
||||
return 7, nil
|
||||
}
|
||||
fake := &larkAlignmentCaller{responses: map[string]string{
|
||||
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://example.test/resource.bin"}}`,
|
||||
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://download.dingtalk.com/resource.bin"}}`,
|
||||
}}
|
||||
helpers.InitDeps(fake)
|
||||
message := map[string]any{
|
||||
"openMessageId": "msg",
|
||||
"content": `{"mediaId":"@same","nested":{"mediaId":"@same"}}`,
|
||||
"quotedMessage": map[string]any{
|
||||
"openMessageId": "msg-quoted",
|
||||
"content": `{"mediaId":"@quoted"}`,
|
||||
},
|
||||
}
|
||||
secondMessage := map[string]any{
|
||||
"openMessageId": "msg-2",
|
||||
@@ -497,7 +501,7 @@ func TestCrossPlatformCoverageMessageResourceDownloadDeduplicatesAndUsesFallback
|
||||
Command: "+gap-resource-download",
|
||||
Flags: MessageResourceDownloadFlags(),
|
||||
Execute: func(rt *shortcut.RuntimeContext) error {
|
||||
ledger = DownloadMessageResources(rt, []map[string]any{message, secondMessage}, "cid-fallback")
|
||||
ledger = DownloadMessageResources(rt, []map[string]any{message, message, secondMessage}, "cid-fallback")
|
||||
return nil
|
||||
},
|
||||
})
|
||||
@@ -506,13 +510,25 @@ func TestCrossPlatformCoverageMessageResourceDownloadDeduplicatesAndUsesFallback
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ledger["discoveredCount"] != 2 || ledger["requestedCount"] != 1 ||
|
||||
ledger["deduplicatedCount"] != 1 || ledger["downloadedCount"] != 1 {
|
||||
if ledger["discoveredCount"] != 5 || ledger["requestedCount"] != 3 ||
|
||||
ledger["deduplicatedCount"] != 2 || ledger["downloadedCount"] != 3 {
|
||||
t.Fatalf("download ledger = %#v", ledger)
|
||||
}
|
||||
if len(fake.calls) != 1 || fake.calls[0].args["openConversationId"] != "cid-fallback" {
|
||||
if len(fake.calls) != 3 {
|
||||
t.Fatalf("resource lookup = %#v", fake.calls)
|
||||
}
|
||||
gotMessageIDs := map[string]bool{}
|
||||
for _, call := range fake.calls {
|
||||
if call.args["openConversationId"] != "cid-fallback" {
|
||||
t.Errorf("resource conversation = %#v", call.args)
|
||||
}
|
||||
gotMessageIDs[call.args["openMessageId"].(string)] = true
|
||||
}
|
||||
for _, messageID := range []string{"msg", "msg-quoted", "msg-2"} {
|
||||
if !gotMessageIDs[messageID] {
|
||||
t.Errorf("missing resource lookup for %q: %#v", messageID, fake.calls)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageMessageFileResourceDownloadUsesDriveAndPreservesName(t *testing.T) {
|
||||
@@ -532,7 +548,7 @@ func TestCrossPlatformCoverageMessageFileResourceDownloadUsesDriveAndPreservesNa
|
||||
return int64(len("drive-file")), nil
|
||||
}
|
||||
fake := &larkAlignmentCaller{responses: map[string]string{
|
||||
"drive/download_file": `{"result":{"downloadUrl":"https://example.test/opaque","fileName":"fixture.txt"}}`,
|
||||
"drive/download_file": `{"result":{"downloadUrl":"https://download.dingtalk.com/opaque","fileName":"fixture.txt"}}`,
|
||||
}}
|
||||
helpers.InitDeps(fake)
|
||||
|
||||
@@ -542,6 +558,7 @@ func TestCrossPlatformCoverageMessageFileResourceDownloadUsesDriveAndPreservesNa
|
||||
"--type", "fileId",
|
||||
"--resource-id", "drive-file",
|
||||
"--output", "./downloads/",
|
||||
"--yes",
|
||||
})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -601,7 +618,7 @@ func TestCrossPlatformCoverageMessageResourceDownloadDisambiguatesSameNames(t *t
|
||||
return 1, nil
|
||||
}
|
||||
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{
|
||||
"drive/download_file": `{"result":{"downloadUrl":"https://example.test/opaque","fileName":"fixture.txt"}}`,
|
||||
"drive/download_file": `{"result":{"downloadUrl":"https://download.dingtalk.com/opaque","fileName":"fixture.txt"}}`,
|
||||
}})
|
||||
|
||||
var ledger map[string]any
|
||||
|
||||
@@ -117,27 +117,35 @@ func TestMessagesSendRoutesIdentitySpecificTransports(t *testing.T) {
|
||||
product string
|
||||
tool string
|
||||
want map[string]any
|
||||
bodyKey string
|
||||
body string
|
||||
}{
|
||||
{
|
||||
name: "user",
|
||||
args: []string{"chat", "+messages-send", "--as", "user", "--chat-id", "cid", "--markdown", "hello", "--idempotency-key", "u1", "--yes"},
|
||||
args: []string{"chat", "+messages-send", "--as", "user", "--chat-id", "cid", "--markdown", "hello @D1", "--at-open-dingtalk-ids", "D1", "--at-all", "--idempotency-key", "u1", "--yes"},
|
||||
product: "chat",
|
||||
tool: "send_personal_message",
|
||||
want: map[string]any{"openConversationId": "cid", "msgType": "markdown", "uuid": "u1"},
|
||||
bodyKey: "content",
|
||||
body: "<@all> hello <@D1>",
|
||||
},
|
||||
{
|
||||
name: "bot",
|
||||
args: []string{"chat", "+messages-send", "--identity", "bot", "--robot-code", "robot", "--group", "cid", "--text", "hello", "--at-all", "--yes"},
|
||||
args: []string{"chat", "+messages-send", "--identity", "bot", "--robot-code", "robot", "--group", "cid", "--text", "<@u1> hello", "--at-user-ids", "u1", "--at-all", "--yes"},
|
||||
product: "bot",
|
||||
tool: "send_robot_group_message",
|
||||
want: map[string]any{"robotCode": "robot", "openConversationId": "cid", "isAtAll": "true"},
|
||||
bodyKey: "markdown",
|
||||
body: "@all @u1 hello",
|
||||
},
|
||||
{
|
||||
name: "webhook",
|
||||
args: []string{"chat", "+messages-send", "--identity", "webhook", "--webhook-token", "token", "--text", "hello", "--at-all", "--yes"},
|
||||
args: []string{"chat", "+messages-send", "--identity", "webhook", "--webhook-token", "token", "--text", "<@13800000000> hello", "--at-mobiles", "13800000000", "--at-all", "--yes"},
|
||||
product: "bot",
|
||||
tool: "send_message_by_custom_robot",
|
||||
want: map[string]any{"robotToken": "token", "isAtAll": true},
|
||||
bodyKey: "text",
|
||||
body: "@all @13800000000 hello",
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
@@ -161,6 +169,17 @@ func TestMessagesSendRoutesIdentitySpecificTransports(t *testing.T) {
|
||||
t.Errorf("%s = %#v, want %#v", key, call.args[key], want)
|
||||
}
|
||||
}
|
||||
if tt.bodyKey == "content" {
|
||||
var content map[string]string
|
||||
if err := json.Unmarshal([]byte(call.args[tt.bodyKey].(string)), &content); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if content["text"] != tt.body {
|
||||
t.Errorf("content text = %q, want %q", content["text"], tt.body)
|
||||
}
|
||||
} else if call.args[tt.bodyKey] != tt.body {
|
||||
t.Errorf("%s = %#v, want %q", tt.bodyKey, call.args[tt.bodyKey], tt.body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
@@ -58,7 +59,7 @@ var MessagesResourceDownload = shortcut.Shortcut{
|
||||
Intent: "当你需要拿到消息里的实际图片、视频、语音或钉盘文件,而不只是资源 ID 时使用;" +
|
||||
"mediaId 用消息和会话身份换取下载地址,fileId 复用钉盘下载能力,再安全写入工作目录内的相对路径。" +
|
||||
"默认不覆盖已有文件,只有显式传 --overwrite 才覆盖。",
|
||||
Risk: shortcut.RiskRead,
|
||||
Risk: shortcut.RiskWrite,
|
||||
Flags: []shortcut.Flag{
|
||||
{Name: "type", Type: shortcut.FlagString, Default: "mediaId", Desc: "资源类型", Enum: []string{"mediaId", "fileId"}},
|
||||
{Name: "resource-id", Type: shortcut.FlagString, Desc: "消息中的 mediaId 或 fileId", Required: true},
|
||||
@@ -233,10 +234,12 @@ func resourceDownloadInfo(data map[string]any) (string, map[string]string, error
|
||||
parsed.Scheme = "https"
|
||||
resourceURL = parsed.String()
|
||||
}
|
||||
if parsed.Scheme != "https" {
|
||||
return "", nil, apperrors.NewAPI(
|
||||
"资源下载接口未返回合法的 HTTPS 下载地址")
|
||||
parsed, err = validateResourceDownloadURL(resourceURL)
|
||||
if err != nil {
|
||||
return "", nil, apperrors.NewAPI(fmt.Sprintf(
|
||||
"资源下载接口返回了不受信任的下载地址: %v", err))
|
||||
}
|
||||
resourceURL = parsed.String()
|
||||
|
||||
headers := map[string]string{}
|
||||
if values, ok := data["headers"].(map[string]any); ok {
|
||||
@@ -263,7 +266,45 @@ func resourceDownloadPreferredName(data map[string]any) string {
|
||||
|
||||
func isAliyunOSSHost(host string) bool {
|
||||
host = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(host), "."))
|
||||
return host == "aliyuncs.com" || strings.HasSuffix(host, ".aliyuncs.com")
|
||||
if !strings.HasSuffix(host, ".aliyuncs.com") {
|
||||
return false
|
||||
}
|
||||
for _, label := range strings.Split(strings.TrimSuffix(host, ".aliyuncs.com"), ".") {
|
||||
if (label == "oss" || strings.HasPrefix(label, "oss-")) &&
|
||||
!strings.Contains(label, "internal") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func validateResourceDownloadURL(rawURL string) (*url.URL, error) {
|
||||
parsed, err := url.Parse(strings.TrimSpace(rawURL))
|
||||
if err != nil ||
|
||||
parsed.Scheme != "https" ||
|
||||
strings.TrimSpace(parsed.Host) == "" ||
|
||||
parsed.User != nil {
|
||||
return nil, apperrors.NewValidation("资源下载地址必须是受信任域名上的 HTTPS URL")
|
||||
}
|
||||
host := strings.ToLower(strings.TrimSuffix(strings.TrimSpace(parsed.Hostname()), "."))
|
||||
if host == "" || net.ParseIP(host) != nil || !isResourceDownloadAllowedHost(host) {
|
||||
return nil, apperrors.NewValidation(fmt.Sprintf(
|
||||
"资源下载地址域名 %q 不属于受信任的钉钉或 OSS 域名", host))
|
||||
}
|
||||
if port := parsed.Port(); port != "" && port != "443" {
|
||||
return nil, apperrors.NewValidation("资源下载地址只允许使用 HTTPS 默认端口")
|
||||
}
|
||||
return parsed, nil
|
||||
}
|
||||
|
||||
func isResourceDownloadAllowedHost(host string) bool {
|
||||
host = strings.ToLower(strings.TrimSuffix(strings.TrimSpace(host), "."))
|
||||
// Lower tools may return signed headers with the URL. Keep those headers
|
||||
// confined to platform-owned public download families; extend this list
|
||||
// only after observing another official production download host.
|
||||
return isAliyunOSSHost(host) ||
|
||||
host == "dingtalk.com" ||
|
||||
strings.HasSuffix(host, ".dingtalk.com")
|
||||
}
|
||||
|
||||
func resolveResourceDownloadPath(
|
||||
@@ -403,22 +444,34 @@ func downloadResourceAtomically(
|
||||
if client == nil {
|
||||
client = &http.Client{Timeout: resourceDownloadTimeout}
|
||||
}
|
||||
parsedResourceURL, err := validateResourceDownloadURL(resourceURL)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
clientCopy := *client
|
||||
client = &clientCopy
|
||||
originalRedirect := client.CheckRedirect
|
||||
client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
|
||||
if req.URL.Scheme != "https" || strings.TrimSpace(req.URL.Host) == "" {
|
||||
return apperrors.NewValidation("资源下载重定向必须使用 HTTPS")
|
||||
if _, redirectErr := validateResourceDownloadURL(req.URL.String()); redirectErr != nil {
|
||||
return apperrors.NewValidation(fmt.Sprintf(
|
||||
"资源下载重定向指向了不受信任的地址: %v", redirectErr))
|
||||
}
|
||||
if len(via) >= 10 {
|
||||
return apperrors.NewAPI("资源下载重定向次数过多")
|
||||
}
|
||||
if len(via) > 0 &&
|
||||
!strings.EqualFold(req.URL.Hostname(), via[len(via)-1].URL.Hostname()) {
|
||||
for key := range headers {
|
||||
req.Header.Del(key)
|
||||
}
|
||||
}
|
||||
if originalRedirect != nil {
|
||||
return originalRedirect(req, via)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
request, err := http.NewRequestWithContext(ctx, http.MethodGet, resourceURL, nil)
|
||||
request, err := http.NewRequestWithContext(
|
||||
ctx, http.MethodGet, parsedResourceURL.String(), nil)
|
||||
if err != nil {
|
||||
return 0, apperrors.NewValidation(fmt.Sprintf("创建资源下载请求失败: %v", err))
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -58,6 +57,7 @@ func TestCrossPlatformCoverageResourceDownloadCommandOutcomes(t *testing.T) {
|
||||
"--resource-id", "@image",
|
||||
"--message-id", "msg",
|
||||
"--open-conversation-id", "cid",
|
||||
"--yes",
|
||||
}
|
||||
t.Run("dry run", func(t *testing.T) {
|
||||
helpers.InitDeps(&larkAlignmentCaller{})
|
||||
@@ -89,7 +89,7 @@ func TestCrossPlatformCoverageResourceDownloadCommandOutcomes(t *testing.T) {
|
||||
resetResourceDownloadHooks(t)
|
||||
resourceGetwd = func() (string, error) { return "", errors.New("getwd") }
|
||||
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{
|
||||
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://example.test/file"}}`,
|
||||
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://download.dingtalk.com/file"}}`,
|
||||
}})
|
||||
root := newPlatformCoverageRoot()
|
||||
root.SetArgs(baseArgs)
|
||||
@@ -101,7 +101,7 @@ func TestCrossPlatformCoverageResourceDownloadCommandOutcomes(t *testing.T) {
|
||||
resetResourceDownloadHooks(t)
|
||||
resourceAbs = func(string) (string, error) { return "", errors.New("path") }
|
||||
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{
|
||||
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://example.test/file"}}`,
|
||||
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://download.dingtalk.com/file"}}`,
|
||||
}})
|
||||
root := newPlatformCoverageRoot()
|
||||
root.SetArgs(baseArgs)
|
||||
@@ -132,7 +132,7 @@ func TestCrossPlatformCoverageResourceDownloadCommandOutcomes(t *testing.T) {
|
||||
resetResourceDownloadHooks(t)
|
||||
resourceDownload = tc.download
|
||||
helpers.InitDeps(&larkAlignmentCaller{responses: map[string]string{
|
||||
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://example.test/file"}}`,
|
||||
"im/get_resource_download_url": `{"result":{"resourceUrl":"https://download.dingtalk.com/file"}}`,
|
||||
}})
|
||||
root := newPlatformCoverageRoot()
|
||||
root.SetArgs(append(append([]string{}, baseArgs...), "--output", filepath.Join(t.TempDir(), "file")))
|
||||
@@ -164,7 +164,7 @@ func TestCrossPlatformCoverageResourceDownloadValidationAndInfo(t *testing.T) {
|
||||
}
|
||||
}
|
||||
resourceURL, headers, err := resourceDownloadInfo(map[string]any{
|
||||
"resourceUrl": "https://example.test/file",
|
||||
"resourceUrl": "https://download.dingtalk.com/file",
|
||||
"headers": map[string]any{
|
||||
"": "ignored",
|
||||
"X-Count": 3,
|
||||
@@ -175,14 +175,29 @@ func TestCrossPlatformCoverageResourceDownloadValidationAndInfo(t *testing.T) {
|
||||
t.Fatalf("resource info = %q %#v %v", resourceURL, headers, err)
|
||||
}
|
||||
for host, want := range map[string]bool{
|
||||
"ALIYUNCS.COM.": true,
|
||||
"bucket.aliyuncs.com": true,
|
||||
"example.com": false,
|
||||
"ALIYUNCS.COM.": false,
|
||||
"bucket.aliyuncs.com": false,
|
||||
"bucket.oss-cn-hangzhou.aliyuncs.com": true,
|
||||
"bucket.oss-internal.aliyuncs.com": false,
|
||||
"bucket.oss-cn-hangzhou-internal.aliyuncs.com": false,
|
||||
"example.com": false,
|
||||
} {
|
||||
if got := isAliyunOSSHost(host); got != want {
|
||||
t.Errorf("isAliyunOSSHost(%q) = %v, want %v", host, got, want)
|
||||
}
|
||||
}
|
||||
for host, want := range map[string]bool{
|
||||
"DINGTALK.COM.": true,
|
||||
"download.dingtalk.com": true,
|
||||
"bucket.oss-cn-hangzhou.aliyuncs.com": true,
|
||||
"aliyuncs.com.evil.test": false,
|
||||
"evildingtalk.com": false,
|
||||
"download.example.invalid": false,
|
||||
} {
|
||||
if got := isResourceDownloadAllowedHost(host); got != want {
|
||||
t.Errorf("isResourceDownloadAllowedHost(%q) = %v, want %v", host, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageResourceDownloadPathErrors(t *testing.T) {
|
||||
@@ -375,63 +390,94 @@ func TestCrossPlatformCoverageDownloadResourceHTTPFailures(t *testing.T) {
|
||||
errorClient := &http.Client{Transport: resourceRoundTripper(func(*http.Request) (*http.Response, error) {
|
||||
return nil, errors.New("transport")
|
||||
})}
|
||||
if _, err := downloadResourceAtomically(context.Background(), errorClient, "https://example.test/file", nil, dest, false); err == nil {
|
||||
if _, err := downloadResourceAtomically(context.Background(), errorClient, "https://download.dingtalk.com/file", nil, dest, false); err == nil {
|
||||
t.Fatal("transport error was swallowed")
|
||||
}
|
||||
if _, err := downloadResourceAtomically(context.Background(), resourceResponseClient(500, "", 0), "https://example.test/file", nil, dest, false); err == nil {
|
||||
if _, err := downloadResourceAtomically(context.Background(), resourceResponseClient(500, "", 0), "https://download.dingtalk.com/file", nil, dest, false); err == nil {
|
||||
t.Fatal("HTTP 500 was accepted")
|
||||
}
|
||||
if _, err := downloadResourceAtomically(context.Background(), resourceResponseClient(200, "x", 2), "https://example.test/file", nil, dest, false); err == nil {
|
||||
if _, err := downloadResourceAtomically(context.Background(), resourceResponseClient(200, "x", 2), "https://download.dingtalk.com/file", nil, dest, false); err == nil {
|
||||
t.Fatal("content-length mismatch was accepted")
|
||||
}
|
||||
|
||||
plain := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, request *http.Request) {
|
||||
if request.Header.Get("X-Test") != "ok" {
|
||||
t.Errorf("missing forwarded header")
|
||||
}
|
||||
_, _ = w.Write([]byte("body"))
|
||||
}))
|
||||
t.Cleanup(plain.Close)
|
||||
nilClientDest := filepath.Join(t.TempDir(), "nil-client")
|
||||
if _, err := downloadResourceAtomically(context.Background(), nil, plain.URL, map[string]string{"X-Test": "ok"}, nilClientDest, true); err != nil {
|
||||
t.Fatal(err)
|
||||
if _, err := downloadResourceAtomically(
|
||||
context.Background(), nil, "https://evil.example/file",
|
||||
map[string]string{"X-Test": "ok"}, nilClientDest, true,
|
||||
); err == nil {
|
||||
t.Fatal("nil-client path accepted an untrusted URL")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDownloadResourceRedirectGuards(t *testing.T) {
|
||||
httpRedirect := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.Redirect(w, &http.Request{}, "http://example.test/file", http.StatusFound)
|
||||
}))
|
||||
t.Cleanup(httpRedirect.Close)
|
||||
httpRedirect := &http.Client{Transport: resourceRoundTripper(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusFound,
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Header: http.Header{"Location": []string{"http://evil.example/file"}},
|
||||
}, nil
|
||||
})}
|
||||
if _, err := downloadResourceAtomically(
|
||||
context.Background(), httpRedirect.Client(), httpRedirect.URL, nil,
|
||||
context.Background(), httpRedirect, "https://download.dingtalk.com/start", nil,
|
||||
filepath.Join(t.TempDir(), "http-redirect"), false,
|
||||
); err == nil {
|
||||
t.Fatal("HTTP redirect was accepted")
|
||||
}
|
||||
|
||||
var loop *httptest.Server
|
||||
loop = httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
http.Redirect(w, &http.Request{}, loop.URL, http.StatusFound)
|
||||
}))
|
||||
t.Cleanup(loop.Close)
|
||||
loop := &http.Client{Transport: resourceRoundTripper(func(*http.Request) (*http.Response, error) {
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusFound,
|
||||
Body: io.NopCloser(strings.NewReader("")),
|
||||
Header: http.Header{"Location": []string{"https://download.dingtalk.com/loop"}},
|
||||
}, nil
|
||||
})}
|
||||
if _, err := downloadResourceAtomically(
|
||||
context.Background(), loop.Client(), loop.URL, nil,
|
||||
context.Background(), loop, "https://download.dingtalk.com/loop", nil,
|
||||
filepath.Join(t.TempDir(), "loop"), false,
|
||||
); err == nil {
|
||||
t.Fatal("redirect loop was accepted")
|
||||
}
|
||||
|
||||
original := loop.Client()
|
||||
original := *loop
|
||||
original.CheckRedirect = func(*http.Request, []*http.Request) error {
|
||||
return errors.New("original redirect policy")
|
||||
}
|
||||
if _, err := downloadResourceAtomically(
|
||||
context.Background(), original, loop.URL, nil,
|
||||
context.Background(), &original, "https://download.dingtalk.com/loop", nil,
|
||||
filepath.Join(t.TempDir(), "original"), false,
|
||||
); err == nil {
|
||||
t.Fatal("original redirect rejection was swallowed")
|
||||
}
|
||||
|
||||
redirectCount := 0
|
||||
crossHost := &http.Client{Transport: resourceRoundTripper(func(request *http.Request) (*http.Response, error) {
|
||||
redirectCount++
|
||||
if redirectCount == 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/final"}},
|
||||
}, nil
|
||||
}
|
||||
if request.Header.Get("X-Resource-Token") != "" {
|
||||
t.Fatal("server-supplied header leaked across download domains")
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader("ok")),
|
||||
ContentLength: 2,
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
if _, err := downloadResourceAtomically(
|
||||
context.Background(), crossHost, "https://download.dingtalk.com/start",
|
||||
map[string]string{"X-Resource-Token": "secret"},
|
||||
filepath.Join(t.TempDir(), "cross-host"), false,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageDownloadResourceFileFailures(t *testing.T) {
|
||||
@@ -439,7 +485,7 @@ func TestCrossPlatformCoverageDownloadResourceFileFailures(t *testing.T) {
|
||||
run := func(t *testing.T, overwrite bool) error {
|
||||
t.Helper()
|
||||
_, err := downloadResourceAtomically(
|
||||
context.Background(), client, "https://example.test/file", nil,
|
||||
context.Background(), client, "https://download.dingtalk.com/file", nil,
|
||||
filepath.Join(t.TempDir(), "resource"), overwrite)
|
||||
return err
|
||||
}
|
||||
@@ -516,7 +562,7 @@ func TestCrossPlatformCoverageDownloadResourceCopySuccessWithBuffer(t *testing.T
|
||||
dest := filepath.Join(t.TempDir(), "resource")
|
||||
if _, err := downloadResourceAtomically(
|
||||
context.Background(), resourceResponseClient(200, "ok", 2),
|
||||
"https://example.test/file", nil, dest, true,
|
||||
"https://download.dingtalk.com/file", nil, dest, true,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -15,18 +15,18 @@ package chat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestResourceDownloadInfo(t *testing.T) {
|
||||
func TestCrossPlatformCoverageResourceDownloadInfo(t *testing.T) {
|
||||
url, headers, err := resourceDownloadInfo(map[string]any{
|
||||
"result": map[string]any{
|
||||
"resourceUrl": []any{"https://download.example.test/path/image.png"},
|
||||
"resourceUrl": []any{"https://download.dingtalk.com/path/image.png"},
|
||||
"headers": map[string]any{
|
||||
"X-Test": "value",
|
||||
},
|
||||
@@ -35,7 +35,7 @@ func TestResourceDownloadInfo(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if url != "https://download.example.test/path/image.png" {
|
||||
if url != "https://download.dingtalk.com/path/image.png" {
|
||||
t.Fatalf("url = %q", url)
|
||||
}
|
||||
if headers["X-Test"] != "value" {
|
||||
@@ -57,6 +57,19 @@ func TestResourceDownloadInfo(t *testing.T) {
|
||||
}); err == nil {
|
||||
t.Fatal("plain HTTP URL unexpectedly accepted")
|
||||
}
|
||||
for _, resourceURL := range []string{
|
||||
"https://evil.example/file",
|
||||
"https://aliyuncs.com.evil.example/file",
|
||||
"https://127.0.0.1/file",
|
||||
"https://user:secret@download.dingtalk.com/file",
|
||||
"https://download.dingtalk.com:8443/file",
|
||||
} {
|
||||
if _, _, err := resourceDownloadInfo(
|
||||
map[string]any{"resourceUrl": resourceURL},
|
||||
); err == nil {
|
||||
t.Fatalf("untrusted URL %q unexpectedly accepted", resourceURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestResolveResourceDownloadPath(t *testing.T) {
|
||||
@@ -115,16 +128,23 @@ func TestResolveResourceDownloadPathRejectsSymlinkParentBeforeCreatingOutside(t
|
||||
}
|
||||
}
|
||||
|
||||
func TestDownloadResourceAtomically(t *testing.T) {
|
||||
func TestCrossPlatformCoverageDownloadResourceAtomically(t *testing.T) {
|
||||
const body = "verified download bytes"
|
||||
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
||||
_, _ = w.Write([]byte(body))
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
client := &http.Client{Transport: resourceRoundTripper(func(request *http.Request) (*http.Response, error) {
|
||||
if request.URL.String() != "https://download.dingtalk.com/resource.bin" {
|
||||
t.Fatalf("request URL = %q", request.URL)
|
||||
}
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
Body: io.NopCloser(strings.NewReader(body)),
|
||||
ContentLength: int64(len(body)),
|
||||
Header: make(http.Header),
|
||||
}, nil
|
||||
})}
|
||||
dest := filepath.Join(t.TempDir(), "resource.bin")
|
||||
|
||||
size, err := downloadResourceAtomically(
|
||||
context.Background(), server.Client(), server.URL, nil, dest, false)
|
||||
context.Background(), client, "https://download.dingtalk.com/resource.bin", nil, dest, false)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -139,7 +159,7 @@ func TestDownloadResourceAtomically(t *testing.T) {
|
||||
t.Fatalf("body = %q", got)
|
||||
}
|
||||
if _, err := downloadResourceAtomically(
|
||||
context.Background(), server.Client(), server.URL, nil, dest, false,
|
||||
context.Background(), client, "https://download.dingtalk.com/resource.bin", nil, dest, false,
|
||||
); err == nil || !strings.Contains(err.Error(), "已存在") {
|
||||
t.Fatalf("no-clobber error = %v", err)
|
||||
}
|
||||
|
||||
@@ -191,6 +191,14 @@ func executeMessagesSend(rt *shortcut.RuntimeContext) error {
|
||||
if contentType == "file" || contentType == "audio" || contentType == "video" {
|
||||
return executeMessagesSendUserFile(rt, group, openID, contentType)
|
||||
}
|
||||
if group != "" {
|
||||
body = helpers.NormalizeMessageMentions(
|
||||
body,
|
||||
uniqueShortcutStrings(rt.StrSlice("at-open-dingtalk-ids")),
|
||||
rt.Bool("at-all"),
|
||||
true,
|
||||
)
|
||||
}
|
||||
content, _ := json.Marshal(map[string]string{"title": title, "text": body})
|
||||
params := rt.AddAIMessageTag(map[string]any{
|
||||
"msgType": "markdown",
|
||||
@@ -212,6 +220,15 @@ func executeMessagesSend(rt *shortcut.RuntimeContext) error {
|
||||
}
|
||||
return executeUnifiedMessageWrite(rt, "chat", "send_personal_message", params)
|
||||
case "bot":
|
||||
body = helpers.NormalizeMessageMentions(
|
||||
body,
|
||||
append(
|
||||
uniqueShortcutStrings(rt.StrSlice("at-user-ids")),
|
||||
uniqueShortcutStrings(rt.StrSlice("at-open-dingtalk-ids"))...,
|
||||
),
|
||||
rt.Bool("at-all"),
|
||||
false,
|
||||
)
|
||||
params := map[string]any{
|
||||
"robotCode": rt.Str("robot-code"),
|
||||
"title": title,
|
||||
@@ -241,6 +258,15 @@ func executeMessagesSend(rt *shortcut.RuntimeContext) error {
|
||||
}
|
||||
return executeUnifiedMessageWrite(rt, "bot", "batch_send_robot_msg_to_users", params)
|
||||
case "webhook":
|
||||
body = helpers.NormalizeMessageMentions(
|
||||
body,
|
||||
append(
|
||||
uniqueShortcutStrings(rt.StrSlice("at-user-ids")),
|
||||
uniqueShortcutStrings(rt.StrSlice("at-mobiles"))...,
|
||||
),
|
||||
rt.Bool("at-all"),
|
||||
false,
|
||||
)
|
||||
params := map[string]any{
|
||||
"robotToken": rt.Str("webhook-token"),
|
||||
"title": title,
|
||||
|
||||
@@ -284,6 +284,44 @@ func Resources(m map[string]any) []map[string]any {
|
||||
return out
|
||||
}
|
||||
|
||||
// ResourcesDeep returns resources from a message and each nested quoted,
|
||||
// replied-to or forwarded message. Every nested resource is projected from the
|
||||
// child message that owns it, so its download arguments never reuse the parent
|
||||
// message ID. A missing child conversation ID inherits the enclosing
|
||||
// conversation because quoted and forwarded records often omit that duplicate
|
||||
// field.
|
||||
func ResourcesDeep(m map[string]any) []map[string]any {
|
||||
return resourcesDeep(m, "", 0)
|
||||
}
|
||||
|
||||
const maxResourceMessageDepth = 32
|
||||
|
||||
func resourcesDeep(m map[string]any, inheritedConversationID string, depth int) []map[string]any {
|
||||
if m == nil || depth > maxResourceMessageDepth {
|
||||
return nil
|
||||
}
|
||||
conversationID := strings.TrimSpace(fmt.Sprint(ConversationID(m)))
|
||||
if conversationID == "" || conversationID == "<nil>" {
|
||||
conversationID = inheritedConversationID
|
||||
}
|
||||
owned := m
|
||||
if ConversationID(m) == nil && conversationID != "" {
|
||||
owned = make(map[string]any, len(m)+1)
|
||||
for key, value := range m {
|
||||
owned[key] = value
|
||||
}
|
||||
owned["openConversationId"] = conversationID
|
||||
}
|
||||
out := append([]map[string]any(nil), Resources(owned)...)
|
||||
if depth == maxResourceMessageDepth {
|
||||
return out
|
||||
}
|
||||
for _, child := range nestedMessageChildren(m) {
|
||||
out = append(out, resourcesDeep(child, conversationID, depth+1)...)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
var mediaIDTextRE = regexp.MustCompile(`(?i)media[_-]?id\s*[:=]\s*["']?([^"'\s)\]}>,]+)`)
|
||||
var fileIDTextRE = regexp.MustCompile(`(?i)\bfile[_-]?id\s*[:=]\s*["']?([^"'\s)\]}>,]+)`)
|
||||
|
||||
@@ -292,13 +330,16 @@ func collectResourceIDs(value any, targetKey string, textPattern *regexp.Regexp,
|
||||
case map[string]any:
|
||||
resourceType := strings.TrimSpace(fmt.Sprint(firstMessageValue(typed, "resourceType", "resource_type")))
|
||||
for key, child := range typed {
|
||||
normalizedKey := strings.ToLower(strings.NewReplacer("_", "", "-", "").Replace(key))
|
||||
normalizedKey := normalizeMessageKey(key)
|
||||
if normalizedKey == targetKey ||
|
||||
(normalizedKey == "resourceid" && strings.EqualFold(resourceType, targetKey)) {
|
||||
if id := resourceIDScalar(child); id != "" {
|
||||
*out = append(*out, id)
|
||||
}
|
||||
}
|
||||
if isNestedMessageBoundaryKey(normalizedKey) {
|
||||
continue
|
||||
}
|
||||
collectResourceIDs(child, targetKey, textPattern, out)
|
||||
}
|
||||
case []any:
|
||||
@@ -327,6 +368,74 @@ func collectResourceIDs(value any, targetKey string, textPattern *regexp.Regexp,
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeMessageKey(key string) string {
|
||||
return strings.ToLower(strings.NewReplacer("_", "", "-", "").Replace(key))
|
||||
}
|
||||
|
||||
func isNestedMessageBoundaryKey(key string) bool {
|
||||
switch key {
|
||||
case "quotedmessage", "replymessage", "quoted", "replytomessage",
|
||||
"forwardmessages", "forwardedmessages", "forwarded":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func nestedMessageMaps(value any) []map[string]any {
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
return []map[string]any{typed}
|
||||
case []map[string]any:
|
||||
return typed
|
||||
case []any:
|
||||
out := make([]map[string]any, 0, len(typed))
|
||||
for _, item := range typed {
|
||||
if child, ok := item.(map[string]any); ok {
|
||||
out = append(out, child)
|
||||
}
|
||||
}
|
||||
return out
|
||||
case string:
|
||||
var decoded any
|
||||
if json.Unmarshal([]byte(strings.TrimSpace(typed)), &decoded) == nil {
|
||||
return nestedMessageMaps(decoded)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nestedMessageChildren(value any) []map[string]any {
|
||||
out := make([]map[string]any, 0)
|
||||
switch typed := value.(type) {
|
||||
case map[string]any:
|
||||
for key, child := range typed {
|
||||
if isNestedMessageBoundaryKey(normalizeMessageKey(key)) {
|
||||
out = append(out, nestedMessageMaps(child)...)
|
||||
continue
|
||||
}
|
||||
out = append(out, nestedMessageChildren(child)...)
|
||||
}
|
||||
case []any:
|
||||
for _, child := range typed {
|
||||
out = append(out, nestedMessageChildren(child)...)
|
||||
}
|
||||
case []map[string]any:
|
||||
for _, child := range typed {
|
||||
out = append(out, nestedMessageChildren(child)...)
|
||||
}
|
||||
case string:
|
||||
trimmed := strings.TrimSpace(typed)
|
||||
if strings.HasPrefix(trimmed, "{") || strings.HasPrefix(trimmed, "[") {
|
||||
var decoded any
|
||||
if json.Unmarshal([]byte(trimmed), &decoded) == nil {
|
||||
out = append(out, nestedMessageChildren(decoded)...)
|
||||
}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func resourceIDScalar(value any) string {
|
||||
text, ok := value.(string)
|
||||
if !ok {
|
||||
|
||||
@@ -189,6 +189,111 @@ func TestQuotedMessageIsBoundedAndSemantic(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageResourcesRespectNestedMessageOwnership(t *testing.T) {
|
||||
message := map[string]any{
|
||||
"openMessageId": "parent",
|
||||
"openConversationId": "cid-parent",
|
||||
"content": map[string]any{"mediaId": "media-parent"},
|
||||
"quotedMessage": map[string]any{
|
||||
"openMessageId": "quoted",
|
||||
"content": map[string]any{"mediaId": "media-quoted"},
|
||||
},
|
||||
"forwardMessages": []any{
|
||||
map[string]any{
|
||||
"openMessageId": "forwarded",
|
||||
"openConversationId": "cid-forwarded",
|
||||
"content": map[string]any{"mediaId": "media-forwarded"},
|
||||
},
|
||||
},
|
||||
}
|
||||
owned := Resources(message)
|
||||
if len(owned) != 1 || owned[0]["resourceId"] != "media-parent" {
|
||||
t.Fatalf("parent resources crossed message boundary: %#v", owned)
|
||||
}
|
||||
|
||||
deep := ResourcesDeep(message)
|
||||
if len(deep) != 3 {
|
||||
t.Fatalf("deep resources = %#v", deep)
|
||||
}
|
||||
argumentsByResource := map[string]map[string]any{}
|
||||
for _, resource := range deep {
|
||||
download := resource["download"].(map[string]any)
|
||||
argumentsByResource[resource["resourceId"].(string)] = download["arguments"].(map[string]any)
|
||||
}
|
||||
for resourceID, want := range map[string][2]string{
|
||||
"media-parent": {"parent", "cid-parent"},
|
||||
"media-quoted": {"quoted", "cid-parent"},
|
||||
"media-forwarded": {"forwarded", "cid-forwarded"},
|
||||
} {
|
||||
arguments := argumentsByResource[resourceID]
|
||||
if arguments["message-id"] != want[0] ||
|
||||
arguments["open-conversation-id"] != want[1] {
|
||||
t.Errorf("%s arguments = %#v, want message=%q conversation=%q",
|
||||
resourceID, arguments, want[0], want[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestCrossPlatformCoverageResourceBoundaryHelpers(t *testing.T) {
|
||||
encoded := map[string]any{
|
||||
"openMessageId": "parent",
|
||||
"openConversationId": "cid",
|
||||
"content": `{"quotedMessage":{"openMessageId":"encoded-child","mediaId":"nested"}}`,
|
||||
}
|
||||
if got := Resources(encoded); got != nil {
|
||||
t.Fatalf("encoded nested resource bound to parent: %#v", got)
|
||||
}
|
||||
if got := ResourcesDeep(encoded); len(got) != 1 {
|
||||
t.Fatalf("encoded nested resources = %#v", got)
|
||||
} else {
|
||||
arguments := got[0]["download"].(map[string]any)["arguments"].(map[string]any)
|
||||
if arguments["message-id"] != "encoded-child" ||
|
||||
arguments["open-conversation-id"] != "cid" {
|
||||
t.Fatalf("encoded child arguments = %#v", arguments)
|
||||
}
|
||||
}
|
||||
if got := resourcesDeep(map[string]any{"mediaId": "x"}, "", maxResourceMessageDepth+1); got != nil {
|
||||
t.Fatalf("over-depth resources = %#v", got)
|
||||
}
|
||||
if got := resourcesDeep(
|
||||
map[string]any{"mediaId": "x"},
|
||||
"",
|
||||
maxResourceMessageDepth,
|
||||
); len(got) != 1 {
|
||||
t.Fatalf("max-depth owned resources = %#v", got)
|
||||
}
|
||||
if got := nestedMessageMaps([]map[string]any{{"id": "a"}}); len(got) != 1 {
|
||||
t.Fatalf("map slice = %#v", got)
|
||||
}
|
||||
if got := nestedMessageMaps(`[{"id":"a"}]`); len(got) != 1 {
|
||||
t.Fatalf("encoded message list = %#v", got)
|
||||
}
|
||||
if got := nestedMessageMaps([]any{"bad", map[string]any{"id": "a"}}); len(got) != 1 {
|
||||
t.Fatalf("mixed message list = %#v", got)
|
||||
}
|
||||
if got := nestedMessageMaps("{"); got != nil {
|
||||
t.Fatalf("invalid encoded messages = %#v", got)
|
||||
}
|
||||
if got := nestedMessageChildren([]map[string]any{
|
||||
{"content": "plain"},
|
||||
{"content": `{"forwardedMessages":[{"openMessageId":"m"}]}`},
|
||||
}); len(got) != 1 {
|
||||
t.Fatalf("nested message children = %#v", got)
|
||||
}
|
||||
if got := nestedMessageChildren([]any{
|
||||
"plain",
|
||||
map[string]any{"quoted": map[string]any{"openMessageId": "m"}},
|
||||
}); len(got) != 1 {
|
||||
t.Fatalf("mixed nested message children = %#v", got)
|
||||
}
|
||||
if got := nestedMessageChildren("{"); len(got) != 0 {
|
||||
t.Fatalf("invalid nested children = %#v", got)
|
||||
}
|
||||
if isNestedMessageBoundaryKey("content") {
|
||||
t.Fatal("ordinary content treated as a message boundary")
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdateTimeOmitsUneditedEcho(t *testing.T) {
|
||||
if got := UpdateTime(map[string]any{
|
||||
"createTime": "2026-07-19 13:37:03",
|
||||
|
||||
@@ -487,8 +487,8 @@
|
||||
},
|
||||
"+messages-mget": {
|
||||
"disposition": "semantic_adapter",
|
||||
"semantic_delta": "按最多 50 个消息 ID 批量读取并输出稳定消息投影、reaction 与 resourceRefs;可用 --download-resources 统一下载 mediaId 与 fileId,复用 HTTPS、相对路径、无覆盖和原子落盘防护,对重复资源去重并逐资源返回下载失败 ledger。",
|
||||
"risk": "read",
|
||||
"semantic_delta": "按最多 50 个消息 ID 批量读取并输出稳定消息投影、reaction 与 resourceRefs;可用 --download-resources 统一下载 mediaId 与 fileId,复用受信任下载域、相对路径、无覆盖和原子落盘防护,对重复资源去重并逐资源返回下载失败 ledger;因可写本地文件按 write 风险确认。",
|
||||
"risk": "write",
|
||||
"public": true,
|
||||
"reviewed": true
|
||||
},
|
||||
@@ -564,8 +564,8 @@
|
||||
},
|
||||
"+messages-resource-download": {
|
||||
"disposition": "primary_smart",
|
||||
"semantic_delta": "统一承接消息 mediaId 与钉盘 fileId:分别复用 IM 临时资源 URL 和 drive.download_file,再通过工作目录内安全路径、默认不覆盖、临时文件下载和原子发布输出结构化结果。",
|
||||
"risk": "read",
|
||||
"semantic_delta": "统一承接消息 mediaId 与钉盘 fileId:分别复用 IM 临时资源 URL 和 drive.download_file,只允许钉钉/OSS HTTPS 下载域且重定向复验并隔离跨域凭据,再通过工作目录内安全路径、默认不覆盖、临时文件下载和原子发布输出结构化结果。",
|
||||
"risk": "write",
|
||||
"public": true,
|
||||
"reviewed": true
|
||||
},
|
||||
@@ -628,7 +628,7 @@
|
||||
"+at-me": {
|
||||
"disposition": "primary_smart",
|
||||
"semantic_delta": "自动构造时间窗,分页拉取跨会话 @我 消息,并保留身份、引用、reaction、resourceRefs 与完整性;可选对资源去重后安全落盘并返回逐项失败 ledger。",
|
||||
"risk": "read",
|
||||
"risk": "write",
|
||||
"public": true,
|
||||
"reviewed": true
|
||||
},
|
||||
@@ -642,7 +642,7 @@
|
||||
"+chat-messages": {
|
||||
"disposition": "primary_smart",
|
||||
"semantic_delta": "统一群聊与两类单聊目标;省略时间时自动以当前时间向前读取最近消息,并输出稳定消息身份、引用、reaction、resourceRefs、时间边界翻页和可读正文;可选对列表内资源去重后安全落盘并返回逐项失败 ledger。",
|
||||
"risk": "read",
|
||||
"risk": "write",
|
||||
"public": true,
|
||||
"reviewed": true
|
||||
},
|
||||
@@ -678,7 +678,7 @@
|
||||
"+search-msg": {
|
||||
"disposition": "primary_smart",
|
||||
"semantic_delta": "统一关键词、发送者、@对象、会话、消息类型和机器人来源过滤;展开下层按会话分组的 conversationMessagesList,支持精确时间窗、page-all、50 条一组 mget 富化,可选安全下载命中消息资源,并以 failure ledger 显式报告截断、富化或下载失败。",
|
||||
"risk": "read",
|
||||
"risk": "write",
|
||||
"public": true,
|
||||
"reviewed": true
|
||||
},
|
||||
@@ -692,7 +692,7 @@
|
||||
"+thread-replies": {
|
||||
"disposition": "primary_smart",
|
||||
"semantic_delta": "接受消息列表直接返回的 threadId(兼容 topicId),拉取回复并输出稳定身份、引用、reaction、resourceRefs、可读正文和时间边界分页;可选对回复资源去重后安全落盘并返回逐项失败 ledger。",
|
||||
"risk": "read",
|
||||
"risk": "write",
|
||||
"public": true,
|
||||
"reviewed": true
|
||||
},
|
||||
|
||||
@@ -38,9 +38,9 @@ import (
|
||||
// so it honours --format/--jq/--fields. When the response carries no
|
||||
// recognisable message list we fall back to printing the raw payload.
|
||||
//
|
||||
// This replaces manually working out the millisecond time window and copying the
|
||||
// list-mentions incantation. Read-only: it only searches and reshapes, never
|
||||
// sends, recalls or marks anything.
|
||||
// This replaces manually working out the millisecond time window and copying
|
||||
// the list-mentions incantation. The default path only searches and reshapes;
|
||||
// --download-resources additionally writes resource files locally.
|
||||
//
|
||||
// dws chat +at-me
|
||||
// dws chat +at-me --days 3
|
||||
@@ -52,8 +52,8 @@ var AtMe = shortcut.Shortcut{
|
||||
Intent: "当你想快速看回最近谁在群里或单聊里 @了你、但不想手动把起止时间换算成毫秒、也不想记 list-mentions 的一堆参数时使用;" +
|
||||
"内部按本地时区算出「最近 N 天」(默认 7 天,可用 --days 调整回溯天数)的时间窗,搜索这段时间内 @我 的消息," +
|
||||
"再在本地把每条消息投影成发送人、时间、内容、所在会话四个关键字段。" +
|
||||
"这是纯只读操作,只做搜索与本地投影,不会发送、撤回或标记任何消息。",
|
||||
Risk: shortcut.RiskRead,
|
||||
"默认只读且不会发送、撤回或标记任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。",
|
||||
Risk: shortcut.RiskWrite,
|
||||
Flags: append([]shortcut.Flag{
|
||||
{Name: "days", Type: shortcut.FlagInt, Desc: "回溯天数(可选,默认 7)", Default: "7", Required: false},
|
||||
{Name: "limit", Type: shortcut.FlagInt, Desc: "每页返回数量(默认 50)", Default: "50"},
|
||||
|
||||
@@ -37,8 +37,8 @@ import (
|
||||
// aliases and one level of nesting;
|
||||
// 3. print via rt.Output as {messages, count} so it honours --format/--jq/--fields.
|
||||
//
|
||||
// Read-only: it only reads a conversation's messages and reshapes them locally,
|
||||
// never posts or mutates anything.
|
||||
// The default path only reads and reshapes conversation messages;
|
||||
// --download-resources additionally writes resource files locally.
|
||||
//
|
||||
// dws chat +chat-messages --group <openconversation_id> --time "2025-03-01 00:00:00"
|
||||
// dws chat +chat-messages --user <userId> --time "2025-03-01 00:00:00" --limit 50
|
||||
@@ -51,8 +51,8 @@ var ChatMessages = shortcut.Shortcut{
|
||||
"群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。" +
|
||||
"省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。" +
|
||||
"内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。" +
|
||||
"这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。",
|
||||
Risk: shortcut.RiskRead,
|
||||
"默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。",
|
||||
Risk: shortcut.RiskWrite,
|
||||
Flags: append([]shortcut.Flag{
|
||||
{Name: "group", Type: shortcut.FlagString, Desc: "群会话 ID(openConversationId),与 --user 互斥"},
|
||||
{Name: "conversation-id", Type: shortcut.FlagString, Desc: "--group 的别名", Hidden: true},
|
||||
|
||||
@@ -89,7 +89,7 @@ func TestCrossPlatformCoverageChatMessagesOpenIDRoute(t *testing.T) {
|
||||
}}
|
||||
helpers.InitDeps(caller)
|
||||
root := newPlatformCoverageRoot()
|
||||
root.SetArgs([]string{"chat", "+chat-messages", "--open-dingtalk-id", "D-user", "--limit", "1"})
|
||||
root.SetArgs([]string{"chat", "+chat-messages", "--open-dingtalk-id", "D-user", "--limit", "1", "--yes"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -202,7 +202,7 @@ func TestCrossPlatformCoverageSearchValidationAndTimeErrors(t *testing.T) {
|
||||
for _, tail := range cases {
|
||||
helpers.InitDeps(&smartCoverageCaller{})
|
||||
root := newPlatformCoverageRoot()
|
||||
root.SetArgs(append([]string{"chat", "+search-msg"}, tail...))
|
||||
root.SetArgs(append([]string{"chat", "+search-msg", "--yes"}, tail...))
|
||||
if err := root.Execute(); err == nil {
|
||||
t.Errorf("invalid search args succeeded: %v", tail)
|
||||
}
|
||||
@@ -247,7 +247,7 @@ func TestCrossPlatformCoverageSearchPaginationFailureModes(t *testing.T) {
|
||||
root := newPlatformCoverageRoot()
|
||||
var output bytes.Buffer
|
||||
root.SetOut(&output)
|
||||
root.SetArgs(append([]string{"chat", "+search-msg"}, tc.args...))
|
||||
root.SetArgs(append([]string{"chat", "+search-msg", "--yes"}, tc.args...))
|
||||
err := root.Execute()
|
||||
if (err != nil) != tc.wantError {
|
||||
t.Fatalf("error = %v, wantError=%v", err, tc.wantError)
|
||||
|
||||
@@ -92,7 +92,7 @@ func TestCrossPlatformCoverageMessageReadShortcutResourceOutputValidation(t *tes
|
||||
} {
|
||||
helpers.InitDeps(&smartCoverageCaller{})
|
||||
root := newPlatformCoverageRoot()
|
||||
root.SetArgs(append(args, "--download-resources", "--output-dir", "../outside"))
|
||||
root.SetArgs(append(args, "--download-resources", "--output-dir", "../outside", "--yes"))
|
||||
if err := root.Execute(); err == nil {
|
||||
t.Fatalf("unsafe output accepted: %v", args)
|
||||
}
|
||||
@@ -104,7 +104,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"})
|
||||
root.SetArgs([]string{"chat", "+chat-messages", "--group", "cid", "--limit", "5", "--yes"})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -133,6 +133,7 @@ func TestCrossPlatformCoverageChatMessagesPreservesExplicitTime(t *testing.T) {
|
||||
"chat", "+chat-messages",
|
||||
"--group", "cid",
|
||||
"--time", "2026-07-01 12:34:56",
|
||||
"--yes",
|
||||
})
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
|
||||
@@ -36,8 +36,9 @@ var SearchMsg = shortcut.Shortcut{
|
||||
Product: "im",
|
||||
Description: "多维搜索消息,可全量翻页并批量富化详情",
|
||||
Intent: "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天," +
|
||||
"也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。",
|
||||
Risk: shortcut.RiskRead,
|
||||
"也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。" +
|
||||
"默认只读;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。",
|
||||
Risk: shortcut.RiskWrite,
|
||||
Flags: append([]shortcut.Flag{
|
||||
{Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词"},
|
||||
{Name: "keyword", Type: shortcut.FlagString, Desc: "--query 的别名", Hidden: true},
|
||||
|
||||
@@ -73,7 +73,7 @@ func executeSearchMsg(t *testing.T, caller *searchMsgExecutionCaller, args ...st
|
||||
root := newPlatformCoverageRoot()
|
||||
var output bytes.Buffer
|
||||
root.SetOut(&output)
|
||||
root.SetArgs(append([]string{"chat", "+search-msg"}, args...))
|
||||
root.SetArgs(append([]string{"chat", "+search-msg", "--yes"}, args...))
|
||||
if err := root.Execute(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -30,8 +30,8 @@ import (
|
||||
// project each reply to {sender, text, createTime} tolerating field aliases;
|
||||
// 3. print via rt.Output as {replies, count} so it honours --format/--jq/--fields.
|
||||
//
|
||||
// Read-only: it only reads a topic's replies and reshapes them locally, never
|
||||
// posts or mutates anything.
|
||||
// The default path only reads and reshapes topic replies;
|
||||
// --download-resources additionally writes resource files locally.
|
||||
//
|
||||
// dws chat +thread-replies --group <openconversationId> --thread-id <threadId>
|
||||
var ThreadReplies = shortcut.Shortcut{
|
||||
@@ -42,8 +42,8 @@ var ThreadReplies = shortcut.Shortcut{
|
||||
Intent: "当你已经拿到某个群里一条「话题消息」的 threadId/topicId、想快速看这条话题下的全部回复(谁在什么时间回复了什么)," +
|
||||
"而不想拿到一大坨原始消息字段时使用;内部按 --group(群会话 ID)和 --thread-id(兼容 --topic-id)拉取该话题的回复列表," +
|
||||
"可选 --time 指定起始时间、--limit 指定每页条数,再在本地投影出每条回复的发言人、文本和回复时间。" +
|
||||
"这是纯只读操作,只做拉取与本地投影,不会发送或修改任何消息。",
|
||||
Risk: shortcut.RiskRead,
|
||||
"默认只读且不会发送或修改任何消息;传 --download-resources 时会在工作目录写文件,因此命令按本地写入操作确认。",
|
||||
Risk: shortcut.RiskWrite,
|
||||
Flags: append([]shortcut.Flag{
|
||||
{Name: "group", Type: shortcut.FlagString, Desc: "群会话 ID(openConversationId,必填)", Required: true},
|
||||
{Name: "thread-id", Type: shortcut.FlagString, Desc: "话题/线程 ID(可直接使用消息列表返回的 threadId)"},
|
||||
|
||||
Reference in new issue
Block a user