opencode-feishu-plugin 0.2.6 → 0.2.8

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 +111 -446
  2. package/README.md +78 -254
  3. package/dist/index.js +252 -28
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -2,56 +2,44 @@
2
2
 
3
3
  [English](./README.en.md) | **简体中文**
4
4
 
5
- 把 [OpenCode](https://opencode.ai) 接进飞书:**一个飞书话题 = 一个 OpenCode 会话**。你在飞书里用话题管理多个会话、指挥 AI 写代码,**权限审批直接在飞书卡片上点按钮**。
5
+ 把 [OpenCode](https://opencode.ai) 接进飞书:**一个飞书话题 = 一个 OpenCode 会话**,权限审批直接在飞书卡片上点按钮。
6
6
 
7
- - **只用 OpenCode V2 插件 API**(`Plugin.define({ id, setup(ctx) })`),不依赖任何 V1 包。
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
  ![飞书话题内的完整会话:工具调用、权限审批卡与强制停止](image/image.png)
17
13
 
18
14
  ## 亮点
19
15
 
20
16
  | | 说明 |
21
17
  |---|---|
22
- | 🔐 **最小权限** | 只要 2 个 scope(单聊读 + 发消息)。不申请任何群权限,机器人物理上收不到群消息 |
23
- | 💬 **话题 = 会话** | 每个飞书话题对应一个 OpenCode 会话,主聊天流只做管理,话题里干活,互不串台 |
24
- | 🚀 **一键进入** | `/new` 直接发建会话表单,提交后机器人自动在你消息下开话题 |
25
- | 📝 **表单一次填完** | `/new` 与 `/form` 完全等价:目录 + 模型 + 权限一张表单一次提交即建会话,**零新增权限** |
26
- | ✅ **卡片审批** | 权限请求变成飞书卡片(允许一次 / 始终允许 / 拒绝 / 本会话内允许),点击即批准,带签名防伪防重放 |
27
- | 🪜 **权限预设** | 只读 / 可编辑 / 高风险审批 / 完全信任,四档一次选定,告别逐次审批 |
28
- | 📊 **实时可见** | 先回执「思考中」,工具调用实时上卡(≥3 个自动折叠),文本流式更新,页脚显示当前模型 |
29
- | 🧵 **原生排队** | 会话忙时自动排队(opencode 原生 `delivery:"queue"`),可用 `/steer` `/now` 插队 |
30
- | ⏹ **一键强停** | 所有 AI 回复卡片都带「强制停止」按钮;卡死会话由看门狗自动中断 |
31
- | 🚫 **无端口** | 全程长连接,服务器不用开放任何入站端口 |
32
-
33
- ---
18
+ | 🔐 **最小权限** | 只要 2 个 scope,不申请任何群权限,机器人物理上收不到群消息 |
19
+ | 💬 **话题 = 会话** | 一个飞书话题对应一个 OpenCode 会话;主聊天流只做管理,互不串台 |
20
+ | 🚀 **一键建会话** | `/new` 一张表单(目录 + 模型 + 权限档位)一次填完,提交即建会话并自动开话题 |
21
+ | ✅ **卡片审批** | 权限请求变飞书卡片:允许一次 / 始终允许 / 本会话内允许 / 拒绝,自签 token 防伪防重放 |
22
+ | 📊 **实时可见** | 「思考中」回执 → 工具调用实时上卡 → 文本流式更新,页脚显示当前模型 |
23
+ | ⏹ **可控可停** | 每张回复卡带「强制停止」;看门狗自动中断卡死会话;忙时原生排队,`/steer` `/now` 可插队 |
24
+ | 🚫 **无端口** | 全程长连接,服务器无需开放任何入站端口 |
34
25
 
35
26
  ## 一、飞书后台配置(约 3 分钟)
36
27
 
37
28
  1. 打开 [飞书开放平台](https://open.feishu.cn/app) → **创建企业自建应用**。
38
29
  2. **添加应用能力 → 机器人**。
39
- 3. **权限管理**,只开通这两个:
30
+ 3. **权限管理**,只开通这两个 scope:
40
31
  - `im:message.p2p_msg:readonly` —— 读取用户发给机器人的单聊消息
41
32
  - `im:message:send_as_bot` —— 以应用身份发消息(也用于更新卡片)
42
33
  4. **事件与回调 → 事件配置**:订阅方式选**「使用长连接接收事件」**(不要选 Webhook),添加事件 `im.message.receive_v1`。
43
- 5. **事件与回调 → 回调配置**:订阅方式同样选**长连接**,添加回调 `card.action.trigger`(**零权限要求**)。
44
- 6. **版本管理与发布**:可用范围 = **仅本人**,创建版本并发布。
34
+ 5. **事件与回调 → 回调配置**:订阅方式同样选**长连接**,添加回调 `card.action.trigger`(零权限要求)。
35
+ 6. **版本管理与发布**:可用范围 = **仅本人**,创建版本并**发布**。⚠️ 不发布就是开发态,长连接连不上,机器人不会有任何反应。
45
36
  7. 记下 **App ID**(`cli_…`)与 **App Secret**。
46
37
 
47
- > **为什么不申请群权限?** 本插件是"一个人的遥控台"。不申请群权限,机器人**物理上收不到群消息**,
48
- > 单人边界由平台 scope 层保证,而不是只靠代码判断。
49
-
50
- ---
38
+ > **为什么不申请群权限?** 本插件是"一个人的遥控台"。不申请群权限,机器人**物理上收不到群消息**,单人边界由平台 scope 层保证,而不是只靠代码判断。
51
39
 
52
40
  ## 二、安装
53
41
 
54
- ### 1. 安装插件(V2:npm 自动加载,推荐)
42
+ ### 1. 安装插件
55
43
 
56
44
  ```bash
57
45
  # 方式 A:CLI(推荐)
@@ -59,54 +47,42 @@ opencode plugin add opencode-feishu-plugin
59
47
  ```
60
48
 
61
49
  ```jsonc
62
- // 方式 B:手写 ~/.config/opencode/opencode.jsonc
50
+ // 方式 B:手写配置(追加到已有 plugins 数组,别覆盖整个文件)
63
51
  { "plugins": ["opencode-feishu-plugin"] }
64
52
  ```
65
53
 
66
- 插件入口指向**自包含**的 `dist/index.js`(含飞书 SDK 等依赖),运行时无需手动 `npm install`。
67
- 本地开发则克隆后 `npm install && npm run build`,把本地目录写进 `plugins`:`{ "plugins": ["./path/to/opencode-feishu-plugin"] }`。
54
+ 插件入口是**自包含**的 `dist/index.js`(已打包飞书 SDK),运行时无需手动 `npm install`。
68
55
 
69
56
  ### 2. 写配置
70
57
 
71
- `<configDir>/plugins/feishu.json`(`configDir` = `OPENCODE_CONFIG_DIR` 或 `~/.config/opencode`):
58
+ 新建 `~/.config/opencode/plugins/feishu.json`(`configDir` = `OPENCODE_CONFIG_DIR` 或 `~/.config/opencode`):
72
59
 
73
60
  ```bash
74
61
  install -m 600 /dev/null ~/.config/opencode/plugins/feishu.json
75
62
  cat > ~/.config/opencode/plugins/feishu.json <<'JSON'
76
63
  {
77
- "appId": "{env:FEISHU_APP_ID}",
78
- "appSecret": "{env:FEISHU_APP_SECRET}"
64
+ "appId": "cli_xxxxxxxx",
65
+ "appSecret": "xxxxxxxx",
66
+ "logFile": true
79
67
  }
80
68
  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
69
  ```
90
70
 
91
- 也可直接明文写进 `feishu.json`(权限记得 `600`)。**优先级**:`options` > `feishu.json` > 环境变量。
71
+ - 凭证**优先级**:`plugins[].options` > `feishu.json` > 环境变量。也可在值里用 `{env:NAME}` / `${NAME}` 占位符从环境变量取值。
72
+ - `logFile: true` **建议开启**:服务模式下 stderr 会被丢弃,开着才有日志可查。
92
73
 
93
- ### 3. 生效与确认
74
+ ### 3. 生效与验证
94
75
 
95
76
  ```bash
96
- opencode reload # 重新加载配置与插件
97
- opencode mcp list # 顺带确认服务健康
77
+ opencode reload
78
+ tail -f ~/.config/opencode/plugins/feishu.log # 应看到「飞书长连接已启动(WSClient)」
98
79
  ```
99
80
 
100
- 在飞书里给机器人发一条消息。**第一次发消息的人会被自动绑定为 owner**,之后其他人被静默忽略。
101
-
102
- > 用全局插件目录加载(离线 / 固定目录)或升级插件后,需要重启服务才重新 import:
103
- > ```bash
104
- > opencode service restart
105
- > ```
81
+ 然后在飞书里给机器人发一条消息。**第一次发消息的人会被自动绑定为 owner**,机器人回复卡片即安装成功;之后其他人会被静默忽略。
106
82
 
107
- ---
83
+ > 升级插件或改用全局插件目录加载后,需要 `opencode service restart` 才会重新 import。
108
84
 
109
- ## 三、怎么用
85
+ ## 三、快速上手
110
86
 
111
87
  ### 主聊天流(管理台)
112
88
 
@@ -114,17 +90,15 @@ opencode mcp list # 顺带确认服务健康
114
90
 
115
91
  | 命令 | 作用 |
116
92
  |---|---|
117
- | `/new [标题]` | 发建会话表单卡,提交即建会话并自动开话题(与 `/form` 等价) |
118
- | `/form [标题]` | 同上,`/new` 的等价入口 |
119
- | `/sessions`(`/ls`) | **全部**会话列表卡片(含本机所有 opencode 会话),可翻页、可进话题、可新建 |
120
- | `/resume [序号]` | 对最近更新(或列表第 N 个)的会话发恢复卡,**回复该卡**即续聊 |
121
- | `/current` | 查看当前会话 |
122
- | `/stop` | 中断当前会话正在跑的任务 |
123
- | `/steer <文本>` | 立即插队发送一条消息(打断当前步骤,不等排队) |
124
- | `/now` | 把该会话已排队的消息全部改为立即执行 |
125
- | `/dir` `/model` `/perm` | 给表单**预填**工作目录 / 模型 / 权限档位 |
126
- | `/cancel` | 放弃未提交的表单 |
127
- | `/help` | 命令列表 |
93
+ | `/new [标题]` | 发建会话表单,提交即建会话并自动开话题(与 `/form` 等价) |
94
+ | `/sessions`(`/ls`) | **全部**会话列表(含本机所有 opencode 会话),可翻页、进话题、新建 |
95
+ | `/resume [序号]` | 对最近(或列表第 N 个)会话发恢复卡,**回复该卡**即续聊 |
96
+ | `/current`、`/stop` | 查看当前会话 / 中断当前任务 |
97
+ | `/steer <文本>`、`/now` | 立即插队发消息 / 把排队消息改为立即执行 |
98
+ | `/dir`、`/model`、`/perm` | 为建会话表单**预填**工作目录 / 模型 / 权限档位 |
99
+ | `/cancel`、`/help` | 放弃未提交的表单 / 命令列表 |
100
+
101
+ ![建会话表单卡:目录、模型、权限一次填完](image/new.png) ![会话列表卡片:翻页、进入/再开、新建](image/sessions.png)
128
102
 
129
103
  ### 话题内(干活)
130
104
 
@@ -132,104 +106,28 @@ opencode mcp list # 顺带确认服务健康
132
106
 
133
107
  | 命令 | 作用 |
134
108
  |---|---|
135
- | `/model` | 切换本会话模型 |
109
+ | `/model` | 切换本会话模型(只影响后续回复) |
136
110
  | `/perm` | 修改本会话权限档位 |
137
111
  | `/cd <路径>` | 迁移本会话工作目录 |
138
- | `/steer <文本>` | 立即插队发一条消息 |
139
- | `/now` | 把本会话排队消息改为立即执行 |
140
- | `/current` `/stop` `/help` | 同主聊天流,作用于本话题会话 |
141
-
142
- ### 建会话(`/new` 与 `/form` 完全等价)
143
-
144
- ```
145
- /new 修一下登录 bug
146
- ↓
147
- 📝 建会话表单卡(工作目录 / 模型 / 权限,一次填完)
148
- ↓ 点「✅ 创建会话」
149
- 表单消息本身成为话题根,机器人 reply_in_thread 发「会话已就绪」卡
150
- ```
151
-
152
- ![/new 建会话表单卡:目录、模型、权限一次填完](image/new.png)
153
-
154
- - 目录可直接输入,也可从下拉选择允许根目录的一级子目录;**留空 = 允许根目录**,不存在会自动创建。
155
- - `/dir` `/model` `/perm` 只作为表单预填能力,不再是必经步骤。
156
- - 目录非法时**不建会话**,回带错误说明并保留已填项;`/cancel` 放弃表单。
157
-
158
- ### 续聊历史会话(`/sessions` + `/resume`)
159
-
160
- `/sessions` 列出 opencode **本机全部**会话(按更新时间倒序,分页 8 条可配):
161
-
162
- ![/sessions 会话列表卡:翻页、进入/再开、新建会话](image/sessions.png)
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` 回到三按钮。
112
+ | `/steer <文本>`、`/now` | 插队 / 立即执行排队消息 |
113
+ | `/current`、`/stop`、`/help` | 同主聊天流,作用于本话题会话 |
202
114
 
203
- ### 排队与插队
115
+ ### 权限档位
204
116
 
205
- 会话忙时新消息默认**原生排队**(页面页脚显示「已排队」),`/steer <文本>` 立即插队打断当前步骤,`/now` 把已排队未执行的消息全部改插队。
206
-
207
- ### 强制停止与看门狗
208
-
209
- 每张回复卡底部都有「⏹ 强制停止」按钮(重签 token,防重放);已完成 / 失败时变灰并只回 toast。
210
- **看门狗**(默认 5 分钟,`staleExecutionMs` 可配):执行态或排队超过阈值无进展即视为卡死,主动 `session.interrupt` + 取消排队 + 发提示卡,避免"一个会话卡死、后续永远排队"。
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
- ### 卡片内容守卫(表格超限降级)
117
+ | 档位 | 含义 |
118
+ |---|---|
119
+ | 🔒 **只读** | 只看不改(禁止 `edit` / `shell`) |
120
+ | ✏️ **可编辑** | 改文件免审批,跑命令要问 |
121
+ | ⚠️ **高风险审批** | 改文件 / 跑命令 / 越目录都逐次审批 |
122
+ | 🔓 **完全信任** | 什么都不问 |
223
123
 
224
- 飞书**单卡最多 5 个表格组件**,超限时 patch 直接返回 400(`code=230099`)——回复里出现大量 markdown 对照表时,卡片会永远停在旧内容、看起来像卡死。插件对**整张卡片**做守卫:
124
+ 权限请求会变成审批卡:`✅ 允许一次` / `🔓 始终允许` / `✅ 本会话内允许该工具` / `❌ 拒绝`。换档(`/perm`)会清除本会话「本会话内允许」授权。
225
125
 
226
- - 表格数**按整卡累计**(`cardMaxTables`,默认 `4`,夹取 1–5),超出的表格**降级为围栏代码块**:内容一字不丢,只是不再按表格渲染。
227
- - 围栏代码块内的 `|` 不会被误判(先逐行计算围栏遮罩),降级幂等;单卡组件数收敛到 ≤200(超限时丢最旧元素)。
228
- - 覆盖运行卡文本块、话题根卡 / 恢复卡 / 摘要,以及发送层兜底(`sendCard` / `replyCard` / `patchCard`);降级记 `warn` 便于观测。
126
+ ### 表单提问(`question` 工具)
229
127
 
230
- ---
128
+ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发文字**都能作答,作答后卡片自动撤回。纯选项题直接回序号 / 字母即可;若发的是其它内容,会当作普通消息交给 AI。
231
129
 
232
- ## 四、配置项
130
+ ## 四、配置项(常用)
233
131
 
234
132
  `<configDir>/plugins/feishu.json`(或 OpenCode `plugins[].options`),支持 `{env:NAME}` / `${NAME}` 展开。
235
133
 
@@ -238,128 +136,54 @@ agent 调 `question` 等 form 类交互时,插件把它转成飞书卡片:
238
136
  | `appId` | string | — | 飞书 App ID(**必填**,缺失则禁用插件) |
239
137
  | `appSecret` | string | — | 飞书 App Secret(**必填**,永不写入日志) |
240
138
  | `domain` | `feishu`\|`lark` | `feishu` | 飞书 / Lark 国际版 |
241
- | `allowUsers` | string[] | `[]` | open_id 白名单。空 = 仅应用 owner |
139
+ | `allowUsers` | string[] | `[]` | open_id 白名单;空 = 仅 owner |
242
140
  | `permissionGate` | `off`\|`notify`\|`gate`\|`lockdown` | `gate` | 全局审批门档位 |
243
141
  | `allowTools` | string[] | `["read","glob","grep","webfetch"]` | 免审批白名单,支持 `prefix*` |
244
142
  | `denyTools` | string[] | `[]` | 强制拒绝(优先于白名单) |
245
- | `allowedRoots` | string[] | `[用户家目录]` | 会话工作目录允许根目录;越界 / 系统目录拒绝;留空回退第一个根,不存在自动创建 |
246
- | `stream` | boolean | `true` | 是否流式回填回复 |
247
- | `streamThrottleMs` | number | `400` | 卡片更新最小间隔(下限 400ms) |
248
- | `threadRouting` | boolean | `true` | 话题路由总开关;`false` 时主聊天流普通文本进当前会话 |
249
- | `topicGuidance` | boolean | `true` | 对飞书会话注入一句轻量 system 说明(离题可 `/new`),不拦截;本地会话绝不注入 |
250
- | `recentDirsLimit` / `recentModelsLimit` | number | `5` | 表单「最近使用」条数(1–20) |
143
+ | `allowedRoots` | string[] | `[用户家目录]` | 允许的工作目录根;越界 / 系统目录拒绝 |
144
+ | `stream` | boolean | `true` | 流式回填回复 |
145
+ | `threadRouting` | boolean | `true` | 话题路由总开关 |
251
146
  | `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 = 不等待,子目录立即兜底) |
147
+ | `logFile` | string \| boolean | — | `true` = 写 `<configDir>/plugins/feishu.log`,服务模式建议开启 |
255
148
  | `approvalTtlMs` | number | `600000` | 审批 token / 卡片有效期 |
256
- | `staleExecutionMs` | number | `300000` | 看门狗阈值(夹取 1–60 分钟) |
257
- | `maxResourcesShown` | number | `8` | 审批卡最多展示的资源行数 |
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
- | `keepalive` | boolean | `true` | **位置保活**:周期性向 opencode 发一次活动,阻止 60 分钟空闲回收 location(会关掉飞书长连接、机器人失联) |
267
- | `keepaliveIntervalMs` | number | `1200000` | 保活间隔(默认 20 分钟,夹取 5–45);必须显著小于 opencode 硬编码的 60 分钟 TTL |
268
-
269
- ---
270
-
271
- ## 五、安全设计
272
-
273
- ```
274
- permission.evaluate (插件 hook) permission.asked (事件流)
275
- 白名单 → allow 拒绝名单 → deny 发飞书审批卡(自签 token)
276
- 会话内已放行 → allow 用户点击 → card.action.trigger(长连接)
277
- 其余(按会话预设)→ ask ────────────────────► 校验:白名单 → 验签 → 绑定字段 → 防重放
278
- → ctx.permission.reply(...)
279
- ```
280
-
281
- - **自签 token**:HMAC-SHA256,绑定 `requestID + sessionID + 点击人 openId + 过期时间 + nonce`;伪造 / 转发 / 重放都会被拒。
282
- - **强停 / 会话内放行按钮同源签名**:各自绑定专属字段 + 用途标签隔离,权责互不通用。
283
- - **只对飞书来源的会话生效**:本地 TUI 等无映射会话不会被降级为 ask(否则会因没有审批出口而永久挂起)。
284
- - **三重单人边界**:可用范围「仅本人」+ 不申请群权限 + 代码层 open_id 白名单。
285
- - **`always` 语义**:仅当请求带 `save[]` 时才持久化,否则等价于「允许一次」。
286
-
287
- ### 位置保活(防空闲失联,默认开启)
288
-
289
- opencode 会**回收空闲的 location**,这会连带卸载插件、关闭飞书长连接:
290
-
291
- | 机制 | 位置 | 触发条件 | 表现 |
292
- |---|---|---|---|
293
- | LayerMap `idleTimeToLive` | `packages/core/src/location-services.ts`(硬编码 `60 minutes`) | 60 分钟内无**会话级请求** | location 服务被销毁(静默) |
294
- | `@opencode/LocationActivity` | 同为硬编码 60 分钟 | 60 分钟内无**带 location 的 durable 事件** | 先 interrupt 活动会话,再 `invalidate(location)`,日志 `location services evicted` |
295
-
296
- 两者都会让插件被 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 无配置项)。
297
-
298
- 插件内置两道防线(都是默认开启,**无需任何外部脚本**):
299
-
300
- 1. **网关保活**(每 20 分钟,`keepaliveIntervalMs`):① 会话级 `GET /api/session/{id}` → `locations.get()` 续期 LayerMap,若已被回收则**重建 location**;② 创建 + GET + 删除一个探针会话 → 续期 `LocationActivity`(`session.created` 事件)。
301
- 2. **进程级网关看门狗**(同样每 20 分钟,每进程仅一个定时器):**任意** location 的插件实例都会登记,周期性对网关 location 做会话级 GET。效果:
302
- - 网关实例即使已被回收,只要进程里还有**别的** location 存活(例如你在别的项目里开了 TUI/Web),网关会被自动救活;
303
- - **服务重启后**,你第一次使用任意 location 时看门狗即启动(并在约 3 秒后立即探测一次),网关随之上线;
304
- - 首次探测带 3 秒延迟,重启后恢复很快。
149
+ | `staleExecutionMs` | number | `300000` | 看门狗阈值(1–60 分钟) |
150
+ | `gatewayLocation` | string | — | 只在该 location(及其子目录)启动网关;留空 = 任意 location 生效 |
305
151
 
306
- > **不需要任何外部脚本 / cron / systemd 配置**:以上两道防线都在插件进程内完成。
307
- > 唯一无法覆盖的是「opencode 进程整个挂掉且长时间无人使用」——此时任何插件都无从执行;
308
- > 重新使用 opencode 时会由看门狗自动恢复。`keepalive: false` 可关闭全部保活。
152
+ 完整配置(含 `cardMaxTables`、`topicStatus*`、`resumeSummary*`、`keepalive*`、`gatewayMatchGraceMs` 等进阶项)见 [docs/advanced.md](./docs/advanced.md#完整配置项)。
309
153
 
310
- ---
311
-
312
- ## 六、故障排查
154
+ ## 五、故障排查
313
155
 
314
156
  | 现象 | 处理 |
315
157
  |---|---|
316
- | 发消息没反应 | ① 应用是否已发布、可用范围是否勾了你;② 订阅是否选了**长连接**(不是 Webhook);③ 是否开通 `im:message.p2p_msg:readonly` |
158
+ | 发消息没反应 | ① 应用是否**已发布**、可用范围是否勾了你;② 订阅是否选了**长连接**(不是 Webhook);③ 是否开通 `im:message.p2p_msg:readonly` |
317
159
  | 改了 `feishu.json` 不生效 | 确认路径,然后 `opencode reload` |
318
160
  | 插件完全没被加载 | npm 方式确认包名在 `plugins` 数组;目录方式确认 `plugins/<名>/index.js` 存在 |
319
- | 改了插件代码不生效 | `opencode reload` 不会重新 import 同路径模块;用 `opencode plugin update` 或重启服务 |
320
- | 出现多个长连接 / 重复回复 | 设置 `gatewayLocation` 为常用工作目录(其子目录也会命中) |
321
- | **完全无响应**,且日志中没有任何「长连接已启动」/「飞书插件已就绪」 | 多半是 `gatewayLocation` 与实际打开 opencode 的目录不匹配。插件会在延迟约 2 秒后用 `warn` 打出「已加载的 location 均未命中」;也可临时设 `logLevel: "debug"` 查看 `跳过非网关 location`。确认无误仍无响应就先**留空** `gatewayLocation` 排除该项 |
322
161
  | 审批卡收不到 | 该会话不是从飞书发起的(无映射),插件按设计不接管 |
323
162
  | 点按钮提示凭证无效 | token 过期(默认 10 分钟)或点击者不在白名单 |
324
- | 卡片内容被截断 | 飞书卡片上限约 30KB,超长会话丢弃卡片上最旧块(完整内容仍在会话里) |
325
- | 建会话后没看到话题 | 表单卡会改写为「✅ 已创建 · …」并附手动创建话题指引 |
326
- | 会话像卡死、发消息只排队 | 看门狗默认 5 分钟后自动中断;也可点「⏹ 强制停止」或发 `/stop` |
327
- | **空闲约 1 小时后机器人完全失联**(日志无「长连接已启动」) | opencode 回收了空闲 location(60 分钟硬编码 TTL)。内置保活 + 进程级看门狗默认开启,会自动恢复;也可手动用 `opencode api get /api/session/{id}`(任一本地会话)立即唤起。若长期不恢复,确认 `keepalive` 未被设为 `false` |
163
+ | 会话像卡死、只排队 | 看门狗默认 5 分钟后自动中断;也可点「⏹ 强制停止」或发 `/stop` |
164
+ | 空闲约 1 小时后失联 | opencode 会回收空闲 location;内置保活默认开启会自动恢复,见 [docs/advanced.md](./docs/advanced.md#位置保活) |
328
165
  | 看不到插件日志 | 服务模式下 stderr 被丢弃,设 `logFile: true` |
329
- | 切了 `/model` 但历史还是旧模型 | 预期行为:切换只影响后续回复,历史消息保留各自当时的模型 |
330
-
331
- ---
332
-
333
- ## 七、开发
166
+ | 多个长连接 / 重复回复 | 设置 `gatewayLocation` 为常用工作目录,见 [docs/advanced.md](./docs/advanced.md#多实例与网关选举) |
334
167
 
335
- ```bash
336
- npm install
337
- npm run typecheck # tsc --noEmit
338
- npm run build # tsup → dist/(自包含 bundle)
339
- npm test # vitest(纯逻辑单测,不连真飞书)
340
- npm run dev # tsup --watch
341
- ```
168
+ ## 六、已知限制
342
169
 
343
- **架构**:`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/`。
344
-
345
- **设计要点**:卡片一律 **JSON 2.0**(按钮放 `body.elements`,回调用 `behaviors`);更新统一节流 ≥400ms;连续工具调用 ≥3 个自动折叠;运行卡状态用**纯 reducer** 维护。
170
+ - 只处理**单聊文本**(含富文本);图片 / 文件 / 音视频只给文字占位,不下载。
171
+ - 只接管**从飞书发起的会话**的审批;本地 TUI 会话不受影响。
172
+ - 建会话只有一条主路径:`/new` 与 `/form` 等价的表单卡。
173
+ - 表单为 JSON 2.0,老客户端对 `select_static` 有最低版本要求(≥ V3.7.0)。
174
+ - 话题首条消息可能不带 `thread_id`:插件会靠 `root_id` 兜底路由;新话题敲命令落到主聊天流时,直接进话题发消息即可。
175
+ - 生态里另有 `opencode-feishu`(V1 插件),与本插件不兼容、不共用代码。
346
176
 
347
- ---
177
+ ## 七、下一步规划(Roadmap)
348
178
 
349
- ## 八、与其它项目的区别
179
+ 按真实使用反馈迭代,当前规划:
350
180
 
351
- - 本插件**只支持 OpenCode V2**(`@opencode/plugin`,`Plugin.define` 形态)。
352
- - 生态里另有 `opencode-feishu`(V1 插件,`@opencode-ai/plugin`),两者**不兼容**、不共用代码,请按你的 OpenCode 版本选择。
181
+ - [ ] **接收图片 / 文件**:目前只处理单聊文本,图片 / 文件仅显示文字占位。规划:下载图片 / 文件并作为**附件挂进会话**,让支持视觉 / 文件的模型直接看图、读文件内容。
182
+ - [ ] **忙时新消息默认插队**:目前会话忙时新消息默认排队(可用 `/steer`、`/now` 手动插队)。规划:忙时你发的新消息**默认直接插队**,立即打断当前步骤优先执行。
353
183
 
354
- ## 已知限制
184
+ ## 高级主题与开发
355
185
 
356
- - 只处理**单聊文本**(含富文本);图片 / 文件 / 音视频只给文字占位,不下载。
357
- - 只接管**从飞书发起的会话**的审批;本地 TUI 会话不受影响。
358
- - 消息去重为 `get-then-set`,非原子:极端并发下理论上可能双处理。
359
- - 话题被删除后映射不主动清理(惰性忽略)。
360
- - 建会话只有一条主路径:`/new` 与 `/form` 等价的表单卡;`/dir` `/model` `/perm` 仅用于预填。
361
- - 表单为 JSON 2.0,部分老客户端对 `select_static` 有最低版本要求(≥ V3.7.0)。
362
- - 话题首条消息可能不带 `thread_id`:回复带 root 映射的卡片时插件会靠 `root_id` 兜底路由;新话题缺 `thread_id` 时,敲 `/new` 等命令会落到主聊天流,直接进话题发消息即可。
186
+ 安全设计、位置保活、卡片守卫、会话恢复、多实例网关选举、完整配置项与开发架构见 [docs/advanced.md](./docs/advanced.md)。
363
187
 
364
188
  ## 许可证
365
189