opencode-feishu-plugin 0.1.1 → 0.1.3
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.
- package/README.en.md +74 -5
- package/README.md +135 -241
- package/dist/index.js +1980 -481
- 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
|
|
5
|
+
把 [OpenCode](https://opencode.ai) 接进飞书:**一个飞书话题 = 一个 OpenCode 会话**。你在飞书里用话题管理多个会话、指挥 AI 写代码,**权限审批直接在飞书卡片上点按钮**。
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
- **只用 OpenCode V2 插件 API**(`Plugin.define({ id, setup(ctx) })`),不依赖任何 V1 包。
|
|
8
|
+
- **纯长连接**(WebSocket)收发事件与卡片回调,不监听端口、不需要公网地址。
|
|
9
9
|
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
## 效果预览
|
|
13
|
+
|
|
14
|
+
在飞书话题里指挥 AI 干活,权限审批、状态跟踪都在卡片上完成:
|
|
15
|
+
|
|
16
|
+

|
|
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
|
-
|
|
|
21
|
-
| ✅ **卡片审批** | 权限请求变成飞书卡片(允许一次 / 始终允许 / 拒绝),点击即批准,带签名防伪防重放 |
|
|
25
|
+
| 📝 **表单一次填完** | `/new` 与 `/form` 完全等价:目录 + 模型 + 权限一张表单一次提交即建会话,**零新增权限** |
|
|
26
|
+
| ✅ **卡片审批** | 权限请求变成飞书卡片(允许一次 / 始终允许 / 拒绝 / 本会话内允许),点击即批准,带签名防伪防重放 |
|
|
22
27
|
| 🪜 **权限预设** | 只读 / 可编辑 / 高风险审批 / 完全信任,四档一次选定,告别逐次审批 |
|
|
23
28
|
| 📊 **实时可见** | 先回执「思考中」,工具调用实时上卡(≥3 个自动折叠),文本流式更新,页脚显示当前模型 |
|
|
24
|
-
| 🧵 **原生排队** | 会话忙时自动排队(
|
|
25
|
-
| ⏹ **一键强停** |
|
|
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. **事件与回调 →
|
|
38
|
-
5. **事件与回调 →
|
|
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
|
|
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
|
-
|
|
66
|
+
插件入口指向**自包含**的 `dist/index.js`(含飞书 SDK 等依赖),运行时无需手动 `npm install`。
|
|
67
|
+
本地开发则克隆后 `npm install && npm run build`,把本地目录写进 `plugins`:`{ "plugins": ["./path/to/opencode-feishu-plugin"] }`。
|
|
77
68
|
|
|
78
|
-
|
|
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
|
-
|
|
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
|
-
|
|
91
|
+
也可直接明文写进 `feishu.json`(权限记得 `600`)。**优先级**:`options` > `feishu.json` > 环境变量。
|
|
115
92
|
|
|
116
|
-
###
|
|
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 [标题]` |
|
|
117
|
+
| `/new [标题]` | 发建会话表单卡,提交即建会话并自动开话题(与 `/form` 等价) |
|
|
136
118
|
| `/form [标题]` | 同上,`/new` 的等价入口 |
|
|
137
|
-
| `/sessions`(`/ls`) |
|
|
138
|
-
| `/
|
|
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
|
-
一个话题 =
|
|
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
|
|
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
|
-
|
|
199
|
-
- `/dir` `/model` `/perm` 仍可用,但只作为**给表单预填字段**的能力(不再是必经步骤):执行后会自动回一张预填好的新表单卡。
|
|
200
|
-
- 表单提交后先消费向导状态,防连点重复建会话;目录非法时**不建会话**,回带错误说明并保留已填项。
|
|
201
|
-
- 随时发 `/cancel` 放弃未提交的表单。
|
|
202
|
-
|
|
203
|
-
### 目录容错规则
|
|
204
|
-
|
|
205
|
-
| 输入 | 行为 |
|
|
206
|
-
|---|---|
|
|
207
|
-
| 留空 | 使用**允许根目录** `allowedRoots[0]`(默认用户家目录),不视为错误 |
|
|
208
|
-
| 不存在的绝对路径 | 自动 `mkdir -p` 创建,但**必须仍在 `allowedRoots` 之下** |
|
|
209
|
-
| 越界 / 系统目录 / `/` | 拒绝,不创建 |
|
|
210
|
-
| 符号链接 | 创建后以 `realpath` 复核,逃逸出 `allowedRoots`或落系统目录 → 拒绝 |
|
|
152
|
+

|
|
211
153
|
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
+

|
|
250
163
|
|
|
251
|
-
-
|
|
252
|
-
-
|
|
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
|
-
|
|
169
|
+
### 话题根卡工作状态
|
|
256
170
|
|
|
257
|
-
|
|
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
|
-
|
|
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
|
-
| ✏️ 可编辑 |
|
|
189
|
+
| ✏️ 可编辑 | 改文件免审批,跑命令要问 | 允许 `edit`,`shell` 转审批 |
|
|
272
190
|
| ⚠️ 高风险审批 | 改文件 / 跑命令 / 越目录都问 | 高风险动作逐次审批 |
|
|
273
191
|
| 🔓 完全信任 | 什么都不问 | 全部放行 |
|
|
274
192
|
|
|
275
|
-
档位写入**会话级** `permissions
|
|
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
|
-
|
|
195
|
+
### 审批卡
|
|
285
196
|
|
|
286
|
-
|
|
197
|
+
审批卡默认 4 个按钮:`✅ 允许一次` / `🔓 始终允许` / `✅ 本会话内允许该工具` / `❌ 拒绝`。
|
|
287
198
|
|
|
288
|
-
|
|
199
|
+
- 「始终允许」按命令前缀持久化;「**本会话内允许**」是中间粒度:只对当前会话生效(记入 `allowActions` 并追加会话级 ruleset),其它会话 / 全局配置不变。
|
|
200
|
+
- 换档(`/perm`)会清除本会话「本会话内允许」授权;不需要时设 `sessionAllowButton: false` 回到三按钮。
|
|
289
201
|
|
|
290
|
-
|
|
291
|
-
- 已完成 / 失败 / 已中断:按钮为 `default` 样式「⏹ 停止」,点击只回 toast「该任务已结束」(避免误以为还能停)。
|
|
202
|
+
### 排队与插队
|
|
292
203
|
|
|
293
|
-
|
|
294
|
-
绑定 `sessionID + 用途标签 + 过期时间 + nonce`;卡片**每次 patch 都会重签**,长任务不会因 token 过期点不动。
|
|
295
|
-
点击校验顺序:**白名单(allowUsers/owner)→ 验签 → 绑定 sessionID → 防重放**;伪造 / 跨会话 / 重放点击都会被拒。
|
|
204
|
+
会话忙时新消息默认**原生排队**(页面页脚显示「已排队」),`/steer <文本>` 立即插队打断当前步骤,`/now` 把已排队未执行的消息全部改插队。
|
|
296
205
|
|
|
297
|
-
|
|
298
|
-
(`session.interrupt`)+ **取消排队中的 inbox 消息** + 收尾运行卡 + 发一张带「强制停止」按钮的提示卡。
|
|
299
|
-
若某会话处于排队且超过同一阈值仍无 `execution.started`,也会触发同样的恢复流程并提示——
|
|
300
|
-
避免历史上「一个会话卡死后,后续消息永远排队、无人处理」的问题。
|
|
206
|
+
### 强制停止与看门狗
|
|
301
207
|
|
|
302
|
-
|
|
208
|
+
每张回复卡底部都有「⏹ 强制停止」按钮(重签 token,防重放);已完成 / 失败时变灰并只回 toast。
|
|
209
|
+
**看门狗**(默认 5 分钟,`staleExecutionMs` 可配):执行态或排队超过阈值无进展即视为卡死,主动 `session.interrupt` + 取消排队 + 发提示卡,避免"一个会话卡死、后续永远排队"。
|
|
303
210
|
|
|
304
211
|
### 表单 / 提问(`question` 工具)
|
|
305
212
|
|
|
306
|
-
agent
|
|
213
|
+
agent 调 `question` 等 form 类交互时,插件把它转成飞书卡片:单选题直接点选项,多字段逐项点选,自由文本点「✍️ 直接回复答案」后在话题里发一条消息。**没有这层转发,agent 一反问飞书会话就会永久卡住**——这也是会话卡死的常见原因。
|
|
307
214
|
|
|
308
|
-
|
|
309
|
-
- 需要自由文本的字段点「✍️ 直接回复答案」,然后在**同一话题**里发一条消息作为答案;
|
|
310
|
-
- 提交/取消后卡片自动收敛为结果态。
|
|
215
|
+
### 卡片内容守卫(表格超限降级)
|
|
311
216
|
|
|
312
|
-
|
|
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
|
|
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[] | `[用户家目录]` |
|
|
330
|
-
| `stream` | boolean | `true` |
|
|
331
|
-
| `streamThrottleMs` | number | `400` | 卡片更新最小间隔(下限 400ms
|
|
332
|
-
| `threadRouting` | boolean | `true` | 话题路由总开关;`false`
|
|
333
|
-
| `topicGuidance` | boolean | `true` |
|
|
334
|
-
| `recentDirsLimit` | number | `5` |
|
|
335
|
-
| `
|
|
336
|
-
| `
|
|
337
|
-
| `
|
|
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` |
|
|
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)
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
-
|
|
366
|
-
-
|
|
367
|
-
-
|
|
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
|
-
| 发消息没反应 | ①
|
|
377
|
-
| 改了 `feishu.json` 不生效 |
|
|
378
|
-
|
|
|
379
|
-
| 改了插件代码不生效 | `opencode reload`
|
|
380
|
-
| 出现多个长连接 / 重复回复 | 设置 `gatewayLocation`
|
|
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
|
-
|
|
|
385
|
-
|
|
|
386
|
-
|
|
|
387
|
-
|
|
|
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
|
|
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
|
|
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
|
-
-
|
|
425
|
-
- 表单为 JSON 2.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
|