opencode-feishu-plugin 0.1.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.en.md +74 -5
  2. package/README.md +135 -241
  3. package/dist/index.js +1958 -473
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -2,27 +2,32 @@
2
2
 
3
3
  [English](./README.en.md) | **简体中文**
4
4
 
5
- 把 [OpenCode](https://opencode.ai) 接进飞书:**一个飞书话题 = 一个 OpenCode 会话**。你在飞书里用话题管理多个会话,在话题里指挥 AI 写代码,**权限审批直接在飞书卡片上点按钮**。
5
+ 把 [OpenCode](https://opencode.ai) 接进飞书:**一个飞书话题 = 一个 OpenCode 会话**。你在飞书里用话题管理多个会话、指挥 AI 写代码,**权限审批直接在飞书卡片上点按钮**。
6
6
 
7
- > **只用 OpenCode V2 插件 API**(`Plugin.define({ id, setup(ctx) })`),不依赖任何 V1 包。
8
- > **纯长连接**(WebSocket)收发事件与卡片回调,**不监听端口、不需要公网地址**。
7
+ - **只用 OpenCode V2 插件 API**(`Plugin.define({ id, setup(ctx) })`),不依赖任何 V1 包。
8
+ - **纯长连接**(WebSocket)收发事件与卡片回调,不监听端口、不需要公网地址。
9
9
 
10
10
  ---
11
11
 
12
+ ## 效果预览
13
+
14
+ 在飞书话题里指挥 AI 干活,权限审批、状态跟踪都在卡片上完成:
15
+
16
+ ![飞书话题内的完整会话:工具调用、权限审批卡与强制停止](image/image.png)
17
+
12
18
  ## 亮点
13
19
 
14
20
  | | 说明 |
15
21
  |---|---|
16
- | 🔐 **最小权限** | 只要 2 个 scope(单聊读 + 发消息)。**不申请任何群权限**,机器人物理上收不到群消息 |
17
- | 💬 **话题 = 会话** | 每个飞书话题对应一个 OpenCode 会话。主聊天流只做管理,话题里干活,互不串台 |
22
+ | 🔐 **最小权限** | 只要 2 个 scope(单聊读 + 发消息)。不申请任何群权限,机器人物理上收不到群消息 |
23
+ | 💬 **话题 = 会话** | 每个飞书话题对应一个 OpenCode 会话,主聊天流只做管理,话题里干活,互不串台 |
18
24
  | 🚀 **一键进入** | `/new` 直接发建会话表单,提交后机器人自动在你消息下开话题 |
19
- | 📝 **表单一次填完** | `/new` 与 `/form` **完全等价**:目录 + 模型 + 权限一张表单一次提交即建会话。目录既可直接输入,也可从下拉选择允许根目录的一级子目录。**零新增权限** |
20
- | 🗂 **目录容错** | 目录留空 = 用允许根目录;不存在会自动创建(仍受 `allowedRoots` 白名单约束) |
21
- | ✅ **卡片审批** | 权限请求变成飞书卡片(允许一次 / 始终允许 / 拒绝),点击即批准,带签名防伪防重放 |
25
+ | 📝 **表单一次填完** | `/new` 与 `/form` 完全等价:目录 + 模型 + 权限一张表单一次提交即建会话,**零新增权限** |
26
+ | ✅ **卡片审批** | 权限请求变成飞书卡片(允许一次 / 始终允许 / 拒绝 / 本会话内允许),点击即批准,带签名防伪防重放 |
22
27
  | 🪜 **权限预设** | 只读 / 可编辑 / 高风险审批 / 完全信任,四档一次选定,告别逐次审批 |
23
28
  | 📊 **实时可见** | 先回执「思考中」,工具调用实时上卡(≥3 个自动折叠),文本流式更新,页脚显示当前模型 |
24
- | 🧵 **原生排队** | 会话忙时自动排队(OpenCode 原生 `delivery:"queue"`),不丢消息 |
25
- | ⏹ **一键强停** | **所有 AI 回复卡片都带「强制停止」按钮**,点击即中断(签名校验:白名单 + HMAC)。卡死会话由看门狗自动中断,不再永久排队 |
29
+ | 🧵 **原生排队** | 会话忙时自动排队(opencode 原生 `delivery:"queue"`),可用 `/steer` `/now` 插队 |
30
+ | ⏹ **一键强停** | 所有 AI 回复卡片都带「强制停止」按钮;卡死会话由看门狗自动中断 |
26
31
  | 🚫 **无端口** | 全程长连接,服务器不用开放任何入站端口 |
27
32
 
28
33
  ---
@@ -34,22 +39,19 @@
34
39
  3. **权限管理**,只开通这两个:
35
40
  - `im:message.p2p_msg:readonly` —— 读取用户发给机器人的单聊消息
36
41
  - `im:message:send_as_bot` —— 以应用身份发消息(也用于更新卡片)
37
- 4. **事件与回调 → 事件配置**:订阅方式选 **「使用长连接接收事件」**(不要选 Webhook),添加事件 `im.message.receive_v1`。
38
- 5. **事件与回调 → 回调配置**:订阅方式同样选 **长连接**,添加回调 `card.action.trigger`(**该项零权限要求**)。
39
- 6. **版本管理与发布**:**可用范围 = 仅本人**(只勾你自己),创建版本并发布。
42
+ 4. **事件与回调 → 事件配置**:订阅方式选**「使用长连接接收事件」**(不要选 Webhook),添加事件 `im.message.receive_v1`。
43
+ 5. **事件与回调 → 回调配置**:订阅方式同样选**长连接**,添加回调 `card.action.trigger`(**零权限要求**)。
44
+ 6. **版本管理与发布**:可用范围 = **仅本人**,创建版本并发布。
40
45
  7. 记下 **App ID**(`cli_…`)与 **App Secret**。
41
46
 
42
- > **为什么不申请群权限?** 本插件的设计是"一个人的遥控台"。不申请群权限,机器人**物理上收不到群消息**,
47
+ > **为什么不申请群权限?** 本插件是"一个人的遥控台"。不申请群权限,机器人**物理上收不到群消息**,
43
48
  > 单人边界由平台 scope 层保证,而不是只靠代码判断。
44
49
 
45
50
  ---
46
51
 
47
52
  ## 二、安装
48
53
 
49
- ### 1. 安装插件(V2:由 npm 自动加载,推荐)
50
-
51
- OpenCode V2 通过配置 `plugins` 数组声明要加载的包,启动时自动用 Bun 安装
52
- (缓存于 `~/.cache/opencode/node_modules/`)。两种等价写法:
54
+ ### 1. 安装插件(V2:npm 自动加载,推荐)
53
55
 
54
56
  ```bash
55
57
  # 方式 A:CLI(推荐)
@@ -58,38 +60,13 @@ opencode plugin add opencode-feishu-plugin
58
60
 
59
61
  ```jsonc
60
62
  // 方式 B:手写 ~/.config/opencode/opencode.jsonc
61
- {
62
- "$schema": "https://opencode.ai/config.json",
63
- "plugins": ["opencode-feishu-plugin"]
64
- }
65
- ```
66
-
67
- 插件入口由 `package.json#exports` 指向**自包含**的 `dist/index.js`(含飞书 SDK 等依赖),
68
- 运行时不需要你手动 `npm install`,也不需要额外 `node_modules`。
69
-
70
- **本地开发(不走 npm)**:克隆后 `npm install && npm run build`,把本地目录写进 `plugins`:
71
-
72
- ```jsonc
73
- { "plugins": ["./path/to/opencode-feishu-plugin"] }
63
+ { "plugins": ["opencode-feishu-plugin"] }
74
64
  ```
75
65
 
76
- ### 2. 另一种加载方式:全局插件目录(离线 / 固定目录)
66
+ 插件入口指向**自包含**的 `dist/index.js`(含飞书 SDK 等依赖),运行时无需手动 `npm install`。
67
+ 本地开发则克隆后 `npm install && npm run build`,把本地目录写进 `plugins`:`{ "plugins": ["./path/to/opencode-feishu-plugin"] }`。
77
68
 
78
- 也可把构建产物放进 `<configDir>/plugins/<任意名>/`(`configDir` = `OPENCODE_CONFIG_DIR` 或 `~/.config/opencode`)。
79
- OpenCode 会自动发现该目录下的 `index.js`(本包根部已带该入口,转发到 `dist/`):
80
-
81
- ```bash
82
- cd /path/to/opencode-feishu-plugin && npm install && npm run build
83
- mkdir -p ~/.config/opencode/plugins/feishu
84
- cp -r dist index.js package.json ~/.config/opencode/plugins/feishu/
85
- ```
86
-
87
- > 无论用哪种方式加载,**升级后都需要重启服务**才会重新 import 模块:
88
- > ```bash
89
- > opencode service restart
90
- > ```
91
-
92
- ### 3. 写配置
69
+ ### 2. 写配置
93
70
 
94
71
  `<configDir>/plugins/feishu.json`(`configDir` = `OPENCODE_CONFIG_DIR` 或 `~/.config/opencode`):
95
72
 
@@ -104,16 +81,16 @@ JSON
104
81
  chmod 600 ~/.config/opencode/plugins/feishu.json
105
82
  ```
106
83
 
107
- 把凭证放进 OpenCode **服务进程**的环境变量(不是你的交互 shell):
84
+ 凭证放进 OpenCode **服务进程**的环境变量(不是交互 shell):
108
85
 
109
86
  ```bash
110
87
  opencode service set env FEISHU_APP_ID cli_xxxxxxxx
111
88
  opencode service set env FEISHU_APP_SECRET xxxxxxxx
112
89
  ```
113
90
 
114
- 也可以直接把明文写进 `feishu.json`(权限记得 `600`)。**优先级**:`options` > `feishu.json` > 环境变量。
91
+ 也可直接明文写进 `feishu.json`(权限记得 `600`)。**优先级**:`options` > `feishu.json` > 环境变量。
115
92
 
116
- ### 4. 生效与确认
93
+ ### 3. 生效与确认
117
94
 
118
95
  ```bash
119
96
  opencode reload # 重新加载配置与插件
@@ -122,6 +99,11 @@ opencode mcp list # 顺带确认服务健康
122
99
 
123
100
  在飞书里给机器人发一条消息。**第一次发消息的人会被自动绑定为 owner**,之后其他人被静默忽略。
124
101
 
102
+ > 用全局插件目录加载(离线 / 固定目录)或升级插件后,需要重启服务才重新 import:
103
+ > ```bash
104
+ > opencode service restart
105
+ > ```
106
+
125
107
  ---
126
108
 
127
109
  ## 三、怎么用
@@ -132,184 +114,111 @@ opencode mcp list # 顺带确认服务健康
132
114
 
133
115
  | 命令 | 作用 |
134
116
  |---|---|
135
- | `/new [标题]` | **直接发建会话表单卡**,提交即建会话并自动开话题(与 `/form` 等价;标题作为会话标题) |
117
+ | `/new [标题]` | 发建会话表单卡,提交即建会话并自动开话题(与 `/form` 等价) |
136
118
  | `/form [标题]` | 同上,`/new` 的等价入口 |
137
- | `/sessions`(`/ls`) | **全部**会话列表卡片:每条显示标题/短 id/相对时间/「💬 已绑话题」/「📍 目录」,并提供「▶️ 进入话题」按钮;分页 8 条(可配 `sessionPageSize`,5–20) |
138
- | `/use <序号\|id前缀>` | 切换当前会话(旧行为,保留兼容) |
139
- | `/resume [序号]` | **续聊历史会话**:对最近更新(或列表第 N 个)的会话直接开话题进入 |
119
+ | `/sessions`(`/ls`) | **全部**会话列表卡片(含本机所有 opencode 会话),可翻页、可进话题、可新建 |
120
+ | `/resume [序号]` | 对最近更新(或列表第 N 个)的会话发恢复卡,**回复该卡**即续聊 |
140
121
  | `/current` | 查看当前会话 |
141
- | `/stop` | 中断当前会话正在跑的任务(每张运行卡上也都有「⏹ 强制停止」按钮) |
142
- | `/steer <文本>` | 发送一条**立即插队**的消息(打断当前步骤插入执行,不等排队) |
143
- | `/now` | 把该会话**已排队**的未执行消息全部改为立即插队执行 |
144
- | `/dir <路径>` | 给表单**预填**工作目录(留空 = 允许根目录;不存在会自动创建) |
145
- | `/model [关键词]` | 给表单**预填**模型(也可在话题内切换当前会话模型) |
146
- | `/perm [档位]` | 给表单**预填**权限档位(也可在话题内修改当前会话权限) |
122
+ | `/stop` | 中断当前会话正在跑的任务 |
123
+ | `/steer <文本>` | 立即插队发送一条消息(打断当前步骤,不等排队) |
124
+ | `/now` | 把该会话已排队的消息全部改为立即执行 |
125
+ | `/dir` `/model` `/perm` | 给表单**预填**工作目录 / 模型 / 权限档位 |
147
126
  | `/cancel` | 放弃未提交的表单 |
148
127
  | `/help` | 命令列表 |
149
128
 
150
129
  ### 话题内(干活)
151
130
 
152
- 一个话题 = 一个会话。**在话题里发普通文本就是给 AI 下指令**;回复会留在同一话题内。
131
+ 一个话题 = 一个会话,发普通文本就是给 AI 下指令。
153
132
 
154
133
  | 命令 | 作用 |
155
134
  |---|---|
156
135
  | `/model` | 切换本会话模型 |
157
136
  | `/perm` | 修改本会话权限档位 |
158
- | `/cd <路径>` | 迁移本会话工作目录(留空 = 回到允许根目录;不存在会自动创建) |
159
- | `/steer <文本>` | 立即插队发送一条消息(打断当前步骤插入执行) |
160
- | `/now` | 把本会话已排队的未执行消息改为立即插队执行 |
137
+ | `/cd <路径>` | 迁移本会话工作目录 |
138
+ | `/steer <文本>` | 立即插队发一条消息 |
139
+ | `/now` | 把本会话排队消息改为立即执行 |
161
140
  | `/current` `/stop` `/help` | 同主聊天流,作用于本话题会话 |
162
141
 
163
- ### 模型切换的真实语义(`/model`)
164
-
165
- `/model` 切换只影响**后续**的模型调用,**不会**改写历史消息:
166
-
167
- - opencode 的 `switchModel` 语义就是「切换后续 provider turn」,并在会话里追加一条 `model-switched` 标记;此前的 assistant 消息仍带着它们**当时实际使用**的模型。
168
- - 因此「`Session.Info.model` 已经是新模型,但更早那批消息仍是旧模型」是**预期行为**,不是没切成功。
169
- - 为稳妥起见,插件切换后会**读回** `ctx.session.get` 校验真实模型:一致才显示「✅ 已切换模型」;读回不一致会明确提示「⚠️ 模型可能未生效」;读回失败会降级为请求值并提示未校验。**运行卡页脚与 `/current` 显示的模型同样以读回的真实值为准**。
170
- - 切换失败(无权限 / 会话不存在等)时回执会给出错误原因,而不是假装成功。
171
-
172
- ### 主题软引导(话题内不硬拦截离题)
173
-
174
- 从飞书 `/new <标题>` 或表单建会话时,标题就是该话题的「主题」。插件**不会**拦截话题内离题的消息,只在 system 里注入一句轻量说明,让 AI 在用户明显转向无关任务时**简短提醒**「可用 `/new` 开新会话」,但不会因此拒答、也不会长篇说教:
175
-
176
- - 仅对**从飞书发起的会话**注入;本地 TUI 等会话**绝不注入**(不污染你自己的会话)。
177
- - 取不到会话标题时跳过注入;注入失败只记 `log.warn`,不影响正常执行。
178
- - 可用 `topicGuidance: false` 完全关闭。
179
-
180
142
  ### 建会话(`/new` 与 `/form` 完全等价)
181
143
 
182
144
  ```
183
- /new 修一下登录 bug (或 /form 修一下登录 bug)
184
- ↓
185
- 📝 建会话表单卡
186
- 工作目录:输入框手填,或从下拉选择(允许根目录的一级子目录);留空 = 允许根目录,不存在会自动创建
187
- 模型: 下拉选(默认最近/当前)
188
- 权限: 四档选一
189
- ↓ 点 「✅ 创建会话」
190
- 表单消息本身成为话题根:机器人对它 reply_in_thread 发出「会话已就绪」卡
191
- ↓
192
- 表单卡被就地改写成成功卡,标题 = `✅ 已创建 · <会话标题>`
193
- (该标题就是话题显示名,一眼可见这是一个已成功创建的会话)
145
+ /new 修一下登录 bug
194
146
  ↓
195
- 点进话题直接发消息即可
147
+ 📝 建会话表单卡(工作目录 / 模型 / 权限,一次填完)
148
+ ↓ 点「✅ 创建会话」
149
+ 表单消息本身成为话题根,机器人 reply_in_thread 发「会话已就绪」卡
196
150
  ```
197
151
 
198
- - `/new` 与 `/form` 走**同一入口**、直接发建会话表单卡;**不再有**目录→模型→权限→确认的分步卡。
199
- - `/dir` `/model` `/perm` 仍可用,但只作为**给表单预填字段**的能力(不再是必经步骤):执行后会自动回一张预填好的新表单卡。
200
- - 表单提交后先消费向导状态,防连点重复建会话;目录非法时**不建会话**,回带错误说明并保留已填项。
201
- - 随时发 `/cancel` 放弃未提交的表单。
202
-
203
- ### 目录容错规则
204
-
205
- | 输入 | 行为 |
206
- |---|---|
207
- | 留空 | 使用**允许根目录** `allowedRoots[0]`(默认用户家目录),不视为错误 |
208
- | 不存在的绝对路径 | 自动 `mkdir -p` 创建,但**必须仍在 `allowedRoots` 之下** |
209
- | 越界 / 系统目录 / `/` | 拒绝,不创建 |
210
- | 符号链接 | 创建后以 `realpath` 复核,逃逸出 `allowedRoots`或落系统目录 → 拒绝 |
152
+ ![/new 建会话表单卡:目录、模型、权限一次填完](image/new.png)
211
153
 
212
- `/cd` 遵循**完全相同**的规则。
213
-
214
- **表单里目录的取值优先级**(下拉与输入框并存时):下拉选中(非「✍️ 手动输入路径」)> 文本输入框 > 两者皆空则用 `allowedRoots[0]`。
215
- 下拉默认停在「✍️ 手动输入路径」,保证手填优先、不会误选一个意料外的目录;用 `/dir <路径>` 预填时写入输入框,若该路径恰是下拉中的某个选项则同步选中,否则回退到「手动输入路径」(任意路径仍可手填)。
216
-
217
- ### 表单一次填完(`/form`)
218
-
219
- - 发 `/form`(或 `/new`,二者等价)直接打开表单卡。
220
- - 表单里一次填好:**工作目录**(输入框手填,或从下拉选允许根目录的一级子目录;可留空)、**模型**(下拉,最近使用 + 常用若干,默认选中当前/最近模型)、**权限档位**(下拉,四档带说明),点「✅ 创建会话」提交。
221
- - 目录下拉的选项:`✍️ 手动输入路径(用上面的输入框)` + `🏠 <根目录>(就用这个根目录)` + 该根目录的**一级子目录**(过滤隐藏目录与 `node_modules`,按名称排序,最多 15 个;含 `.git` 的子目录前缀 `📦 `)。
222
- - 下拉**只用 `allowedRoots[0]`**(第一个允许根目录);扫描失败(不存在 / 无权限)静默降级为仅「手动输入 + 根目录」两项,不影响表单与插件;扫描在渲染表单卡时进行(低频、不缓存)。`/dir` 仍可手填任意(在允许范围内的)路径。
223
- - 提交后:`session.create` → 对**表单卡消息** `reply_in_thread` 引发会话就绪卡(表单消息即话题根)→ 绑定,随即在新话题里干活。
224
- - `/new <标题>` 的标题会写入向导状态,提交后作为会话标题。
225
- - **零新增权限**:表单提交复用的就是 `card.action.trigger` 回调(官方权限要求为 None),**不需要**新开 scope、也不需要重发应用版本。
226
- - 目录非法时**不会建会话**:会回一张带错误说明的表单卡,并保留你已填的目录/模型/权限,改完再提交即可。
154
+ - 目录可直接输入,也可从下拉选择允许根目录的一级子目录;**留空 = 允许根目录**,不存在会自动创建。
155
+ - `/dir` `/model` `/perm` 只作为表单预填能力,不再是必经步骤。
156
+ - 目录非法时**不建会话**,回带错误说明并保留已填项;`/cancel` 放弃表单。
227
157
 
228
158
  ### 续聊历史会话(`/sessions` + `/resume`)
229
159
 
230
- 除了从飞书新建的会话,还能**加载任意一个 opencode 本机可见的历史会话**接着聊。
231
-
232
- **`/sessions`(别名 `/ls`)— 全部会话列表**
233
-
234
- ```
235
- /sessions
236
- ↓
237
- 🧩 OpenCode 会话(全部)
238
- 1. 修一下登录 bug(`ses_ab12cd34…`)· 3 小时前 · 💬 已绑话题 · 📍 my-app
239
- 2. 重构 API(`ses_ef56gh78…`)· 2 天前 · 📍 api-server
240
- …
241
- [▶️ 进入话题] [▶️ 再开话题] [⬅️ 上一页] [➡️ 下一页] [➕ 新建会话]
242
- ```
243
-
244
- - 数据源是 `ctx.session.list()`(opencode **全部**会话,按 `time.updated` 倒序),不再只列插件映射表里的会话;拿不到时回退映射表列表并记 `log.warn`。
245
- - 每条显示:标题(截断)、短 id、相对时间、`💬 已绑话题`(该会话已有话题映射)、`📍 <目录尾段>`。
246
- - **分页**:默认每页 8 条(`sessionPageSize`,夹取 5–20),底部按钮翻页(`{cmd:"list", page:N}`)。
247
- - **「➕ 新建会话」** = 打开发建会话表单卡(与 `/new` `/form` 等价),不再直接建会话。
160
+ `/sessions` 列出 opencode **本机全部**会话(按更新时间倒序,分页 8 条可配):
248
161
 
249
- **「▶️ 进入话题」— 把历史会话接到话题里**
162
+ ![/sessions 会话列表卡:翻页、进入/再开、新建会话](image/sessions.png)
250
163
 
251
- - 点按钮(值 `{cmd:"open", s, c}`):先校验会话存在(`ctx.session.get`),不存在 → toast「会话不存在」并把列表卡改写成提示卡。
252
- - 存在 → 对**你点的这条列表卡消息** `reply_in_thread` 发一张「✅ 已进入会话」卡,拿到 `thread_id` 后绑定话题 ↔ 会话(同时记录话题根),之后**在该话题里发消息就续上这个历史会话**(opencode 会话上下文天然持久,等同于 resume)。
253
- - 已绑话题的会话按钮文案变成「▶️ 再开话题」,**同一会话可被多个话题路由**(每个话题各自会话上下文;回复落在触发话题内)。
164
+ - 每条显示标题 / 短 id / 相对时间 / 是否已绑话题 / 目录,当前会话标「← 当前」;已绑话题的按钮显示「▶️ 再开」,其余为「▶️ 进入」;底部可翻页 + 「➕ 新建会话」。
165
+ - **「▶️ 进入话题」**:在主聊天流发一张恢复卡(含会话摘要),**直接回复这张卡**即续聊该历史会话。
166
+ - `/resume [序号]` 跳过列表直达,同一套「发恢复卡 → 回复即续聊」流程。
167
+ - 摘要走「复用原生 compaction 摘要 → 缺失才快摘要」,另带「🗜 压缩并总结」按钮(显式触发,不隐式修改会话历史);快摘要请求**必须携带 `x-opencode-session` 头**(否则 opencode-go 端拒绝),实现为**优先 `ctx.generate.text(input, { headers })`、失败回退本机 HTTP `POST /api/experimental/generate`**,绝不整会话喂模型。
254
168
 
255
- **`/resume [序号]` — 跳过列表直接进入**
169
+ ### 话题根卡工作状态
256
170
 
257
- - `/resume` = 对**最近更新**的会话走同一「进入话题」流程;`/resume 3` = 列表第 3 个。序号越界会提示有效范围。
258
- - 与 `/sessions` 用同一份排序(`time.updated` 倒序)。
171
+ 根卡会实时反映会话状态,在话题列表里一眼看出哪些会话需要你:
259
172
 
260
- **限制**
173
+ | 档位 | header 颜色 | 正文页脚 |
174
+ |---|---|---|
175
+ | 🟡 待审核 | `orange` | `🟡 待审核:<工具>` |
176
+ | 🧠 运行中 | `blue` | `🧠 运行中 · 12:03` |
177
+ | ⏳ 待回复 | `grey` | `⏳ 待回复(排队 2)` |
178
+ | 🔴 失败 | `red` | `🔴 失败` |
179
+ | ⏹ 已中断 | `grey` | `⏹ 已中断` |
180
+ | ✅ 完成 | `green` | `✅ 完成` |
261
181
 
262
- - 只能续 **opencode 本机可见**的会话;已删除 / 不属于本机 / `session.list` 看不到的会话无法进入。
263
- - `/sessions` `/resume` 属主聊天流命令,**话题内被禁用**(会提示回主聊天流);进入某个话题后无需再敲命令,直接发消息即可。
264
- - `threadRouting=false`(回退模式)下不支持进入话题 / `/resume`。
182
+ **优先级:待审核 > 运行中 > 待回复 > 失败/中断 > 完成。** 标题默认不带状态(默认 `topicStatusInTitle: false`,避免侧栏话题名频繁变动),摘要 / 元信息在刷新时不会丢失。
265
183
 
266
184
  ### 四档权限预设
267
185
 
268
186
  | 档位 | 含义 | 会话级规则 |
269
187
  |---|---|---|
270
188
  | 🔒 只读 | 只看不改,最安全 | 禁止 `edit` / `shell` |
271
- | ✏️ 可编辑 | 改文件免审批,**跑命令要问** | 允许 `edit`,`shell` 转审批 |
189
+ | ✏️ 可编辑 | 改文件免审批,跑命令要问 | 允许 `edit`,`shell` 转审批 |
272
190
  | ⚠️ 高风险审批 | 改文件 / 跑命令 / 越目录都问 | 高风险动作逐次审批 |
273
191
  | 🔓 完全信任 | 什么都不问 | 全部放行 |
274
192
 
275
- 档位写入**会话级** `permissions`,可在话题内用 `/perm` 随时改,不影响其它会话。
276
-
277
- ### 排队与插队(`/steer` `/now`)
278
-
279
- 会话正在干活时,再发的消息默认走 opencode 的**原生排队**(`delivery:"queue"`,卡片页脚显示「已排队」),当前任务跑完才轮到它。想立刻插队有两种方式:
280
-
281
- - `/steer <文本>`:把这条消息以 `delivery:"steer"` **立即插入**执行(打断当前步骤,类似 TUI 里的 steer 发送)。
282
- - `/now`:把该会话**已经排队、尚未投递**的消息全部改成 `steer`,立刻执行(走 opencode 的 `session.inbox.update`,不重新发送内容)。
193
+ 档位写入**会话级** `permissions`,话题内可用 `/perm` 随时改。
283
194
 
284
- 排队消息存放在 opencode 的 session inbox 中,`/now` 只是把它们「插队」,不会丢失或重复。
195
+ ### 审批卡
285
196
 
286
- ### 强制停止按钮与卡死自动恢复(看门狗)
197
+ 审批卡默认 4 个按钮:`✅ 允许一次` / `🔓 始终允许` / `✅ 本会话内允许该工具` / `❌ 拒绝`。
287
198
 
288
- **每张 AI 回复卡片底部都有「⏹ 强制停止」按钮**(回执卡、流式运行卡、终态卡、卡死提示卡全覆盖):
199
+ - 「始终允许」按命令前缀持久化;「**本会话内允许**」是中间粒度:只对当前会话生效(记入 `allowActions` 并追加会话级 ruleset),其它会话 / 全局配置不变。
200
+ - 换档(`/perm`)会清除本会话「本会话内允许」授权;不需要时设 `sessionAllowButton: false` 回到三按钮。
289
201
 
290
- - 运行中 / 排队中:按钮为**红色 danger**「⏹ 强制停止」,点击即中断该会话当前执行,并取消尚未投递的排队消息;
291
- - 已完成 / 失败 / 已中断:按钮为 `default` 样式「⏹ 停止」,点击只回 toast「该任务已结束」(避免误以为还能停)。
202
+ ### 排队与插队
292
203
 
293
- 按钮走**签名动作** `{ cmd:"stop", sid:<sessionID>, t:<token> }`,token 复用审批卡的 HMAC 机制,
294
- 绑定 `sessionID + 用途标签 + 过期时间 + nonce`;卡片**每次 patch 都会重签**,长任务不会因 token 过期点不动。
295
- 点击校验顺序:**白名单(allowUsers/owner)→ 验签 → 绑定 sessionID → 防重放**;伪造 / 跨会话 / 重放点击都会被拒。
204
+ 会话忙时新消息默认**原生排队**(页面页脚显示「已排队」),`/steer <文本>` 立即插队打断当前步骤,`/now` 把已排队未执行的消息全部改插队。
296
205
 
297
- **看门狗(5 分钟阈值,可配置)**:执行态超过阈值无任何事件即视为卡死,插件会**真正中断服务端会话**
298
- (`session.interrupt`)+ **取消排队中的 inbox 消息** + 收尾运行卡 + 发一张带「强制停止」按钮的提示卡。
299
- 若某会话处于排队且超过同一阈值仍无 `execution.started`,也会触发同样的恢复流程并提示——
300
- 避免历史上「一个会话卡死后,后续消息永远排队、无人处理」的问题。
206
+ ### 强制停止与看门狗
301
207
 
302
- 阈值由 `staleExecutionMs` 配置(默认 5 分钟,夹取 1–60 分钟)。
208
+ 每张回复卡底部都有「⏹ 强制停止」按钮(重签 token,防重放);已完成 / 失败时变灰并只回 toast。
209
+ **看门狗**(默认 5 分钟,`staleExecutionMs` 可配):执行态或排队超过阈值无进展即视为卡死,主动 `session.interrupt` + 取消排队 + 发提示卡,避免"一个会话卡死、后续永远排队"。
303
210
 
304
211
  ### 表单 / 提问(`question` 工具)
305
212
 
306
- agent 主动调用 `question` 工具(或其它 form 类交互)时,opencode 会创建一个 pending form 并阻塞执行。插件会把它转发成飞书卡片:
213
+ agent 调 `question` 等 form 类交互时,插件把它转成飞书卡片:单选题直接点选项,多字段逐项点选,自由文本点「✍️ 直接回复答案」后在话题里发一条消息。**没有这层转发,agent 一反问飞书会话就会永久卡住**——这也是会话卡死的常见原因。
307
214
 
308
- - 单选题直接点选项按钮;多字段表单逐项点选,填满后自动提交;
309
- - 需要自由文本的字段点「✍️ 直接回复答案」,然后在**同一话题**里发一条消息作为答案;
310
- - 提交/取消后卡片自动收敛为结果态。
215
+ ### 卡片内容守卫(表格超限降级)
311
216
 
312
- 没有这层转发时,agent 只要一反问,飞书会话就会永久卡住、后续消息全部排队——这也是「会话卡死」的常见原因之一。
217
+ 飞书**单卡最多 5 个表格组件**,超限时 patch 直接返回 400(`code=230099`)——回复里出现大量 markdown 对照表时,卡片会永远停在旧内容、看起来像卡死。插件对**整张卡片**做守卫:
218
+
219
+ - 表格数**按整卡累计**(`cardMaxTables`,默认 `4`,夹取 1–5),超出的表格**降级为围栏代码块**:内容一字不丢,只是不再按表格渲染。
220
+ - 围栏代码块内的 `|` 不会被误判(先逐行计算围栏遮罩),降级幂等;单卡组件数收敛到 ≤200(超限时丢最旧元素)。
221
+ - 覆盖运行卡文本块、话题根卡 / 恢复卡 / 摘要,以及发送层兜底(`sendCard` / `replyCard` / `patchCard`);降级记 `warn` 便于观测。
313
222
 
314
223
  ---
315
224
 
@@ -319,53 +228,51 @@ agent 主动调用 `question` 工具(或其它 form 类交互)时,opencode
319
228
 
320
229
  | 字段 | 类型 | 默认 | 说明 |
321
230
  |---|---|---|---|
322
- | `appId` | string | — | 飞书 App ID(**必填**,缺失则禁用插件,不抛异常) |
231
+ | `appId` | string | — | 飞书 App ID(**必填**,缺失则禁用插件) |
323
232
  | `appSecret` | string | — | 飞书 App Secret(**必填**,永不写入日志) |
324
233
  | `domain` | `feishu`\|`lark` | `feishu` | 飞书 / Lark 国际版 |
325
- | `allowUsers` | string[] | `[]` | open_id 白名单。**空 = 仅应用 owner**(首个发消息者绑定并持久化) |
234
+ | `allowUsers` | string[] | `[]` | open_id 白名单。空 = 仅应用 owner |
326
235
  | `permissionGate` | `off`\|`notify`\|`gate`\|`lockdown` | `gate` | 全局审批门档位 |
327
236
  | `allowTools` | string[] | `["read","glob","grep","webfetch"]` | 免审批白名单,支持 `prefix*` |
328
237
  | `denyTools` | string[] | `[]` | 强制拒绝(优先于白名单) |
329
- | `allowedRoots` | string[] | `[用户家目录]` | 会话工作目录的允许根目录(默认目录即 `allowedRoots[0]`);`/`、文件系统根与系统目录一律拒绝;目录留空回退到第一个根、不存在会自动创建。**目录下拉只扫描第一个根的一级子目录** |
330
- | `stream` | boolean | `true` | 是否用流式卡片回填回复 |
331
- | `streamThrottleMs` | number | `400` | 卡片更新最小间隔(下限 400ms,飞书限 5 QPS) |
332
- | `threadRouting` | boolean | `true` | 话题路由总开关;`false` 时主聊天流普通文本进当前会话(回退用) |
333
- | `topicGuidance` | boolean | `true` | 主题软引导:对飞书会话注入一句「离题可 `/new` 开新会话」的 system 说明(不拦截消息);非飞书会话绝不注入 |
334
- | `recentDirsLimit` | number | `5` | 「最近使用目录」条数(1–20) |
335
- | `recentModelsLimit` | number | `5` | 「最近使用模型」条数(1–20) |
336
- | `logLevel` | `debug`\|`info`\|`warn`\|`error` | `info` | 日志级别(只记 secret 存在性,绝不含明文) |
337
- | `logFile` | string \| boolean | — | `true` = 写 `<configDir>/plugins/feishu.log`;或指定路径。**服务模式下插件 stderr 会被丢弃,排查问题请开它** |
338
- | `gatewayLocation` | string | — | 只在该 location 启动网关。OpenCode 会按 location 多次加载全局插件(独立 VM context,无法用进程内单例收敛);**强烈建议设为你常用的工作目录**,否则会出现多个长连接 |
238
+ | `allowedRoots` | string[] | `[用户家目录]` | 会话工作目录允许根目录;越界 / 系统目录拒绝;留空回退第一个根,不存在自动创建 |
239
+ | `stream` | boolean | `true` | 是否流式回填回复 |
240
+ | `streamThrottleMs` | number | `400` | 卡片更新最小间隔(下限 400ms) |
241
+ | `threadRouting` | boolean | `true` | 话题路由总开关;`false` 时主聊天流普通文本进当前会话 |
242
+ | `topicGuidance` | boolean | `true` | 对飞书会话注入一句轻量 system 说明(离题可 `/new`),不拦截;本地会话绝不注入 |
243
+ | `recentDirsLimit` / `recentModelsLimit` | number | `5` | 表单「最近使用」条数(1–20) |
244
+ | `logLevel` | `debug`\|`info`\|`warn`\|`error` | `info` | 日志级别 |
245
+ | `logFile` | string \| boolean | — | `true` = 写 `<configDir>/plugins/feishu.log`;服务模式建议开启 |
246
+ | `gatewayLocation` | string | — | 只在该 location 启动网关(OpenCode 按 location 多次加载全局插件,建议设为常用工作目录) |
339
247
  | `approvalTtlMs` | number | `600000` | 审批 token / 卡片有效期 |
340
- | `staleExecutionMs` | number | `300000` | 看门狗阈值:执行态超过此时长无事件即视为卡死,主动中断并收尾;排队超过此时长仍无 `execution.started` 也提示。夹取 1–60 分钟 |
248
+ | `staleExecutionMs` | number | `300000` | 看门狗阈值(夹取 1–60 分钟) |
341
249
  | `maxResourcesShown` | number | `8` | 审批卡最多展示的资源行数 |
250
+ | `sessionAllowButton` | boolean | `true` | 审批卡是否显示「本会话内允许该工具」按钮 |
251
+ | `resumeSummary` | boolean | `true` | 恢复卡是否展示会话摘要 |
252
+ | `resumeSummaryTimeoutMs` | number | `15000` | 快摘要生成超时(3–60s),超时降级提示 |
253
+ | `resumeCompactTimeoutMs` | number | `120000` | 用户主动压缩后的轮询超时(30–300s) |
254
+ | `topicStatus` | boolean | `true` | 话题根卡工作状态总开关 |
255
+ | `topicStatusInTitle` | boolean | `false` | 是否在根卡标题加状态 emoji 前缀 |
256
+ | `topicStatusThrottleMs` | number | `1000` | 根卡状态刷新最小间隔(500–10000) |
257
+ | `cardMaxTables` | number | `4` | 单卡最多保留的 markdown 表格数(1–5);超出按整卡累计降级为围栏代码块,避免飞书 400 `code=230099` |
342
258
 
343
259
  ---
344
260
 
345
261
  ## 五、安全设计
346
262
 
347
263
  ```
348
- permission.evaluate (插件 hook) permission.asked (事件流)
349
- ────────────────────────── ──────────────────────
350
- 白名单工具 → allow 事件带 {id, sessionID, action, resources, save}
351
- 拒绝名单 → deny │
352
- 其余(按会话预设)→ ask ─────────────────────────────────────┘
353
- ▼
354
- 发飞书审批卡(按钮 value = 自签 token)
355
- │ 用户点击
356
- ▼
357
- card.action.trigger(长连接到达,3 秒内回 toast)
358
- │
359
- 校验:点击人在白名单 → 验签 → 绑定字段 → 防重放
360
- ▼
361
- ctx.permission.reply({sessionID, requestID, reply})
264
+ permission.evaluate (插件 hook) permission.asked (事件流)
265
+ 白名单 → allow 拒绝名单 → deny 发飞书审批卡(自签 token)
266
+ 会话内已放行 → allow 用户点击 → card.action.trigger(长连接)
267
+ 其余(按会话预设)→ ask ────────────────────► 校验:白名单 → 验签 → 绑定字段 → 防重放
268
+ → ctx.permission.reply(...)
362
269
  ```
363
270
 
364
271
  - **自签 token**:HMAC-SHA256,绑定 `requestID + sessionID + 点击人 openId + 过期时间 + nonce`;伪造 / 转发 / 重放都会被拒。
365
- - **强停按钮同源签名**:运行卡「强制停止」token 绑定 `sessionID + 用途标签 + 过期时间 + nonce`,点击先过 open_id 白名单再验签,且与审批 token 用途隔离(互不通用)。
366
- - **只对飞书来源的会话生效**:没有 chat↔session 映射的会话(例如你本地 TUI)**不会被降级为 ask**,否则会因为没有审批出口而永久挂起。
367
- - **三重单人边界**:平台可用范围「仅本人」+ 不申请群权限 + 代码层 open_id 白名单静默忽略。
368
- - **`always` 语义**:仅当请求带 `save[]` 时才持久化,否则等价于「允许一次」(卡片会提示)。
272
+ - **强停 / 会话内放行按钮同源签名**:各自绑定专属字段 + 用途标签隔离,权责互不通用。
273
+ - **只对飞书来源的会话生效**:本地 TUI 等无映射会话不会被降级为 ask(否则会因没有审批出口而永久挂起)。
274
+ - **三重单人边界**:可用范围「仅本人」+ 不申请群权限 + 代码层 open_id 白名单。
275
+ - **`always` 语义**:仅当请求带 `save[]` 时才持久化,否则等价于「允许一次」。
369
276
 
370
277
  ---
371
278
 
@@ -373,21 +280,18 @@ permission.evaluate (插件 hook) permission.asked (事件流)
373
280
 
374
281
  | 现象 | 处理 |
375
282
  |---|---|
376
- | 发消息没反应 | ① 应用是否**已发布**、可用范围是否勾了你;② 事件/回调订阅是否选了**长连接**(不是 Webhook);③ 是否开通 `im:message.p2p_msg:readonly` |
377
- | 改了 `feishu.json` 不生效 | 确认路径是 `<configDir>/plugins/feishu.json`,然后 `opencode reload` |
378
- | 放了插件但完全没被加载(无日志、无报错) | npm 方式:确认包名已写进配置 `plugins` 数组(`opencode plugin list` 可见)。目录方式:确认 `plugins/<名>/index.js` 存在(OpenCode 不读 `package.json#main`) |
379
- | 改了插件代码不生效 | `opencode reload` **只重跑 `setup`,不会重新 import 同路径模块**。升级插件用 `opencode plugin update opencode-feishu-plugin`,或重启服务(`opencode service restart`) |
380
- | 出现多个长连接 / 重复回复 | 设置 `gatewayLocation` 为你常用的工作目录(OpenCode 按 location 多次加载全局插件) |
381
- | 审批卡收不到 | 该会话不是从飞书发起的(无映射);插件按安全设计不接管 |
283
+ | 发消息没反应 | ① 应用是否已发布、可用范围是否勾了你;② 订阅是否选了**长连接**(不是 Webhook);③ 是否开通 `im:message.p2p_msg:readonly` |
284
+ | 改了 `feishu.json` 不生效 | 确认路径,然后 `opencode reload` |
285
+ | 插件完全没被加载 | npm 方式确认包名在 `plugins` 数组;目录方式确认 `plugins/<名>/index.js` 存在 |
286
+ | 改了插件代码不生效 | `opencode reload` 不会重新 import 同路径模块;用 `opencode plugin update` 或重启服务 |
287
+ | 出现多个长连接 / 重复回复 | 设置 `gatewayLocation` 为常用工作目录 |
288
+ | 审批卡收不到 | 该会话不是从飞书发起的(无映射),插件按设计不接管 |
382
289
  | 点按钮提示凭证无效 | token 过期(默认 10 分钟)或点击者不在白名单 |
383
- | 卡片内容被截断 | 飞书卡片上限约 30KB,插件截断并标注;超长会话会丢弃卡片上最旧的块(完整内容仍在会话里) |
384
- | 表单卡提交没反应 / 报错 | 客户端过旧(`select_static` 需 ≥ V3.7.0);或该卡已失效(表单被消费/取消)——重新发 `/form` 或 `/new` 打开新表单 |
385
- | 表单提交后没建会话 | 目录越出白名单 / 命中系统目录时会回错误卡且**不建会话**,按提示改目录再提交;留空或不存在的允许范围内目录会自动创建,不会因此失败 |
386
- | 建会话后没看到话题 | 自动开话题失败时表单卡会改写为「✅ 已创建 · …」并附手动创建话题指引;可在 `/sessions` 会话卡上手动「创建话题」 |
387
- | 看不到插件日志 | 服务模式下插件 stderr 会被丢弃;设 `logFile: true`,然后看 `<configDir>/plugins/feishu.log` |
388
- | 主聊天流发消息只回提示卡 | 预期行为:主聊天流只做管理。用 `/new` 进话题;想恢复旧行为设 `threadRouting: false` |
389
- | 会话像卡死、发消息只排队 | 看门狗会在 `staleExecutionMs`(默认 5 分钟)后自动中断该会话并取消排队,同时发提示卡;也可直接点卡片上的「⏹ 强制停止」或发 `/stop` |
390
- | 切了 `/model` 但历史消息还是旧模型 | 预期行为:切换只影响**后续**回复,历史消息保留各自当时的模型;回执 / 运行卡页脚 / `/current` 均以读回的真实值为准 |
290
+ | 卡片内容被截断 | 飞书卡片上限约 30KB,超长会话丢弃卡片上最旧块(完整内容仍在会话里) |
291
+ | 建会话后没看到话题 | 表单卡会改写为「✅ 已创建 · …」并附手动创建话题指引 |
292
+ | 会话像卡死、发消息只排队 | 看门狗默认 5 分钟后自动中断;也可点「⏹ 强制停止」或发 `/stop` |
293
+ | 看不到插件日志 | 服务模式下 stderr 被丢弃,设 `logFile: true` |
294
+ | 切了 `/model` 但历史还是旧模型 | 预期行为:切换只影响后续回复,历史消息保留各自当时的模型 |
391
295
 
392
296
  ---
393
297
 
@@ -401,37 +305,27 @@ npm test # vitest(纯逻辑单测,不连真飞书)
401
305
  npm run dev # tsup --watch
402
306
  ```
403
307
 
404
- **架构**:`src/index.ts` 只做装配(配置、gateway、watchdog、hook 注册与 cleanup);`src/runtime/` 放可单测的事件分发(`event-router.ts`)与卡片回调分流(`card-action-router.ts`);会话命令编排拆在 `src/session/`(`session-commands.ts` 为薄门面,实现分在 `session-list.ts` / `setup-wizard.ts` / `session-ops.ts` / `model-perm.ts` / `context.ts`);飞书交互层在 `src/feishu/`(事件解析、卡片构建、话题路由、向导状态机、流式卡片 reducer 等,**以纯函数为主便于单测**);安全层在 `src/security/`(token 签名、白名单)。
308
+ **架构**:`src/index.ts` 只做装配(配置、gateway、watchdog、hook 注册与 cleanup);`src/runtime/` 放可单测的事件分发(`event-router.ts`)、卡片回调分流(`card-action-router.ts`)与话题根卡状态接线(`topic-status.ts`);会话命令编排拆在 `src/session/`(`session-commands.ts` 为薄门面,实现分在 `session-list.ts` / `setup-wizard.ts` / `session-ops.ts` / `model-perm.ts` / `context.ts`);飞书交互层在 `src/feishu/`(以纯函数为主便于单测);安全层在 `src/security/`。
405
309
 
406
- **设计要点**:
407
- - 卡片一律 **JSON 2.0**(按钮直接放 `body.elements`,回调用 `behaviors`;1.0 的 `tag:"action"` 在 2.0 会 400)。表单卡额外约束:`form` 必须在 `body.elements` 根节点、交互组件 `name` 全局唯一、至少一个 `form_action_type:"submit"` 按钮。
408
- - 卡片更新统一节流 ≥400ms;连续工具调用 ≥3 个自动折叠(只留名称行)以防 30KB 超限。
409
- - 运行卡状态用一个**纯 reducer** 维护(文本块 / 工具块 / 页脚 / 终态),事件按 `assistantMessageID` 分步。
310
+ **设计要点**:卡片一律 **JSON 2.0**(按钮放 `body.elements`,回调用 `behaviors`);更新统一节流 ≥400ms;连续工具调用 ≥3 个自动折叠;运行卡状态用**纯 reducer** 维护。
410
311
 
411
312
  ---
412
313
 
413
314
  ## 八、与其它项目的区别
414
315
 
415
316
  - 本插件**只支持 OpenCode V2**(`@opencode/plugin`,`Plugin.define` 形态)。
416
- - 生态里另有 `opencode-feishu`(V1 插件,`@opencode-ai/plugin`),两者**不兼容**,也不共用代码,请按你的 OpenCode 版本选择。
317
+ - 生态里另有 `opencode-feishu`(V1 插件,`@opencode-ai/plugin`),两者**不兼容**、不共用代码,请按你的 OpenCode 版本选择。
417
318
 
418
319
  ## 已知限制
419
320
 
420
321
  - 只处理**单聊文本**(含富文本);图片 / 文件 / 音视频只给文字占位,不下载。
421
- - 只接管**从飞书发起的会话**的审批;本地 TUI 会话不受影响(安全设计)。
422
- - 消息去重为 `get-then-set`,非原子:极端并发下理论上可能双处理(正常情况下单实例顺序处理)。
322
+ - 只接管**从飞书发起的会话**的审批;本地 TUI 会话不受影响。
323
+ - 消息去重为 `get-then-set`,非原子:极端并发下理论上可能双处理。
423
324
  - 话题被删除后映射不主动清理(惰性忽略)。
424
- - 建会话只有一条主路径:**`/new` 与 `/form` 等价的表单卡**;`/dir` `/model` `/perm` 仅用于给表单预填字段。旧的目录/模型/权限/确认分步卡已从 `/new` 下线(目录卡的构建函数已彻底移除;模型/权限/确认卡的构建函数与兼容回调保留,标注 deprecated,供仍持有旧卡片的用户点击时继续可用)。
425
- - 表单为 JSON 2.0(`form` 置于 `body.elements` 根节点,交互组件 `name` 全局唯一,提交按钮带 `form_action_type:"submit"`);部分老客户端对 `select_static` 有最低版本要求(≥ V3.7.0)。
426
-
427
- - **话题首条消息可能不带 `thread_id`**:飞书有时在事件里省略 `thread_id`(随后才归属到话题)。若此时你敲了 `/new` 这类仅限主聊天流的命令,它会被当作主聊天流命令执行(例如表单卡发到主聊天流)。遇到这种情况,直接进话题重新发普通消息即可。
428
-
429
-
430
- > 本地发布小贴士:`npm publish` 会触发 `prepublishOnly`(typecheck + build + test)。若 `node_modules` 不存在会**自动先跑 `npm ci`**,所以新克隆的仓库可以直接 `npm publish`,无需手动安装依赖。
431
-
432
-
433
- > 本地发布注意:provenance 只能在 CI(GitHub Actions)里生成,因此 `package.json` 里**没有**设 `publishConfig.provenance`;CI 工作流用 `npm publish --provenance` 显式开启。本地发布直接 `npm publish --access public` 即可。
325
+ - 建会话只有一条主路径:`/new` 与 `/form` 等价的表单卡;`/dir` `/model` `/perm` 仅用于预填。
326
+ - 表单为 JSON 2.0,部分老客户端对 `select_static` 有最低版本要求(≥ V3.7.0)。
327
+ - 话题首条消息可能不带 `thread_id`:回复带 root 映射的卡片时插件会靠 `root_id` 兜底路由;新话题缺 `thread_id` 时,敲 `/new` 等命令会落到主聊天流,直接进话题发消息即可。
434
328
 
435
329
  ## 许可证
436
330
 
437
- MIT
331
+ MIT