opencode-feishu-plugin 0.2.7 → 0.2.9
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 +114 -457
- package/README.md +82 -268
- package/dist/index.js +296 -41
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,56 +2,48 @@
|
|
|
2
2
|
|
|
3
3
|
[English](./README.en.md) | **简体中文**
|
|
4
4
|
|
|
5
|
-
把 [OpenCode](https://opencode.ai) 接进飞书:**一个飞书话题 = 一个 OpenCode
|
|
5
|
+
把 [OpenCode](https://opencode.ai) 接进飞书:**一个飞书话题 = 一个 OpenCode 会话**,权限审批直接在飞书卡片上点按钮。
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
- **纯长连接**(WebSocket
|
|
9
|
-
|
|
10
|
-
---
|
|
7
|
+
- 只支持 OpenCode **V2**(`@opencode/plugin`,`Plugin.define`),不依赖任何 V1 包。
|
|
8
|
+
- **纯长连接**(WebSocket)收发事件与卡片回调:不监听端口、不需要公网地址。
|
|
11
9
|
|
|
12
10
|
## 效果预览
|
|
13
11
|
|
|
14
|
-
在飞书话题里指挥 AI 干活,权限审批、状态跟踪都在卡片上完成:
|
|
15
|
-
|
|
16
12
|

|
|
17
13
|
|
|
18
14
|
## 亮点
|
|
19
15
|
|
|
20
16
|
| | 说明 |
|
|
21
17
|
|---|---|
|
|
22
|
-
| 🔐 **最小权限** | 只要 2 个 scope
|
|
23
|
-
| 💬 **话题 = 会话** |
|
|
24
|
-
| 🚀
|
|
25
|
-
|
|
|
26
|
-
|
|
|
27
|
-
|
|
|
28
|
-
|
|
|
29
|
-
|
|
|
30
|
-
| ⏹ **一键强停** | 所有 AI 回复卡片都带「强制停止」按钮;卡死会话由看门狗自动中断 |
|
|
31
|
-
| 🚫 **无端口** | 全程长连接,服务器不用开放任何入站端口 |
|
|
32
|
-
|
|
33
|
-
---
|
|
18
|
+
| 🔐 **最小权限** | 只要 2 个 scope,不申请任何群权限,机器人物理上收不到群消息 |
|
|
19
|
+
| 💬 **话题 = 会话** | 一个飞书话题对应一个 OpenCode 会话;主聊天流只做管理,互不串台 |
|
|
20
|
+
| 🚀 **一键建会话** | `/new` 一张表单(目录 + 模型 + 权限档位)一次填完,提交即建会话并自动开话题 |
|
|
21
|
+
| ✅ **卡片审批** | 权限请求变飞书卡片:允许一次 / 始终允许 / 本会话内允许 / 拒绝,自签 token 防伪防重放 |
|
|
22
|
+
| 📊 **实时可见** | 「思考中」回执 → 工具调用实时上卡 → 文本流式更新,页脚显示当前模型 |
|
|
23
|
+
| ⏹ **可控可停** | 每张回复卡带「强制停止」;看门狗自动中断卡死会话;忙时原生排队,`/steer` `/now` 可插队 |
|
|
24
|
+
| 📎 **图片 / 文件** | 飞书里的图片 / 文件自动下载并挂进会话,支持视觉 / 文件的模型直接看图、读文件 |
|
|
25
|
+
| 🚫 **无端口** | 全程长连接,服务器无需开放任何入站端口 |
|
|
34
26
|
|
|
35
27
|
## 一、飞书后台配置(约 3 分钟)
|
|
36
28
|
|
|
37
29
|
1. 打开 [飞书开放平台](https://open.feishu.cn/app) → **创建企业自建应用**。
|
|
38
30
|
2. **添加应用能力 → 机器人**。
|
|
39
|
-
3.
|
|
31
|
+
3. **权限管理**,开通这两个最小 scope:
|
|
40
32
|
- `im:message.p2p_msg:readonly` —— 读取用户发给机器人的单聊消息
|
|
41
33
|
- `im:message:send_as_bot` —— 以应用身份发消息(也用于更新卡片)
|
|
34
|
+
|
|
35
|
+
如需**接收图片 / 文件**(下载后挂进会话),再加开一个:
|
|
36
|
+
- `im:message:readonly` —— 获取消息中的资源文件(图片 / 文件下载的必要条件)
|
|
42
37
|
4. **事件与回调 → 事件配置**:订阅方式选**「使用长连接接收事件」**(不要选 Webhook),添加事件 `im.message.receive_v1`。
|
|
43
|
-
5. **事件与回调 → 回调配置**:订阅方式同样选**长连接**,添加回调 `card.action.trigger
|
|
44
|
-
6. **版本管理与发布**:可用范围 =
|
|
38
|
+
5. **事件与回调 → 回调配置**:订阅方式同样选**长连接**,添加回调 `card.action.trigger`(零权限要求)。
|
|
39
|
+
6. **版本管理与发布**:可用范围 = **仅本人**,创建版本并**发布**。⚠️ 不发布就是开发态,长连接连不上,机器人不会有任何反应。
|
|
45
40
|
7. 记下 **App ID**(`cli_…`)与 **App Secret**。
|
|
46
41
|
|
|
47
|
-
> **为什么不申请群权限?** 本插件是"一个人的遥控台"
|
|
48
|
-
> 单人边界由平台 scope 层保证,而不是只靠代码判断。
|
|
49
|
-
|
|
50
|
-
---
|
|
42
|
+
> **为什么不申请群权限?** 本插件是"一个人的遥控台"。不申请群权限,机器人**物理上收不到群消息**,单人边界由平台 scope 层保证,而不是只靠代码判断。
|
|
51
43
|
|
|
52
44
|
## 二、安装
|
|
53
45
|
|
|
54
|
-
### 1.
|
|
46
|
+
### 1. 安装插件
|
|
55
47
|
|
|
56
48
|
```bash
|
|
57
49
|
# 方式 A:CLI(推荐)
|
|
@@ -59,54 +51,42 @@ opencode plugin add opencode-feishu-plugin
|
|
|
59
51
|
```
|
|
60
52
|
|
|
61
53
|
```jsonc
|
|
62
|
-
// 方式 B
|
|
54
|
+
// 方式 B:手写配置(追加到已有 plugins 数组,别覆盖整个文件)
|
|
63
55
|
{ "plugins": ["opencode-feishu-plugin"] }
|
|
64
56
|
```
|
|
65
57
|
|
|
66
|
-
|
|
67
|
-
本地开发则克隆后 `npm install && npm run build`,把本地目录写进 `plugins`:`{ "plugins": ["./path/to/opencode-feishu-plugin"] }`。
|
|
58
|
+
插件入口是**自包含**的 `dist/index.js`(已打包飞书 SDK),运行时无需手动 `npm install`。
|
|
68
59
|
|
|
69
60
|
### 2. 写配置
|
|
70
61
|
|
|
71
|
-
|
|
62
|
+
新建 `~/.config/opencode/plugins/feishu.json`(`configDir` = `OPENCODE_CONFIG_DIR` 或 `~/.config/opencode`):
|
|
72
63
|
|
|
73
64
|
```bash
|
|
74
65
|
install -m 600 /dev/null ~/.config/opencode/plugins/feishu.json
|
|
75
66
|
cat > ~/.config/opencode/plugins/feishu.json <<'JSON'
|
|
76
67
|
{
|
|
77
|
-
"appId": "
|
|
78
|
-
"appSecret": "
|
|
68
|
+
"appId": "cli_xxxxxxxx",
|
|
69
|
+
"appSecret": "xxxxxxxx",
|
|
70
|
+
"logFile": true
|
|
79
71
|
}
|
|
80
72
|
JSON
|
|
81
|
-
chmod 600 ~/.config/opencode/plugins/feishu.json
|
|
82
|
-
```
|
|
83
|
-
|
|
84
|
-
凭证放进 OpenCode **服务进程**的环境变量(不是交互 shell):
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
opencode service set env FEISHU_APP_ID cli_xxxxxxxx
|
|
88
|
-
opencode service set env FEISHU_APP_SECRET xxxxxxxx
|
|
89
73
|
```
|
|
90
74
|
|
|
91
|
-
|
|
75
|
+
- 凭证**优先级**:`plugins[].options` > `feishu.json` > 环境变量。也可在值里用 `{env:NAME}` / `${NAME}` 占位符从环境变量取值。
|
|
76
|
+
- `logFile: true` **建议开启**:服务模式下 stderr 会被丢弃,开着才有日志可查。
|
|
92
77
|
|
|
93
|
-
### 3.
|
|
78
|
+
### 3. 生效与验证
|
|
94
79
|
|
|
95
80
|
```bash
|
|
96
|
-
opencode reload
|
|
97
|
-
|
|
81
|
+
opencode reload
|
|
82
|
+
tail -f ~/.config/opencode/plugins/feishu.log # 应看到「飞书长连接已启动(WSClient)」
|
|
98
83
|
```
|
|
99
84
|
|
|
100
|
-
|
|
85
|
+
然后在飞书里给机器人发一条消息。**第一次发消息的人会被自动绑定为 owner**,机器人回复卡片即安装成功;之后其他人会被静默忽略。
|
|
101
86
|
|
|
102
|
-
>
|
|
103
|
-
> ```bash
|
|
104
|
-
> opencode service restart
|
|
105
|
-
> ```
|
|
87
|
+
> 升级插件或改用全局插件目录加载后,需要 `opencode service restart` 才会重新 import。
|
|
106
88
|
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
## 三、怎么用
|
|
89
|
+
## 三、快速上手
|
|
110
90
|
|
|
111
91
|
### 主聊天流(管理台)
|
|
112
92
|
|
|
@@ -114,17 +94,15 @@ opencode mcp list # 顺带确认服务健康
|
|
|
114
94
|
|
|
115
95
|
| 命令 | 作用 |
|
|
116
96
|
|---|---|
|
|
117
|
-
| `/new [标题]` |
|
|
118
|
-
| `/
|
|
119
|
-
| `/
|
|
120
|
-
| `/
|
|
121
|
-
| `/
|
|
122
|
-
| `/
|
|
123
|
-
| `/
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
| `/cancel` | 放弃未提交的表单 |
|
|
127
|
-
| `/help` | 命令列表 |
|
|
97
|
+
| `/new [标题]` | 发建会话表单,提交即建会话并自动开话题(与 `/form` 等价) |
|
|
98
|
+
| `/sessions`(`/ls`) | **全部**会话列表(含本机所有 opencode 会话),可翻页、进话题、新建 |
|
|
99
|
+
| `/resume [序号]` | 对最近(或列表第 N 个)会话发恢复卡,**回复该卡**即续聊 |
|
|
100
|
+
| `/current`、`/stop` | 查看当前会话 / 中断当前任务 |
|
|
101
|
+
| `/steer <文本>`、`/now` | 立即插队发消息 / 把排队消息改为立即执行 |
|
|
102
|
+
| `/dir`、`/model`、`/perm` | 为建会话表单**预填**工作目录 / 模型 / 权限档位 |
|
|
103
|
+
| `/cancel`、`/help` | 放弃未提交的表单 / 命令列表 |
|
|
104
|
+
|
|
105
|
+
 
|
|
128
106
|
|
|
129
107
|
### 话题内(干活)
|
|
130
108
|
|
|
@@ -132,104 +110,28 @@ opencode mcp list # 顺带确认服务健康
|
|
|
132
110
|
|
|
133
111
|
| 命令 | 作用 |
|
|
134
112
|
|---|---|
|
|
135
|
-
| `/model` |
|
|
113
|
+
| `/model` | 切换本会话模型(只影响后续回复) |
|
|
136
114
|
| `/perm` | 修改本会话权限档位 |
|
|
137
115
|
| `/cd <路径>` | 迁移本会话工作目录 |
|
|
138
|
-
| `/steer
|
|
139
|
-
| `/
|
|
140
|
-
| `/current` `/stop` `/help` | 同主聊天流,作用于本话题会话 |
|
|
141
|
-
|
|
142
|
-
### 建会话(`/new` 与 `/form` 完全等价)
|
|
143
|
-
|
|
144
|
-
```
|
|
145
|
-
/new 修一下登录 bug
|
|
146
|
-
↓
|
|
147
|
-
📝 建会话表单卡(工作目录 / 模型 / 权限,一次填完)
|
|
148
|
-
↓ 点「✅ 创建会话」
|
|
149
|
-
表单消息本身成为话题根,机器人 reply_in_thread 发「会话已就绪」卡
|
|
150
|
-
```
|
|
151
|
-
|
|
152
|
-

|
|
153
|
-
|
|
154
|
-
- 目录可直接输入,也可从下拉选择允许根目录的一级子目录;**留空 = 允许根目录**,不存在会自动创建。
|
|
155
|
-
- `/dir` `/model` `/perm` 只作为表单预填能力,不再是必经步骤。
|
|
156
|
-
- 目录非法时**不建会话**,回带错误说明并保留已填项;`/cancel` 放弃表单。
|
|
157
|
-
|
|
158
|
-
### 续聊历史会话(`/sessions` + `/resume`)
|
|
159
|
-
|
|
160
|
-
`/sessions` 列出 opencode **本机全部**会话(按更新时间倒序,分页 8 条可配):
|
|
161
|
-
|
|
162
|
-

|
|
163
|
-
|
|
164
|
-
- **数据源**:优先插件原生 `ctx.session.list()`(V2 运行时通常未暴露)→ **本机 HTTP `GET /api/session`**(与 opencode 同机,列出**全量**会话,含 TUI / Web 里开的)→ `SessionMap` 回退(仅机器人自己的会话)。进入外部会话时会补一条映射,审批 / 失败通知照常。
|
|
165
|
-
- 每条显示标题 / 短 id / 相对时间 / 是否已绑话题 / 目录,当前会话标「← 当前」;已绑话题的按钮显示「▶️ 再开」,其余为「▶️ 进入」;底部可翻页 + 「➕ 新建会话」。
|
|
166
|
-
- **「▶️ 进入话题」**:在主聊天流发一张恢复卡(含会话摘要),**直接回复这张卡**即续聊该历史会话。
|
|
167
|
-
- `/resume [序号]` 跳过列表直达,同一套「发恢复卡 → 回复即续聊」流程。
|
|
168
|
-
- 摘要走「复用原生 compaction 摘要 → 缺失才快摘要」,另带「🗜 压缩并总结」按钮(显式触发,不隐式修改会话历史);快摘要请求**必须携带 `x-opencode-session` 头**(否则 opencode-go 端拒绝),实现为**优先 `ctx.generate.text(input, { headers })`、失败回退本机 HTTP `POST /api/experimental/generate`**,绝不整会话喂模型。
|
|
169
|
-
|
|
170
|
-
### 话题根卡工作状态
|
|
171
|
-
|
|
172
|
-
根卡会实时反映会话状态,在话题列表里一眼看出哪些会话需要你:
|
|
173
|
-
|
|
174
|
-
| 档位 | header 颜色 | 正文页脚 |
|
|
175
|
-
|---|---|---|
|
|
176
|
-
| 🟡 待审核 | `orange` | `🟡 待审核:<工具>` |
|
|
177
|
-
| 🧠 运行中 | `blue` | `🧠 运行中 · 12:03` |
|
|
178
|
-
| ⏳ 待回复 | `grey` | `⏳ 待回复(排队 2)` |
|
|
179
|
-
| 🔴 失败 | `red` | `🔴 失败` |
|
|
180
|
-
| ⏹ 已中断 | `grey` | `⏹ 已中断` |
|
|
181
|
-
| ✅ 完成 | `green` | `✅ 完成` |
|
|
182
|
-
|
|
183
|
-
**优先级:待审核 > 运行中 > 待回复 > 失败/中断 > 完成。** 标题默认不带状态(默认 `topicStatusInTitle: false`,避免侧栏话题名频繁变动),摘要 / 元信息在刷新时不会丢失。
|
|
184
|
-
|
|
185
|
-
### 四档权限预设
|
|
186
|
-
|
|
187
|
-
| 档位 | 含义 | 会话级规则 |
|
|
188
|
-
|---|---|---|
|
|
189
|
-
| 🔒 只读 | 只看不改,最安全 | 禁止 `edit` / `shell` |
|
|
190
|
-
| ✏️ 可编辑 | 改文件免审批,跑命令要问 | 允许 `edit`,`shell` 转审批 |
|
|
191
|
-
| ⚠️ 高风险审批 | 改文件 / 跑命令 / 越目录都问 | 高风险动作逐次审批 |
|
|
192
|
-
| 🔓 完全信任 | 什么都不问 | 全部放行 |
|
|
193
|
-
|
|
194
|
-
档位写入**会话级** `permissions`,话题内可用 `/perm` 随时改。
|
|
195
|
-
|
|
196
|
-
### 审批卡
|
|
197
|
-
|
|
198
|
-
审批卡默认 4 个按钮:`✅ 允许一次` / `🔓 始终允许` / `✅ 本会话内允许该工具` / `❌ 拒绝`。
|
|
199
|
-
|
|
200
|
-
- 「始终允许」按命令前缀持久化;「**本会话内允许**」是中间粒度:只对当前会话生效(记入 `allowActions` 并追加会话级 ruleset),其它会话 / 全局配置不变。
|
|
201
|
-
- 换档(`/perm`)会清除本会话「本会话内允许」授权;不需要时设 `sessionAllowButton: false` 回到三按钮。
|
|
116
|
+
| `/steer <文本>`、`/now` | 插队 / 立即执行排队消息 |
|
|
117
|
+
| `/current`、`/stop`、`/help` | 同主聊天流,作用于本话题会话 |
|
|
202
118
|
|
|
203
|
-
###
|
|
119
|
+
### 权限档位
|
|
204
120
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
### 表单 / 提问(`question` 工具)
|
|
213
|
-
|
|
214
|
-
agent 调 `question` 等 form 类交互时,插件把它转成飞书卡片:
|
|
215
|
-
|
|
216
|
-
- **两种作答方式等价**:直接点选项按钮,或**直接在话题里发文字**(无需先点「✍️ 直接回复答案」)。文本会智能匹配——命中选项 label/value 用选项值,`boolean` 认「是/否、yes/no、1/0」,`number`/`integer` 转数值,多选按顿号/逗号拆分,其余视为**手动输入**。
|
|
217
|
-
- 多字段表单可以混合作答:点几个按钮 + 补一条文字,填满即自动提交。
|
|
218
|
-
- **纯选项题**(有选项且不允许自填):可以直接**回复序号/字母**(如 `1`、`B`)或选项原文;若你发的是**其它内容**,插件会视为「你想说别的」——**自动跳过该表单**并把这条消息当**普通消息**交给 AI 处理,不再被误当成答案。
|
|
219
|
-
- **作答 / 取消后卡片会被撤回**(不再残留待填卡);若超出飞书撤回时限,降级为「已提交 / 已取消」结果卡。
|
|
220
|
-
- **没有这层转发,agent 一反问飞书会话就会永久卡住**——这也是会话卡死的常见原因。
|
|
221
|
-
|
|
222
|
-
### 卡片内容守卫(表格超限降级)
|
|
121
|
+
| 档位 | 含义 |
|
|
122
|
+
|---|---|
|
|
123
|
+
| 🔒 **只读** | 只看不改(禁止 `edit` / `shell`) |
|
|
124
|
+
| ✏️ **可编辑** | 改文件免审批,跑命令要问 |
|
|
125
|
+
| ⚠️ **高风险审批** | 改文件 / 跑命令 / 越目录都逐次审批 |
|
|
126
|
+
| 🔓 **完全信任** | 什么都不问 |
|
|
223
127
|
|
|
224
|
-
|
|
128
|
+
权限请求会变成审批卡:`✅ 允许一次` / `🔓 始终允许` / `✅ 本会话内允许该工具` / `❌ 拒绝`。换档(`/perm`)会清除本会话「本会话内允许」授权。
|
|
225
129
|
|
|
226
|
-
|
|
227
|
-
- 围栏代码块内的 `|` 不会被误判(先逐行计算围栏遮罩),降级幂等;单卡组件数收敛到 ≤200(超限时丢最旧元素)。
|
|
228
|
-
- 覆盖运行卡文本块、话题根卡 / 恢复卡 / 摘要,以及发送层兜底(`sendCard` / `replyCard` / `patchCard`);降级记 `warn` 便于观测。
|
|
130
|
+
### 表单提问(`question` 工具)
|
|
229
131
|
|
|
230
|
-
|
|
132
|
+
agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发文字**都能作答,作答后卡片自动撤回。纯选项题直接回序号 / 字母即可;若发的是其它内容,会当作普通消息交给 AI。
|
|
231
133
|
|
|
232
|
-
##
|
|
134
|
+
## 四、配置项(常用)
|
|
233
135
|
|
|
234
136
|
`<configDir>/plugins/feishu.json`(或 OpenCode `plugins[].options`),支持 `{env:NAME}` / `${NAME}` 展开。
|
|
235
137
|
|
|
@@ -238,142 +140,54 @@ agent 调 `question` 等 form 类交互时,插件把它转成飞书卡片:
|
|
|
238
140
|
| `appId` | string | — | 飞书 App ID(**必填**,缺失则禁用插件) |
|
|
239
141
|
| `appSecret` | string | — | 飞书 App Secret(**必填**,永不写入日志) |
|
|
240
142
|
| `domain` | `feishu`\|`lark` | `feishu` | 飞书 / Lark 国际版 |
|
|
241
|
-
| `allowUsers` | string[] | `[]` | open_id
|
|
143
|
+
| `allowUsers` | string[] | `[]` | open_id 白名单;空 = 仅 owner |
|
|
242
144
|
| `permissionGate` | `off`\|`notify`\|`gate`\|`lockdown` | `gate` | 全局审批门档位 |
|
|
243
145
|
| `allowTools` | string[] | `["read","glob","grep","webfetch"]` | 免审批白名单,支持 `prefix*` |
|
|
244
146
|
| `denyTools` | string[] | `[]` | 强制拒绝(优先于白名单) |
|
|
245
|
-
| `allowedRoots` | string[] | `[用户家目录]` |
|
|
246
|
-
| `stream` | boolean | `true` |
|
|
247
|
-
| `
|
|
248
|
-
| `threadRouting` | boolean | `true` | 话题路由总开关;`false` 时主聊天流普通文本进当前会话 |
|
|
249
|
-
| `topicGuidance` | boolean | `true` | 对飞书会话注入一句轻量 system 说明(离题可 `/new`),不拦截;本地会话绝不注入 |
|
|
250
|
-
| `recentDirsLimit` / `recentModelsLimit` | number | `5` | 表单「最近使用」条数(1–20) |
|
|
147
|
+
| `allowedRoots` | string[] | `[用户家目录]` | 允许的工作目录根;越界 / 系统目录拒绝 |
|
|
148
|
+
| `stream` | boolean | `true` | 流式回填回复 |
|
|
149
|
+
| `threadRouting` | boolean | `true` | 话题路由总开关 |
|
|
251
150
|
| `logLevel` | `debug`\|`info`\|`warn`\|`error` | `info` | 日志级别 |
|
|
252
|
-
| `logFile` | string \| boolean | — | `true` = 写 `<configDir>/plugins/feishu.log
|
|
253
|
-
| `gatewayLocation` | string | — | 只在该 location(或其**子目录**兜底)启动网关;`~` 自动展开、相对路径/尾斜杠会归一化。留空 = 任意 location 生效 |
|
|
254
|
-
| `gatewayMatchGraceMs` | number | `3000` | **精确匹配优先**的宽限窗口:子目录候选先等这么久,出现 `here === gatewayLocation` 就让位(0 = 不等待,子目录立即兜底) |
|
|
151
|
+
| `logFile` | string \| boolean | — | `true` = 写 `<configDir>/plugins/feishu.log`,服务模式建议开启 |
|
|
255
152
|
| `approvalTtlMs` | number | `600000` | 审批 token / 卡片有效期 |
|
|
256
|
-
| `staleExecutionMs` | number | `300000` |
|
|
257
|
-
| `
|
|
258
|
-
| `sessionAllowButton` | boolean | `true` | 审批卡是否显示「本会话内允许该工具」按钮 |
|
|
259
|
-
| `resumeSummary` | boolean | `true` | 恢复卡是否展示会话摘要 |
|
|
260
|
-
| `resumeSummaryTimeoutMs` | number | `15000` | 快摘要生成超时(3–60s),超时降级提示 |
|
|
261
|
-
| `resumeCompactTimeoutMs` | number | `120000` | 用户主动压缩后的轮询超时(30–300s) |
|
|
262
|
-
| `topicStatus` | boolean | `true` | 话题根卡工作状态总开关 |
|
|
263
|
-
| `topicStatusInTitle` | boolean | `false` | 是否在根卡标题加状态 emoji 前缀 |
|
|
264
|
-
| `topicStatusThrottleMs` | number | `1000` | 根卡状态刷新最小间隔(500–10000) |
|
|
265
|
-
| `cardMaxTables` | number | `4` | 单卡最多保留的 markdown 表格数(1–5);超出按整卡累计降级为围栏代码块,避免飞书 400 `code=230099` |
|
|
266
|
-
| `runnerCardMaxTools` | number | `12` | 运行卡最多保留的工具块数(1–50);更早的合并为「已省略前 N 次工具调用」 |
|
|
267
|
-
| `runnerCardTextMax` | number | `2048` | 运行卡单个文本块字符上限(512–8192) |
|
|
268
|
-
| `finalAnswerMinChars` | number | `600` | 最终回答 ≥ 该长度即**单独成卡/成文件**(0 = 关闭拆分) |
|
|
269
|
-
| `finalAnswerFileMinBytes` | number | `20480` | 最终回答 ≥ 该字节数转为 `.md` 文件发送(8192–102400) |
|
|
270
|
-
| `keepalive` | boolean | `true` | **位置保活**:周期性向 opencode 发一次活动,阻止 60 分钟空闲回收 location(会关掉飞书长连接、机器人失联) |
|
|
271
|
-
| `keepaliveIntervalMs` | number | `1200000` | 保活间隔(默认 20 分钟,夹取 5–45);必须显著小于 opencode 硬编码的 60 分钟 TTL |
|
|
272
|
-
|
|
273
|
-
---
|
|
274
|
-
|
|
275
|
-
## 五、安全设计
|
|
276
|
-
|
|
277
|
-
```
|
|
278
|
-
permission.evaluate (插件 hook) permission.asked (事件流)
|
|
279
|
-
白名单 → allow 拒绝名单 → deny 发飞书审批卡(自签 token)
|
|
280
|
-
会话内已放行 → allow 用户点击 → card.action.trigger(长连接)
|
|
281
|
-
其余(按会话预设)→ ask ────────────────────► 校验:白名单 → 验签 → 绑定字段 → 防重放
|
|
282
|
-
→ ctx.permission.reply(...)
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
- **自签 token**:HMAC-SHA256,绑定 `requestID + sessionID + 点击人 openId + 过期时间 + nonce`;伪造 / 转发 / 重放都会被拒。
|
|
286
|
-
- **强停 / 会话内放行按钮同源签名**:各自绑定专属字段 + 用途标签隔离,权责互不通用。
|
|
287
|
-
- **只对飞书来源的会话生效**:本地 TUI 等无映射会话不会被降级为 ask(否则会因没有审批出口而永久挂起)。
|
|
288
|
-
- **三重单人边界**:可用范围「仅本人」+ 不申请群权限 + 代码层 open_id 白名单。
|
|
289
|
-
- **`always` 语义**:仅当请求带 `save[]` 时才持久化,否则等价于「允许一次」。
|
|
290
|
-
|
|
291
|
-
### 长回答处理(运行卡瘦身 + 最终答案独立)
|
|
292
|
-
|
|
293
|
-
长任务(几十次工具调用)会把单张运行卡撑到上限(28KB / 200 元素),触发降级与**丢弃最旧块**——用户会看到"卡被撑满、前面内容消失"。默认策略:
|
|
294
|
-
|
|
295
|
-
1. **运行卡只做进度**:最多保留最近 `runnerCardMaxTools`(默认 12)个工具块,更早的合并为「…已省略前 N 次工具调用」;单个文本块上限 `runnerCardTextMax`(默认 2KB)。
|
|
296
|
-
2. **最终答案独立发送**:一轮结束时末尾文本 ≥ `finalAnswerMinChars`(默认 600 字符)→ 单独发一张「✅ 完整回答」卡(不与被工具噪声塞满的运行卡抢空间);运行卡内只留「完整回答已单独发送」提示。
|
|
297
|
-
3. **超长转文件**:最终回答 ≥ `finalAnswerFileMinBytes`(默认 20KB)→ 作为 `.md` 文件发送(可预览/下载),内容不截断、不丢失。
|
|
298
|
-
|
|
299
|
-
> 想要"完整轨迹保留、不省略工具调用":把 `runnerCardMaxTools` 调到足够大并接受卡片消息变多(或提高 `finalAnswerMinChars` 降低拆分频率)。
|
|
153
|
+
| `staleExecutionMs` | number | `300000` | 看门狗阈值(1–60 分钟) |
|
|
154
|
+
| `gatewayLocation` | string | — | 只在该 location(及其子目录)启动网关;留空 = 任意 location 生效 |
|
|
300
155
|
|
|
301
|
-
|
|
156
|
+
完整配置(含 `cardMaxTables`、`topicStatus*`、`resumeSummary*`、`keepalive*`、`gatewayMatchGraceMs` 等进阶项)见 [docs/advanced.md](./docs/advanced.md#完整配置项)。
|
|
302
157
|
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
| 机制 | 位置 | 触发条件 | 表现 |
|
|
306
|
-
|---|---|---|---|
|
|
307
|
-
| LayerMap `idleTimeToLive` | `packages/core/src/location-services.ts`(硬编码 `60 minutes`) | 60 分钟内无**会话级请求** | location 服务被销毁(静默) |
|
|
308
|
-
| `@opencode/LocationActivity` | 同为硬编码 60 分钟 | 60 分钟内无**带 location 的 durable 事件** | 先 interrupt 活动会话,再 `invalidate(location)`,日志 `location services evicted` |
|
|
309
|
-
|
|
310
|
-
两者都会让插件被 dispose(飞书长连接关闭)。**此后若该 location 再无请求,插件不会自行恢复 → 机器人永久沉默**(官方 issue:[#51343](https://github.com/anomalyco/opencode/issues/51343)、[#51891→#48691](https://github.com/anomalyco/opencode/issues/48691)、[#51828](https://github.com/anomalyco/opencode/issues/51828);TTL 无配置项)。
|
|
311
|
-
|
|
312
|
-
插件内置两道防线(都是默认开启,**无需任何外部脚本**):
|
|
313
|
-
|
|
314
|
-
1. **网关保活**(每 20 分钟,`keepaliveIntervalMs`):① 会话级 `GET /api/session/{id}` → `locations.get()` 续期 LayerMap,若已被回收则**重建 location**;② 创建 + GET + 删除一个探针会话 → 续期 `LocationActivity`(`session.created` 事件)。
|
|
315
|
-
2. **进程级网关看门狗**(同样每 20 分钟,每进程仅一个定时器):**任意** location 的插件实例都会登记,周期性对网关 location 做会话级 GET。效果:
|
|
316
|
-
- 网关实例即使已被回收,只要进程里还有**别的** location 存活(例如你在别的项目里开了 TUI/Web),网关会被自动救活;
|
|
317
|
-
- **服务重启后**,你第一次使用任意 location 时看门狗即启动(并在约 3 秒后立即探测一次),网关随之上线;
|
|
318
|
-
- 首次探测带 3 秒延迟,重启后恢复很快。
|
|
319
|
-
|
|
320
|
-
> **不需要任何外部脚本 / cron / systemd 配置**:以上两道防线都在插件进程内完成。
|
|
321
|
-
> 唯一无法覆盖的是「opencode 进程整个挂掉且长时间无人使用」——此时任何插件都无从执行;
|
|
322
|
-
> 重新使用 opencode 时会由看门狗自动恢复。`keepalive: false` 可关闭全部保活。
|
|
323
|
-
|
|
324
|
-
---
|
|
325
|
-
|
|
326
|
-
## 六、故障排查
|
|
158
|
+
## 五、故障排查
|
|
327
159
|
|
|
328
160
|
| 现象 | 处理 |
|
|
329
161
|
|---|---|
|
|
330
|
-
| 发消息没反应 | ①
|
|
162
|
+
| 发消息没反应 | ① 应用是否**已发布**、可用范围是否勾了你;② 订阅是否选了**长连接**(不是 Webhook);③ 是否开通 `im:message.p2p_msg:readonly` |
|
|
331
163
|
| 改了 `feishu.json` 不生效 | 确认路径,然后 `opencode reload` |
|
|
332
164
|
| 插件完全没被加载 | npm 方式确认包名在 `plugins` 数组;目录方式确认 `plugins/<名>/index.js` 存在 |
|
|
333
|
-
| 改了插件代码不生效 | `opencode reload` 不会重新 import 同路径模块;用 `opencode plugin update` 或重启服务 |
|
|
334
|
-
| 出现多个长连接 / 重复回复 | 设置 `gatewayLocation` 为常用工作目录(其子目录也会命中) |
|
|
335
|
-
| **完全无响应**,且日志中没有任何「长连接已启动」/「飞书插件已就绪」 | 多半是 `gatewayLocation` 与实际打开 opencode 的目录不匹配。插件会在延迟约 2 秒后用 `warn` 打出「已加载的 location 均未命中」;也可临时设 `logLevel: "debug"` 查看 `跳过非网关 location`。确认无误仍无响应就先**留空** `gatewayLocation` 排除该项 |
|
|
336
165
|
| 审批卡收不到 | 该会话不是从飞书发起的(无映射),插件按设计不接管 |
|
|
337
166
|
| 点按钮提示凭证无效 | token 过期(默认 10 分钟)或点击者不在白名单 |
|
|
338
|
-
|
|
|
339
|
-
|
|
|
340
|
-
| 会话像卡死、发消息只排队 | 看门狗默认 5 分钟后自动中断;也可点「⏹ 强制停止」或发 `/stop` |
|
|
341
|
-
| **空闲约 1 小时后机器人完全失联**(日志无「长连接已启动」) | opencode 回收了空闲 location(60 分钟硬编码 TTL)。内置保活 + 进程级看门狗默认开启,会自动恢复;也可手动用 `opencode api get /api/session/{id}`(任一本地会话)立即唤起。若长期不恢复,确认 `keepalive` 未被设为 `false` |
|
|
167
|
+
| 会话像卡死、只排队 | 看门狗默认 5 分钟后自动中断;也可点「⏹ 强制停止」或发 `/stop` |
|
|
168
|
+
| 空闲约 1 小时后失联 | opencode 会回收空闲 location;内置保活默认开启会自动恢复,见 [docs/advanced.md](./docs/advanced.md#位置保活) |
|
|
342
169
|
| 看不到插件日志 | 服务模式下 stderr 被丢弃,设 `logFile: true` |
|
|
343
|
-
|
|
|
170
|
+
| 多个长连接 / 重复回复 | 设置 `gatewayLocation` 为常用工作目录,见 [docs/advanced.md](./docs/advanced.md#多实例与网关选举) |
|
|
344
171
|
|
|
345
|
-
|
|
172
|
+
## 六、已知限制
|
|
346
173
|
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
npm test # vitest(纯逻辑单测,不连真飞书)
|
|
354
|
-
npm run dev # tsup --watch
|
|
355
|
-
```
|
|
356
|
-
|
|
357
|
-
**架构**:`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/`。
|
|
358
|
-
|
|
359
|
-
**设计要点**:卡片一律 **JSON 2.0**(按钮放 `body.elements`,回调用 `behaviors`);更新统一节流 ≥400ms;连续工具调用 ≥3 个自动折叠;运行卡状态用**纯 reducer** 维护。
|
|
174
|
+
- 图片 / 文件消息会**下载到本地并挂进会话**(需开 `im:message:readonly`);音频 / 视频 / 表情包仍只给文字占位。
|
|
175
|
+
- 只接管**从飞书发起的会话**的审批;本地 TUI 会话不受影响。
|
|
176
|
+
- 建会话只有一条主路径:`/new` 与 `/form` 等价的表单卡。
|
|
177
|
+
- 表单为 JSON 2.0,老客户端对 `select_static` 有最低版本要求(≥ V3.7.0)。
|
|
178
|
+
- 话题首条消息可能不带 `thread_id`:插件会靠 `root_id` 兜底路由;新话题敲命令落到主聊天流时,直接进话题发消息即可。
|
|
179
|
+
- 生态里另有 `opencode-feishu`(V1 插件),与本插件不兼容、不共用代码。
|
|
360
180
|
|
|
361
|
-
|
|
181
|
+
## 七、下一步规划(Roadmap)
|
|
362
182
|
|
|
363
|
-
|
|
183
|
+
按真实使用反馈迭代,当前规划:
|
|
364
184
|
|
|
365
|
-
-
|
|
366
|
-
-
|
|
185
|
+
- [x] **接收图片 / 文件**:已支持——自动下载到 `<configDir>/plugins/feishu-files/` 并作为附件挂进会话(需开 `im:message:readonly`;单附件默认 ≤20MB)。
|
|
186
|
+
- [ ] **忙时新消息默认插队**:目前会话忙时新消息默认排队(可用 `/steer`、`/now` 手动插队)。规划:忙时你发的新消息**默认直接插队**,立即打断当前步骤优先执行。
|
|
367
187
|
|
|
368
|
-
##
|
|
188
|
+
## 高级主题与开发
|
|
369
189
|
|
|
370
|
-
|
|
371
|
-
- 只接管**从飞书发起的会话**的审批;本地 TUI 会话不受影响。
|
|
372
|
-
- 消息去重为 `get-then-set`,非原子:极端并发下理论上可能双处理。
|
|
373
|
-
- 话题被删除后映射不主动清理(惰性忽略)。
|
|
374
|
-
- 建会话只有一条主路径:`/new` 与 `/form` 等价的表单卡;`/dir` `/model` `/perm` 仅用于预填。
|
|
375
|
-
- 表单为 JSON 2.0,部分老客户端对 `select_static` 有最低版本要求(≥ V3.7.0)。
|
|
376
|
-
- 话题首条消息可能不带 `thread_id`:回复带 root 映射的卡片时插件会靠 `root_id` 兜底路由;新话题缺 `thread_id` 时,敲 `/new` 等命令会落到主聊天流,直接进话题发消息即可。
|
|
190
|
+
安全设计、位置保活、卡片守卫、会话恢复、多实例网关选举、完整配置项与开发架构见 [docs/advanced.md](./docs/advanced.md)。
|
|
377
191
|
|
|
378
192
|
## 许可证
|
|
379
193
|
|