diff --git a/bot/commands.py b/bot/commands.py
index 378e7e6..e38357e 100644
--- a/bot/commands.py
+++ b/bot/commands.py
@@ -85,7 +85,7 @@ async def handle_command(user_id: str, text: str) -> Optional[str]:
if cmd in (P+"new", P+"n"):
return await _cmd_new(user_id, args)
- elif cmd in (P+"status", P+"list", P+"ls", P+"l"):
+ elif cmd in (P+"list", P+"ls", P+"l", P+"status"):
return await _cmd_status(user_id)
elif cmd in (P+"close", P+"c"):
return await _cmd_close(user_id, args)
@@ -443,7 +443,7 @@ async def _cmd_nodes(user_id: str, args: str) -> str:
"""List nodes or switch active node."""
from config import ROUTER_MODE
if not ROUTER_MODE:
- return "Not in router mode. Run standalone.py for multi-host support."
+ return "Not in router mode."
from router.nodes import get_node_registry
registry = get_node_registry()
@@ -476,29 +476,25 @@ def _cmd_help() -> str:
"""Show help."""
from config import COMMAND_PREFIX as P
return f"""**Commands:** (prefix: `{P}`)
-{P}new
[msg] [--idle N] [--perm MODE] - Create session
-{P}status - Show sessions and current mode
-{P}close [n] - Close session (active or by number)
-{P}switch - Switch to session by number
-{P}perm [conv_id] - Set permission mode (default/edit/plan/bypass/auto)
-{P}stop - Interrupt the current task
-{P}progress - Show task progress
-{P}direct - Direct mode: messages → Claude Code (no LLM overhead)
+{P}new [msg] [--idle N] [--perm MODE] - Create session (alias: {P}n)
+{P}list - Show sessions and current mode (alias: {P}ls, {P}l, {P}status)
+{P}close [n] - Close session (active or by number) (alias: {P}c)
+{P}switch - Switch to session by number (alias: {P}s)
+{P}perm [conv_id] - Set permission mode (alias: {P}perm)
+{P}stop - Interrupt the current task (alias: {P}interrupt)
+{P}progress - Show task progress (alias: {P}p)
+{P}direct - Direct mode: messages → Claude Code
{P}smart - Smart mode: messages → LLM routing (default)
-{P}shell - Run shell command (bypasses LLM)
+{P}shell - Run shell command
{P}remind - Set reminder (e.g. {P}remind 10m check build)
{P}tasks - List background tasks
{P}nodes - List connected host nodes
{P}node - Switch active node
-{P}retry - Retry last message
-{P}help - Show this help
+{P}help - Show this help (alias: {P}h, {P}?)
**Permission modes** (used by {P}perm and {P}new --perm):
- default — 默认模式,遇到文件编辑操作时手动确认
- edit — 自动接受文件编辑,但 shell 命令仍需手动确认
- 适合:日常开发,需要对命令执行保持控制
+ default — 默认模式,需审批工具调用
+ edit — 自动接受文件编辑,shell 命令仍需确认
plan — 只规划、不执行任何写操作
- 适合:先预览 CC 的操作计划再决定是否执行
- bypass — 跳过所有权限确认,CC 自动执行一切操作
- 适合:受信任的沙盒环境、自动化任务
- auto — 允许所有工具,不询问(等效 bypass + dontAsk)"""
\ No newline at end of file
+ bypass — 跳过所有权限确认
+ auto — 允许所有工具,不询问"""
\ No newline at end of file
diff --git a/bot/feishu.py b/bot/feishu.py
index ca3e5e5..6c98057 100644
--- a/bot/feishu.py
+++ b/bot/feishu.py
@@ -149,7 +149,11 @@ def build_sessions_card(sessions: list[dict], active_conv_id: str | None, mode:
def build_approval_card(conv_id: str, tool_name: str, summary: str, timeout: int = 120) -> dict:
- """Build an approval card for a tool call (schema 2.0, with approve/deny buttons)."""
+ """Build an approval card for a tool call (schema 2.0, with approve/deny buttons).
+
+ Note: JSON 2.0 does NOT support "action" wrapper or "note" components.
+ Buttons go directly in elements; use div with small text for the note.
+ """
return {
"schema": "2.0",
"header": {
@@ -163,27 +167,23 @@ def build_approval_card(conv_id: str, tool_name: str, summary: str, timeout: int
"content": f"**工具:** `{tool_name}`\n**参数:** {summary}",
},
{
- "tag": "action",
- "actions": [
- {
- "tag": "button",
- "text": {"tag": "plain_text", "content": "✅ 批准"},
- "type": "primary",
- "value": {"action": "approve", "conv_id": conv_id},
- },
- {
- "tag": "button",
- "text": {"tag": "plain_text", "content": "❌ 拒绝"},
- "type": "danger",
- "value": {"action": "deny", "conv_id": conv_id},
- },
- ],
+ "tag": "button",
+ "text": {"tag": "plain_text", "content": "✅ 批准"},
+ "type": "primary",
+ "value": {"action": "approve", "conv_id": conv_id},
},
{
- "tag": "note",
- "elements": [
- {"tag": "plain_text", "content": f"超时 {timeout}s 自动拒绝 | 也可回复 y/n"},
- ],
+ "tag": "button",
+ "text": {"tag": "plain_text", "content": "❌ 拒绝"},
+ "type": "danger",
+ "value": {"action": "deny", "conv_id": conv_id},
+ },
+ {
+ "tag": "div",
+ "text": {
+ "tag": "plain_text",
+ "content": f"超时 {timeout}s 自动拒绝 | 也可回复 y/n",
+ },
},
],
},
diff --git a/docs/feishu/card_json_schema_2.md b/docs/feishu/card_json_schema_2.md
new file mode 100644
index 0000000..ab20963
--- /dev/null
+++ b/docs/feishu/card_json_schema_2.md
@@ -0,0 +1,281 @@
+# 卡片 JSON 2.0 结构
+
+本文档介绍卡片 JSON 2.0 的整体结构和属性说明。
+
+## 概念说明
+
+- 卡片 JSON 2.0 是指在卡片 JSON 数据中,声明了 `schema` 属性为 `"2.0"` 的版本。与 1.0 版本相比,2.0 版本有较多不兼容差异和新增属性,详情参考[卡片 JSON 2.0 版本更新说明](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-breaking-changes-release-notes)。
+
+- 在可视化搭建工具中,你可通过搭建[新版卡片](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/feishu-card-cardkit/cardkit-upgraded-version-card-release-notes),获取 2.0 版本的卡片 JSON 源代码。
+## 注意事项
+
+- 卡片 JSON 2.0 结构支持飞书客户端 7.20 及之后版本。当使用 JSON 2.0 结构的卡片发送至低于 7.20 版本的客户端时,卡片标题可正常显示,但内容将展示兜底的升级提示文案。
+
+
+
+- 卡片 JSON 2.0 结构暂时仅支持共享卡片,不支持独享卡片配置。即 `update_multi` 参数仅支持设为 `true`。
+
+- 卡片 JSON 2.0 结构中,一张卡片最多支持 200 个元素(如 `tag` 为 `plain_text` 的文本元素)或组件。
+
+## JSON 结构
+
+以下为卡片 JSON 2.0 的整体结构。
+```JSON
+{
+ "schema": "2.0", // 卡片 JSON 结构的版本。默认为 1.0。要使用 JSON 2.0 结构,必须显示声明 2.0。
+ "config": {
+ "streaming_mode": true, // 卡片是否处于流式更新模式,默认值为 false。
+ "streaming_config": {}, // 流式更新配置。详情参考下文。
+ "summary": { // 卡片摘要信息。可通过该参数自定义客户端聊天栏消息预览中的展示文案。
+ "content": "自定义内容", // 自定义摘要信息。如果开启了流式更新模式,该参数将默认为“生成中”。
+ "i18n_content": { // 摘要信息的多语言配置。了解支持的所有语种。参考配置卡片多语言文档。
+ "zh_cn": "",
+ "en_us": "",
+ "ja_jp": ""
+ }
+ },
+ "locales": [ // JSON 2.0 新增属性。用于指定生效的语言。如果配置 locales,则只有 locales 中的语言会生效。
+ "en_us",
+ "ja_jp"
+ ],
+ "enable_forward": true, // 是否支持转发卡片。默认值为 true。
+ "update_multi": true, // 是否为共享卡片。默认值为 true,JSON 2.0 暂时仅支持设为 true,即更新卡片的内容对所有收到这张卡片的人员可见。
+ "width_mode": "fill", // 卡片宽度模式。支持 "compact"(紧凑宽度 400px)模式 或 "fill"(撑满聊天窗口宽度)模式。默认不填时的宽度为 600px。
+ "use_custom_translation": false, // 是否使用自定义翻译数据。默认值 false。为 true 时,在用户点击消息翻译后,使用 i18n 对应的目标语种作为翻译结果。若 i18n 取不到,则使用当前内容请求翻译,不使用自定义翻译数据。
+ "enable_forward_interaction": false, // 转发的卡片是否仍然支持回传交互。默认值 false。
+ "style": { // 添加自定义字号和颜色。可应用在组件 JSON 数据中,设置字号和颜色属性。
+ "text_size": { // 分别为移动端和桌面端添加自定义字号,同时添加兜底字号。用于在组件 JSON 中设置字号属性。支持添加多个自定义字号对象。
+ "cus-0": {
+ "default": "medium", // 在无法差异化配置字号的旧版飞书客户端上,生效的字号属性。选填。
+ "pc": "medium", // 桌面端的字号。
+ "mobile": "large" // 移动端的字号。
+ }
+ },
+ "color": { // 分别为飞书客户端浅色主题和深色主题添加 RGBA 语法。用于在组件 JSON 中设置颜色属性。支持添加多个自定义颜色对象。
+ "cus-0": {
+ "light_mode": "rgba(5,157,178,0.52)", // 浅色主题下的自定义颜色语法
+ "dark_mode": "rgba(78,23,108,0.49)" // 深色主题下的自定义颜色语法
+ }
+ }
+ }
+ },
+ "card_link": {
+ // 指定卡片整体的跳转链接。
+ "url": "https://www.baidu.com", // 默认链接地址。未配置指定端地址时,该配置生效。
+ "android_url": "https://developer.android.com/",
+ "ios_url": "https://developer.apple.com/",
+ "pc_url": "https://www.windows.com"
+ },
+ "header": {
+ "title": {
+ // 卡片主标题。必填。要为标题配置多语言,参考配置卡片多语言文档。
+ "tag": "plain_text", // 文本类型的标签。可选值:plain_text 和 lark_md。
+ "content": "示例标题" // 标题内容。
+ },
+ "subtitle": {
+ // 卡片副标题。可选。
+ "tag": "plain_text", // 文本类型的标签。可选值:plain_text 和 lark_md。
+ "content": "示例文本" // 标题内容。
+ },
+ "text_tag_list": [
+ // 标题后缀标签,最多设置 3 个 标签,超出不展示。可选。
+ {
+ "tag": "text_tag",
+ "element_id": "custom_id", // 操作元素的唯一标识。用于在调用组件相关接口中指定元素。需开发者自定义。
+ "text": {
+ // 标签内容
+ "tag": "plain_text",
+ "content": "标签 1"
+ },
+ "color": "neutral" // 标签颜色
+ }
+ ],
+ "i18n_text_tag_list": {
+ // 多语言标题后缀标签。每个语言环境最多设置 3 个 tag,超出不展示。可选。同时配置原字段和国际化字段,优先生效多语言配置。
+ "zh_cn": [],
+ "en_us": [],
+ "ja_jp": [],
+ "zh_hk": [],
+ "zh_tw": []
+ },
+ "template": "blue", // 标题主题样式颜色。支持 "blue"|"wathet"|"turquoise"|"green"|"yellow"|"orange"|"red"|"carmine"|"violet"|"purple"|"indigo"|"grey"|"default"。默认值 default。
+ "icon": { // 前缀图标。
+ "tag": "standard_icon", // 图标类型。
+ "token": "chat-forbidden_outlined", // 图标的 token。仅在 tag 为 standard_icon 时生效。
+ "color": "orange", // 图标颜色。仅在 tag 为 standard_icon 时生效。
+ "img_key": "img_v2_38811724" // 图片的 key。仅在 tag 为 custom_icon 时生效。
+ },
+ "padding": "12px 8px 12px 8px" // 标题组件的内边距。JSON 2.0 新增属性。默认值 "12px",支持范围 [0,99]px。
+ },
+ "body": { // 卡片正文。
+ // JSON 2.0 新增布局类属性,用于控制子元素排列:
+ "direction": "vertical", // 正文或容器内组件的排列方向。可选值:"vertical"(垂直排列)、"horizontal"(水平排列)。默认为 "vertical"。
+ "padding": "12px 8px 12px 8px", // 正文或容器内组件的内边距,支持范围 [0,99]px。
+ "horizontal_spacing": "3px", // 正文或容器内组件的水平间距,可选值:"small"(4px)、"medium"(8px)、"large"(12px)、"extra_large"(16px)或[0,99]px。
+ "horizontal_align": "left", // 正文或容器内组件的水平对齐方式,可选值:"left"、"center"、"right"。默认值为 "left"。
+ "vertical_spacing": "4px", // 正文或容器内组件的垂直间距,可选值:"small"(4px)、"medium"(8px)、"large"(12px)、"extra_large"(16px)或[0,99]px。
+ "vertical_align": "center", // 正文或容器内组件的垂直对齐方式,可选值:"top"、"center"、"bottom",默认值为 "top"。
+ "elements": [ // 在此传入各个组件的 JSON 数据,组件将按数组顺序纵向流式排列。
+ {
+ "tag": "xxx", // 组件的标签。
+ "margin": "4px", // 组件的外边距,默认值 "0",支持范围 [-99,99]px。JSON 2.0 新增属性。
+ "element_id": "custom_id" // 操作组件的唯一标识。JSON 2.0 新增属性。用于在调用流式更新相关接口中指定组件。在同一张卡片内,该字段的值全局唯一。仅允许使用字母、数字和下划线,必须以字母开头,不得超过 20 字符。
+ }
+ ]
+ }
+}
+```
+
+## 属性说明
+
+本小节介绍卡片结构中的属性。
+
+### 全局属性
+
+卡片全局属性包括以下字段。
+```JSON
+{
+ "schema": "2.0",
+ "config": {},
+ "card_link": {},
+ "header": {},
+ "body": {
+ "elements": []
+ }
+}
+```
+各个字段说明如下所示。
+若这些字段均不传,则卡片 JSON 为 "{}"。飞书开放平台支持发送卡片 JSON 为 "{}" 的空白卡片。
+
+字段 | 是否必填 | 描述
+---|---|---
+schema | 否 | 卡片结构的版本声明。默认为 1.0 版本。要使用 JSON 2.0 结构,必须显示声明 2.0。可选值: - 1.0:卡片 JSON 1.0 结构。详情参考[卡片 JSON 1.0 结构](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-structure)。 - 2.0:卡片 JSON 2.0 结构。支持更多字段和能力,如卡片流式更新能力、富文本组件(markdown)更多语法等。详情参考[卡片 JSON 2.0 不兼容变更&更新说明](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-breaking-changes-release-notes)。
+config | 否 | 配置卡片的全局行为,包括流式更新模式(JSON 2.0 新增能力)、是否允许被转发、是否为共享卡片等。
+card_link | 否 | 指定卡片整体的点击跳转链接。你可以配置一个默认链接,也可以分别为 PC 端、Android 端、iOS 端配置不同的跳转链接。
+header | 否 | 标题组件相关配置。详情参考[标题](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-components/content-components/title)组件。
+body | 否 | 卡片正文,包含一个名为 elements 的数组,用于放置各类组件。
+
+### 卡片全局行为设置 `config`
+
+`config` 用于配置卡片的全局行为,包括流式更新模式、是否允许被转发、是否为共享卡片等。
+```json
+{
+ "config": {
+ "streaming_mode": true, // 卡片是否处于流式更新模式,默认值为 false。
+ "streaming_config": { // 流式更新配置。
+ "print_frequency_ms": { // // 流式更新频率,单位:ms
+ "default": 30,
+ "android": 25,
+ "ios": 40,
+ "pc": 50
+ },
+ "print_step": { // // 流式更新步长,单位:字符数
+ "default": 2,
+ "android": 3,
+ "ios": 4,
+ "pc": 5
+ },
+ "print_strategy": "fast" // 流式更新策略,枚举值,可取:fast/delay
+ },
+ "summary": { // 卡片摘要信息。可通过该参数自定义客户端聊天栏消息预览中的展示文案。
+ "content": "自定义内容", // 自定义摘要信息。如果开启了流式更新模式,该参数将默认为“生成中”。
+ "i18n_content": { // 摘要信息的多语言配置。了解支持的所有语种。参考配置卡片多语言文档。
+ "zh_cn": "",
+ "en_us": "",
+ "ja_jp": ""
+ }
+ },
+ "locales": [ // JSON 2.0 新增属性。用于指定生效的语言。如果配置 locales,则只有 locales 中的语言会生效。
+ "en_us",
+ "ja_jp"
+ ], // 卡片支持的语言列表。
+ "enable_forward": true, // 是否支持转发卡片。默认值为 true。
+ "update_multi": true, // 是否为共享卡片。默认值为 true,JSON 2.0 暂时仅支持设为 true,即更新卡片的内容对所有收到这张卡片的人员可见。
+ "width_mode": "fill", // 卡片宽度模式。支持 "compact"(紧凑宽度 400px)模式 或 "fill"(撑满聊天窗口宽度)模式。默认不填时的宽度为 600px。
+ "use_custom_translation": false, // 是否使用自定义翻译数据。默认值 false。为 true 时,在用户点击消息翻译后,使用 i18n 对应的目标语种作为翻译结果。若 i18n 取不到,则使用当前内容请求翻译,不使用自定义翻译数据。
+ "enable_forward_interaction": false, // 转发的卡片是否仍然支持回传交互。默认值 false。
+ "style": { // 添加自定义字号和颜色。可应用在组件 JSON 数据中,设置字号和颜色属性。
+ "text_size": { // 分别为移动端和桌面端添加自定义字号,同时添加兜底字号。用于在组件 JSON 中设置字号属性。支持添加多个自定义字号对象。
+ "cus-0": {
+ "default": "medium", // 在无法差异化配置字号的旧版飞书客户端上,生效的字号属性。选填。
+ "pc": "medium", // 桌面端的字号。
+ "mobile": "large" // 移动端的字号。
+ }
+ },
+ "color": { // 分别为飞书客户端浅色主题和深色主题添加 RGBA 语法。用于在组件 JSON 中设置颜色属性。支持添加多个自定义颜色对象。
+ "cus-0": {
+ "light_mode": "rgba(5,157,178,0.52)", // 浅色主题下的自定义颜色语法
+ "dark_mode": "rgba(78,23,108,0.49)" // 深色主题下的自定义颜色语法
+ }
+ }
+ }
+ }
+}
+```
+`config` 下的各字段说明如下表所示。
+
+字段名称 | 是否必填 | 类型 | 默认值 | 说明
+---|---|---|---|---
+streaming_mode | 否 | Boolean | false | 卡片是否处于流式更新模式。详情参考[流式更新卡片](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/streaming-updates-openapi-overview)。
+streaming_config | 否 | object | / | 流式更新相关配置。详情参考[流式更新卡片](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/streaming-updates-openapi-overview)。
+summary | 否 | Object | / | 自定义摘要信息配置。即飞书客户端聊天栏消息预览中的文案。
+content | 否 | String | 无 | 摘要文本。当 `streaming_mode` 为 `true` 时,该字段默认为“生成中”。支持自定义。
+i18n_content | 否 | Object | / | 摘要文本的多语言配置。详情参考[局部国际化配置](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/configure-multi-language-content)。
+enable_forward | 否 | Boolean | true | 是否允许转发卡片。取值: - true:允许 - false:不允许
+update_multi | 否 | Boolean | true | 是否为共享卡片。取值: - true:是共享卡片,更新卡片的内容对所有收到这张卡片的人员可见。 - false:非共享卡片,仅操作用户可见卡片的更新内容。
+width_mode | 否 | String | default | 卡片宽度模式。取值: - default:默认宽度。PC 端宽版、iPad 端上的宽度上限为 600px。 - compact:紧凑宽度 400px - fill:自适应屏幕宽度 注意:卡片搭建工具上暂时不支持 `width_mode` 属性。
+use_custom_translation | 否 | Boolean | false | 是否使用自定义翻译数据。取值: - true:在用户点击消息翻译后,使用 i18n 对应的目标语种作为翻译结果。若 i18n 取不到,则使用当前内容请求飞书的机器翻译。 - false:不使用自定义翻译数据,直接请求飞书的机器翻译。
+enable_forward_interaction | 否 | Boolean | false | 转发的卡片是否仍然支持回传交互。
+style | 否 | Object | 空 | 添加自定义字号和颜色。可应用于组件的 JSON 数据中,设置字号和颜色属性。
+text_size | 否 | Object | 空 | 分别为移动端和桌面端添加自定义字号,同时添加兜底字号。用于在普通文本组件和富文本组件 JSON 中设置字号属性。支持添加多个自定义字号对象。详情参考[普通文本](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-components/content-components/plain-text)组件和[富文本(Markdown)](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-components/content-components/rich-text)组件。
+color | 否 | Object | 空 | 分别为飞书客户端浅色主题和深色主题添加 RGBA 语法。用于在组件 JSON 中设置颜色属性。支持添加多个自定义颜色对象。详情参考[颜色枚举值](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/enumerations-for-fields-related-to-color)。
+
+### 卡片全局跳转链接 `card_link`
+
+`card_link` 字段用于指定卡片整体的点击跳转链接。你可以配置一个默认链接,也可以分别为 PC 端、Android 端、iOS 端配置不同的跳转链接。
+
+```json
+"card_link": {
+ // 指定卡片整体的跳转链接。
+ "url": "https://www.baidu.com", // 默认链接地址。未配置指定端地址时,该配置生效。
+ "android_url": "https://developer.android.com/",
+ "ios_url": "https://developer.apple.com/",
+ "pc_url": "https://www.windows.com"
+ }
+```
+card_link 下的各字段说明如下表所示。
+**注意事项**:**注意**
+- url 和各端的链接(android_url、ios_url、pc_url)必填其中一个。如果不填写 url,则必须完整填写 android_url、ios_url、pc_url 三个字段。如果同时填写了 url 和 android_url、ios_url、pc_url,url 字段生效。
+- 如果需要禁止某端进行跳转,可以将对应的参数值配置为 `lark://msgcard/unsupported_action`。
+
+字段名称 | 是否必填 | 类型 | 说明
+---|---|---|---
+url | 否 | String | 默认的链接地址。
+pc_url | 否 | String | PC 端的链接地址。
+ios_url | 否 | String | iOS 端的链接地址。
+android_url | 否 | String | Android 端的链接地址。
+
+### 卡片标题 `header`
+
+`header` 字段用于配置卡片的标题。了解`header` 字段说明,参见[标题组件](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-components/content-components/title)。
+```json
+ "header": {} // 卡片标题
+```
+
+### 卡片正文 `body`
+
+在卡片的`body`字段中,你需要添加卡片组件作为卡片正文内容,组件将按数组顺序纵向流式排列。了解卡片组件,参考[卡片 JSON 2.0 版本组件概述](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-components/component-json-v2-overview)。
+
+在卡片 JSON 2.0 结构中,所有组件(标题组件除外)和元素(如 tag 为 plain_text 的文本元素)新增 element_id 属性,作为操作组件或元素的唯一标识。在同一张卡片内,该字段的值全局唯一。仅允许使用字母、数字和下划线,必须以字母开头,不得超过 20 字符。
+```json
+{
+ "body": { // 卡片正文。
+ "elements": [ // 在此传入各个组件的 JSON 数据,组件将按数组顺序纵向流式排列。
+ {
+ "tag": "xxx", // 组件的标签。
+ "element_id": "custom_id" // 操作组件的唯一标识。
+ }
+ ]
+ }
+}
+```
diff --git a/docs/feishu/card_json_schema_2_breaking_changes.md b/docs/feishu/card_json_schema_2_breaking_changes.md
new file mode 100644
index 0000000..2b22bfb
--- /dev/null
+++ b/docs/feishu/card_json_schema_2_breaking_changes.md
@@ -0,0 +1,239 @@
+# 卡片 JSON 2.0 版本更新说明
+
+本文档介绍卡片 JSON 2.0 版本与 1.0 版本结构之间的不兼容变更和优化说明。了解完整的 JSON 2.0 结构数据,参考[卡片 JSON 2.0 结构](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-structure)。
+
+## 注意事项
+
+- 卡片 JSON 2.0 结构支持飞书客户端 7.20 及之后版本。当使用 JSON 2.0 结构的卡片发送至低于 7.20 版本的客户端时,卡片标题可正常显示,但内容将展示兜底的升级提示文案。
+
+
+
+- 卡片 JSON 2.0 结构暂时仅支持共享卡片,不支持独享卡片配置。即 `update_multi` 参数仅支持设为 `true`。
+
+## 不兼容变更
+
+本小节介绍卡片 JSON 2.0 版本相对于 1.0 版本所发生的不兼容变更。
+
+### 卡片交互有效期变更
+
+- 1.0 结构:发出卡片的可交互时间为 30 天,可更新时间为 14 天(如果在第 14-30 天交互卡片,且交互回调动作为更新卡片,更新动作将不会生效)。
+- 2.0 结构:卡片可交互和可更新时间统一为 14 天。
+
+### 属性校验变更
+
+在 JSON 2.0 版本中,传入不支持的属性将报错。
+| **1.0 结构** | **2.0 结构** |
+| ------------- | ----------- |
+| 传入不支持的属性作忽略处理 | 传入不支持的属性将报错 |
+
+### JSON 全局结构和字段变更
+
+- **结构变更**
+
+- JSON 2.0 版本新增 `body` 字段,`elements` 属性放置在 `body` 层级下。了解 2.0 整体结构,参考[卡片 JSON 2.0 结构](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-structure)。
+ - JSON 2.0 版本不再支持通过 `i18n_elements` 字段设置全局多语言。你可通过 `i18n_content` 等局部多语言字段实现组件级别的多语言配置,详情参考[配置卡片多语言](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/configure-multi-language-content)。
+
+- **`fallback`** **字段变更**
+
+JSON 2.0 版本暂不支持使用 `fallback` 字段配置自定义的全局降级规则。
+
+- **默认值变更**
+
+JSON 2.0 版本中的 `update_multi` 默认值变更为 `true`,且暂时仅支持设为 `true`。`update_multi` 属性用于设置卡片是否为共享卡片;`true` 表示设置卡片为共享卡片,更新卡片的内容对所有收到这张卡片的人员可见;`false` 表示设置卡片为独享卡片,更新卡片的内容对他人不可见。
+
+1.0 结构 | 2.0 结构
+---|---
+```json { "schema": "1.0", // 不填默认为 1.0 "config": { "update_multi": false // 默认值为 false。 }, "card_link": {}, "header": {}, "i18n_header": {}, "elements": [], "i18n_elements": {}, "fallback": {} } ``` | ```json { "schema": "2.0", // 2.0 需主动声明 "config": { "update_multi": true // 默认值为 true,且暂时仅支持设为 `true` }, "card_link": {}, "header": {}, "body": { // 新增 body 字段,elements 属性放置在 body 层级下。 "elements": [] // 不再支持 i18n_elements 字段 }, "fallback": {} } ```
+
+### 容器类组件布局属性默认值变更
+
+- [表单容器](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-components/containers/form-container)的 `vertical_spacing` 和 `horizontal_spacing` 字段的默认值由 `16px` 改为 `12px`,且支持开发者自定义配置。
+
+1.0 结构 | 2.0 结构
+---|---
+```json { "margin": "0", // 容器的外边距设置。 "padding": "0", // 容器的内边距设置。 "vertical_spacing": "16px", // 容器内组件的垂直边距设置。 "horizontal_spacing": "16px" // 容器内组件的水平边距设置。 } ``` | ```json { "margin": "0", "padding": "0", "vertical_spacing": "12px", // 默认值变更,且支持自定义。 "horizontal_spacing": "12px" // 默认值变更,且支持自定义。 } ```
+
+
+
+- [交互容器](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-components/containers/interactive-container)的 `vertical_spacing` 和 `horizontal_spacing` 字段的默认值由 `12px` 改为 `4px` 和 `8px`,且支持开发者自定义配置。
+
+1.0 结构 | 2.0 结构
+---|---
+```json { "margin": "0", // 容器的外边距设置。 "padding": "4px 12px", // 容器的内边距设置。 "vertical_spacing": "12px", // 容器内组件的垂直边距设置。 "horizontal_spacing": "12px" // 容器内组件的水平边距设置。 } ``` | ```json { "margin": "0", "padding": "4px 12px", "vertical_spacing": "4px", // 默认值变更,且支持自定义。 "horizontal_spacing": "8px" // 默认值变更,且支持自定义。 } ```
+
+
+- [折叠面板](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-components/containers/collapsible-panel)的 `padding` 字段默认值变更:
+ - 当折叠面板配置了边框(border)或背景色(background_color)时,标题区 `padding` 字段的默认值变更为上下边距 4px,左右边距 8px。
+
+1.0 结构 | 2.0 结构
+---|---
+```json // 有边框(border)或背景色(background_color)时 { "header": { "margin": "0", "padding": "8px" // 标题区的内边距。 }, "margin": "0", "padding": "8px", "vertical_spacing": "8px", "horizontal_spacing": "8px" } ``` | ```json // 有边框(border)或背景色(background_color)时 { "header": { "margin": "0", "padding": "4px 8px" // 标题区的内边距默认值变更。上下边距为 4px,左右边距为 8px。 }, "margin": "0", "padding": "8px", "vertical_spacing": "8px", "horizontal_spacing": "8px" } ```
+
+- 当折叠面板未配置边框(border)或背景色(background_color)时,标题区 `padding` 字段的默认值变更为 0,内容区的内边距默认值变更为上边距 8px,右、下、左边距 0。
+
+1.0 结构 | 2.0 结构
+---|---
+```json // 无边框(border)或背景色(background_color)时 { "header": { "margin": "0", "padding": "8px 0 8px 0" // 标题区的内边距。 }, "margin": "0", "padding": "0", "vertical_spacing": "8px", "horizontal_spacing": "8px" } | ```json // 无边框(border)或背景色(background_color)时 { "header": { "margin": "0", "padding": "0" // 标题区的内边距默认值变更 }, "margin": "0", "padding": "8px 0 0 0", // 内容区的内边距默认值变更。上边距为 8px,右、下、左边距为 0px。 "vertical_spacing": "8px", "horizontal_spacing": "8px" }
+
+### `vertical_spacing` 和 `horizontal_spacing` 枚举值 & 映射数值变更
+
+1.0 结构 | 2.0 结构
+---|---
+vertical_spacing 和 horizontal_spacing字段的枚举和对应的值为: - small:4px - medium:8px - large:16px | vertical_spacing 和 horizontal_spacing字段的枚举和对应的值为: - small:4px - medium:8px - large:12px - extra_large:16px
+
+### 标题组件配置变更
+
+- 标题组件的 icon 配置结构变更,对齐其它组件:
+
+1.0 结构 | 2.0 结构
+---|---
+```json { "header": { "title": {}, "icon": { "img_key": "img_v2_38811724" }, "ud_icon": { "token": "chat-forbidden_outlined", "style": { "color": "red" } } } } ``` | ```json { "header": { "title": {}, "icon": { "tag": "standard_icon", "token": "chat-forbidden_outlined", "color": "orange", "img_key": "img_v2_38811724" } } } ```
+
+### 图片组件不再支持通栏配置
+
+1.0 结构 | 2.0 结构
+---|---
+支持 stretch_without_padding 通栏配置,图片的宽度将撑满卡片宽度。 ```json { "tag": "img", "img_key": "img_v3_0238_073f1823-df2b-4377-86c6-e293f183622j", "size": "stretch_without_padding" // 支持通栏配置,图片宽度将撑满卡片宽度。 } ``` | 不再支持通栏配置,但可设置 margin 字段为负数实现通栏效果。 ```json { "tag": "img", "img_key": "img_v3_0238_073f1823-df2b-4377-86c6-e293f183622j", "size": "crop_center", "margin": "4px -12px" } ```
+
+### 富文本(Markdown)组件废弃差异化跳转语法
+
+2.0 结构不再支持以下差异化跳转语法。你可使用` ` 标签替代,如 ` `` 战略研讨会 `。
+```json
+{
+ "tag": "markdown",
+ "href": {
+ "urlVal": {
+ "url": "xxx",
+ "pc_url": "xxx",
+ "ios_url": "xxx",
+ "android_url": "xxx"
+ }
+ },
+ "content": "[差异化跳转]($urlVal)"
+}
+```
+
+### 兜底高度 & 宽度变更
+
+1.0 结构 | 2.0 结构
+---|---
+- 卡片兜底高度:24px - 组件宽度设置的像素值如果大于父容器宽度,会收缩限制到父容器宽度,仅在交互容器中会截断展示 | - 卡片的兜底高度:40px - 组件宽度设置的像素值如果大于父容器宽度,将会截断展示
+
+### 废弃备注组件 & 交互模块
+
+2.0 结构不再支持[备注](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-components/content-components/note)(note)组件和[交互模块](https://open.feishu.cn/document/ukTMukTMukTM/uYzM3QjL2MzN04iNzcDN/component-list/common-components-and-elements)("tag" 为 "action"),相关效果可由以下组件和属性实现:
+- 备注(note)组件:可由普通文本组件配置 notation 字号、grey 字体颜色、icon 属性替代;
+- 交互模块:可由按钮(button)或折叠按钮组(overflow)组件配置合适的组件间距 (`vertical_spacing` 和 `horizontal_spacing`) 替代。
+
+## 新增属性和优化说明
+
+本小节介绍 2.0 结构新增的属性和优化点。
+
+### 新增 `streaming_mode` 属性,支持流式更新
+
+2.0 结构新增 `streaming_mode` 和 `summary` 字段,支持卡片流式更新、文本流式更新能力。详情参考[流式更新 OpenAPI 调用指南](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/streaming-updates-openapi-overview)。
+```json
+{
+ "schema": "2.0", // 卡片 JSON 结构的版本。默认为 1.0。
+ "config": {
+ "streaming_mode": true, // 卡片是否处于流式更新模式,默认值为 false。
+ "summary": {
+ "content": "自定义内容", // 自定义摘要信息。默认为“生成中”。
+ "i18n_content": { // 摘要信息的多语言配置。了解支持的所有语种。参考。
+ "zh_cn": "",
+ "en_us": "",
+ "ja_jp": ""
+ }
+ }
+ }
+}
+```
+
+### 新增 `element_id` 属性,用于操作组件
+
+所有组件和元素(如 `tag` 为 `plain_text` 的文本元素)新增 `element_id` 属性,作为操作组件或元素的唯一标识。在同一张卡片内,该字段的值全局唯一。仅允许使用字母、数字和下划线,必须以字母开头,不得超过 20 字符。
+```json
+{
+ "tag": "button", // 组件的标签。
+ "element_id": "button_1" // 操作组件时的唯一标识。
+}
+```
+
+### 组件统一支持布局相关能力
+
+卡片 JSON 2.0 结构中,各类组件统一新增了一批布局类属性。
+```json
+// 卡片层级
+{
+ "schema": "2.0",
+ "header": {
+ "title": {},
+ "padding": "4px" // 支持设置[0,99]px
+ },
+ "body": {
+ "vertical_spacing": "4px", // body 内子组件的垂直间距,支持设置[0,99]px
+ "padding": "4px", // body 的内边距配置,支持设置[0,99]px
+ "elements": []
+ }
+}
+// 组件
+{
+ "tag": "xxxx",
+ // 各组件均新增的布局类属性
+ "margin": "4px", // 外边距,默认值 "0",支持范围 [-99,99]px
+ // 容器类组件(含elements)新增的布局类属性,用于控制子元素排列
+ "padding": "4px", // 内边距,支持范围 [0,99]px
+ "direction": "vertical", // 布局方向,支持 "vertical"|"horizontal",默认值 "vertical"
+ "horizontal_spacing": "3px", // 水平间距,支持范围 [0,99]px
+ "vertical_spacing": "4px", // 垂直间距,支持范围 [0,99]px
+ "horizontal_align": "left", // 水平对齐,支持 "left"|"center"|"right",默认值 "left"
+ "vertical_align": "center", // 垂直对齐,支持 "top"|"center"|"bottom",默认值 "top"
+ // 其他
+ "elements": []
+}
+```
+[普通文本组件](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-components/content-components/plain-text)支持配置 width 属性。可取值:
+- fill:文本的宽度将与组件宽度一致,撑满组件。
+- auto:文本的宽度自适应文本内容本身的长度。
+- [16,999]px:自定义文本宽度。
+```json
+{
+ "tag": "div",
+ "width": "fill", // 文本宽度。支持 "fill"|"auto"|"{{[16,999]}}px"。默认值为 fill。
+ }
+```
+
+### 富文本组件支持标准 markdown 语法
+
+[卡片 JSON 2.0 结构](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-structure)支持除 `HTMLBlock` 外所有标准的 Markdown 语法和部分 HTML 语法。了解 Markdown 标准语法,请参考 [CommonMark Spec 官方文档](https://spec.commonmark.org/0.31.2/)。你也可以使用 [CommonMark playground](https://spec.commonmark.org/dingus/) 预览 Markdown 效果。了解更多,参考[富文本(Markdown)](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-components/content-components/rich-text)。
+
+注意,在卡片的富文本组件中,以下语法的渲染效果与 CommonMark 有差异:
+- 富文本组件支持使用一个 Enter 键作为软换行(Soft Break);支持两个 Enter 键作为硬换行(Hard Break)。软换行在渲染时可能会被忽略,具体取决于渲染器如何处理;硬换行在渲染时始终会显示为一个新行。
+
+- 2.0 结构支持以下 HTML 语法:
+ - 开标签 ` `
+ - 自闭合标签 ` `
+ - 开标签 ` `
+ - 自闭合标签 ` `
+ - 闭合标签 ` `
+ - 闭合标签 ` `
+ - 闭合标签 ` `
+ - 闭合标签 ` `
+ - 闭合标签 ` `
+ - 闭合标签 ` `
+ - 闭合标签 ` `
+ - 闭合标签 ``,支持嵌套其它标签,如 `redgreenagain `。其它标签包括:
+ - 闭合标签 ` `
+ - 闭合标签 ` `
+ - 闭合标签 ` `
+ - 闭合标签 ` `
+ - 闭合标签 ` `
+
+### 容器类组件新增可内嵌的组件类型
+
+JSON 2.0 结构中,表单容器、交互容器、折叠面板、分栏组件可内嵌除表单容器和表格组件外的其它所有组件。
+
+1.0 结构 | 2.0 结构
+---|---
+- 表单容器:不支持内嵌表格、图表、和表单容器组件;不可直接内嵌普通文本组件 - 交互容器:仅支持内嵌普通文本、富文本、图片、备注、分栏、勾选器、交互容器组件 - 折叠面板:不支持内嵌表单容器(form)和表格组件(table)组件 - 分栏:不支持内嵌表格(table)、表单(form)和多图混排(img_combination)组件 | 表单容器、交互容器、折叠面板、分栏组件可内嵌除表单容器(form)和表格组件(table)外的其它所有组件。
+
\ No newline at end of file
diff --git a/orchestrator/agent.py b/orchestrator/agent.py
index acbfd59..00876dd 100644
--- a/orchestrator/agent.py
+++ b/orchestrator/agent.py
@@ -49,7 +49,7 @@ Bot command prefix: {prefix}
- Close/switch sessions: "关掉第一个" → run_command("{prefix}close 1")
- Change routing mode: "切换到直连模式" → run_command("{prefix}direct")
- Set a reminder: "10分钟后提醒我" → run_command("{prefix}remind 10m 提醒我")
-- Check status: "看看现在有哪些 session" → run_command("{prefix}status")
+- Check status: "看看现在有哪些 session" → run_command("{prefix}list")
- Any other command the user would type manually
Available bot commands (pass verbatim to run_command):
@@ -59,7 +59,7 @@ Available bot commands (pass verbatim to run_command):
{prefix}perm [conv_id] — permission mode: default, edit, plan, bypass
{prefix}direct — direct mode (bypass LLM for CC messages)
{prefix}smart — smart mode (LLM routing, default)
- {prefix}status — list sessions
+ {prefix}list — list sessions
{prefix}remind — one-shot reminder
{prefix}tasks — list background tasks
diff --git a/tests/test_commands.py b/tests/test_commands.py
index f06954f..b54fa5c 100644
--- a/tests/test_commands.py
+++ b/tests/test_commands.py
@@ -36,9 +36,14 @@ class TestHelp:
from bot.commands import handle_command
_setup_user()
reply = await handle_command("user_abc123", "//help")
- for cmd in ("//new", "//status", "//close", "//switch", "//perm",
- "//stop", "//progress", "//direct", "//smart", "//shell"):
+ for cmd in ("//new", "//list", "//close", "//switch", "//perm",
+ "//stop", "//progress", "//direct", "//smart", "//shell",
+ "//help"):
assert cmd in reply, f"Missing {cmd} in help"
+ # Should NOT list //retry (unimplemented)
+ assert "//retry" not in reply
+ # Should list aliases
+ assert "alias" in reply.lower()
@pytest.mark.asyncio
async def test_h_alias(self):
@@ -184,12 +189,12 @@ class TestSwitch:
# ── //status ────────────────────────────────────────────────────────────────
-class TestStatus:
+class TestList:
@pytest.mark.asyncio
async def test_no_sessions(self):
from bot.commands import handle_command
_setup_user()
- reply = await handle_command("user_abc123", "//status")
+ reply = await handle_command("user_abc123", "//list")
assert "No active sessions" in reply
@pytest.mark.asyncio
@@ -198,7 +203,7 @@ class TestStatus:
_setup_user()
_add_session("s1")
_add_session("s2")
- reply = await handle_command("user_abc123", "//status")
+ reply = await handle_command("user_abc123", "//list")
assert "s1" in reply
assert "s2" in reply
@@ -207,9 +212,16 @@ class TestStatus:
from bot.commands import handle_command
_setup_user()
_add_session("s1", activate=True)
- reply = await handle_command("user_abc123", "//status")
+ reply = await handle_command("user_abc123", "//list")
assert "→" in reply
+ @pytest.mark.asyncio
+ async def test_status_alias_still_works(self):
+ from bot.commands import handle_command
+ _setup_user()
+ reply = await handle_command("user_abc123", "//status")
+ assert "No active sessions" in reply
+
# ── //perm ──────────────────────────────────────────────────────────────────
diff --git a/tests/test_sdk_migration.py b/tests/test_sdk_migration.py
index 84d66a5..255e7a5 100644
--- a/tests/test_sdk_migration.py
+++ b/tests/test_sdk_migration.py
@@ -767,20 +767,18 @@ class TestBuildApprovalCard:
assert "权限审批" in card["header"]["title"]["content"]
body_elements = card["body"]["elements"]
- # Should have markdown, action, and note elements
tags = [e["tag"] for e in body_elements]
+ # JSON 2.0: no "action" wrapper, no "note" — buttons are direct elements
+ assert "action" not in tags
+ assert "note" not in tags
assert "markdown" in tags
- assert "action" in tags
- assert "note" in tags
+ assert tags.count("button") == 2
- # Action should have 2 buttons
- action_el = next(e for e in body_elements if e["tag"] == "action")
- assert len(action_el["actions"]) == 2
- # First button should be approve
- assert action_el["actions"][0]["value"]["action"] == "approve"
- assert action_el["actions"][0]["value"]["conv_id"] == "c1"
- # Second button should be deny
- assert action_el["actions"][1]["value"]["action"] == "deny"
+ # Buttons carry approve/deny values
+ buttons = [e for e in body_elements if e["tag"] == "button"]
+ assert buttons[0]["value"]["action"] == "approve"
+ assert buttons[0]["value"]["conv_id"] == "c1"
+ assert buttons[1]["value"]["action"] == "deny"
# ===========================================================================