opencode-feishu-plugin 0.1.3 → 0.2.0
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 +28 -6
- package/README.md +31 -3
- package/dist/index.js +475 -157
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -242,7 +242,7 @@ Besides sessions created from Feishu, you can **load any past OpenCode session v
|
|
|
242
242
|
[▶️ Enter topic] [▶️ New topic] [⬅️ Prev] [➡️ Next] [➕ New session]
|
|
243
243
|
```
|
|
244
244
|
|
|
245
|
-
- Data source
|
|
245
|
+
- Data source, in order: `ctx.session.list()` (usually **not exposed** in the V2 plugin runtime) → **local HTTP `GET /api/session`** (same machine, returns **all** sessions including ones created in the TUI/Web) → the plugin's mapping table. Entering an external session also binds a mapping for it so approvals/notifications keep working.
|
|
246
246
|
- Each row shows: title (truncated), short id, relative time, `💬 topic-bound` (this session already has a topic mapping), `📍 <directory tail>`.
|
|
247
247
|
- **Paging**: 8 per page by default (`sessionPageSize`, clamped 5–20); the bottom buttons flip pages (`{cmd:"list", page:N}`).
|
|
248
248
|
- **"➕ New session"** opens the setup form card (same as `/new` `/form`) instead of creating a session directly.
|
|
@@ -358,9 +358,9 @@ The threshold is `staleExecutionMs` (default 5 minutes, clamped to 1–60 minute
|
|
|
358
358
|
|
|
359
359
|
When the agent calls the `question` tool (or any form interaction), OpenCode creates a pending form that blocks execution. The plugin relays it as a Feishu card:
|
|
360
360
|
|
|
361
|
-
- tap an option
|
|
362
|
-
-
|
|
363
|
-
- the card
|
|
361
|
+
- **Two equivalent ways to answer**: tap an option button, or just **send text in the topic** (no need to tap "✍️ reply directly" first). Text is matched intelligently — option label/value are matched to their value, booleans accept 是/否 & yes/no & 1/0, numbers are parsed, multiselect splits on commas, anything else counts as a **manual answer**;
|
|
362
|
+
- multi-field forms can be answered with a mix of taps and a text reply; they submit automatically once every field is filled;
|
|
363
|
+
- **the card is recalled once answered/cancelled**; if it is past Feishu's recall window, it degrades to a "submitted/cancelled" result card instead.
|
|
364
364
|
|
|
365
365
|
Without this relay, any clarifying question would stall the Feishu session forever and every later message would queue behind it — a common cause of "stuck sessions".
|
|
366
366
|
|
|
@@ -388,7 +388,7 @@ Without this relay, any clarifying question would stall the Feishu session forev
|
|
|
388
388
|
| `recentModelsLimit` | number | `5` | Number of recent models (1–20) |
|
|
389
389
|
| `logLevel` | `debug`\|`info`\|`warn`\|`error` | `info` | Log level (secrets are never logged, only their presence) |
|
|
390
390
|
| `logFile` | string \| boolean | — | `true` writes `<configDir>/plugins/feishu.log`. **Plugin stderr is discarded in service mode — enable this when debugging** |
|
|
391
|
-
| `gatewayLocation` | string | — | Only start the gateway in this location
|
|
391
|
+
| `gatewayLocation` | string | — | Only start the gateway in this location **or any of its subdirectories**. `~` is expanded and relative paths / trailing slashes are normalized. **Set this to your usual working directory** to avoid multiple long connections; leave empty to run in every location |
|
|
392
392
|
| `approvalTtlMs` | number | `600000` | Approval token / card TTL |
|
|
393
393
|
| `staleExecutionMs` | number | `300000` | Watchdog threshold: an execution with no event for this long is treated as stuck and auto-interrupted; a queue stuck this long without `execution.started` also triggers a notice. Clamped to 1–60 minutes |
|
|
394
394
|
| `maxResourcesShown` | number | `8` | Max resource lines shown on an approval card |
|
|
@@ -400,6 +400,26 @@ Without this relay, any clarifying question would stall the Feishu session forev
|
|
|
400
400
|
| `topicStatusInTitle` | boolean | `false` | Add a status emoji prefix to the root card title (e.g. `🟡 session name`). Off by default: the topic name shows in the sidebar, and flipping it would be noisy |
|
|
401
401
|
| `topicStatusThrottleMs` | number | `1000` | Min root-card status refresh interval (clamped 500–10000); patched only when the kind changes |
|
|
402
402
|
| `cardMaxTables` | number | `4` | Max markdown tables kept per card (clamped 1–5); tables beyond it are degraded **cumulatively per card** into fenced code blocks (no content lost) to avoid Feishu 400 `code=230099` |
|
|
403
|
+
| `keepalive` | boolean | `true` | **Location keep-alive**: periodically emits activity so OpenCode does not evict the idle Location after 60 minutes (which unloads the plugin and closes the Feishu long connection) |
|
|
404
|
+
| `keepaliveIntervalMs` | number | `1200000` | Keep-alive interval (default 20 min, clamped 5–45); must stay well below OpenCode's hardcoded 60-minute TTL |
|
|
405
|
+
|
|
406
|
+
### Location keep-alive (on by default)
|
|
407
|
+
|
|
408
|
+
OpenCode **evicts idle Locations**, which unloads plugins and closes the Feishu long connection:
|
|
409
|
+
|
|
410
|
+
| Mechanism | Where | Trigger | Effect |
|
|
411
|
+
|---|---|---|---|
|
|
412
|
+
| LayerMap `idleTimeToLive` | `packages/core/src/location-services.ts` (hardcoded `60 minutes`) | no **session-scoped request** for 60 min | Location services destroyed (silently) |
|
|
413
|
+
| `@opencode/LocationActivity` | hardcoded 60 min as well | no **durable event carrying the location** for 60 min | interrupts active sessions, then `invalidate(location)`; logs `location services evicted` |
|
|
414
|
+
|
|
415
|
+
Both dispose the plugin (closing the Feishu WS). **After that, no request means no recovery — the bot stays silent permanently** (see issues [#51343](https://github.com/anomalyco/opencode/issues/51343), [#48691](https://github.com/anomalyco/opencode/issues/48691), [#51828](https://github.com/anomalyco/opencode/issues/51828); the TTL has no config knob).
|
|
416
|
+
|
|
417
|
+
The plugin ships a two-channel keep-alive (default every 20 min):
|
|
418
|
+
|
|
419
|
+
1. **session-scoped `GET /api/session/{id}`** → `locations.get()` renews the LayerMap entry; if it was already evicted, this request **re-creates the Location** (plugin reloaded, WS reconnected);
|
|
420
|
+
2. **create + immediately delete a probe session** → the `session.created` event renews `LocationActivity`.
|
|
421
|
+
|
|
422
|
+
> **External safety net**: a plugin cannot revive itself once evicted (its timer dies with it), and it is not loaded after a **service restart** either. Add a cron / systemd timer that performs one session-scoped GET every 15–30 minutes (see `scripts/keepalive-feishu.sh`) to also cover restarts and long sleeps.
|
|
403
423
|
|
|
404
424
|
---
|
|
405
425
|
|
|
@@ -440,7 +460,8 @@ otherwise (per session preset) → ask ─────────────
|
|
|
440
460
|
| `feishu.json` changes ignored | Confirm the path is `<configDir>/plugins/feishu.json`, then `opencode reload` |
|
|
441
461
|
| Plugin never loads (no logs, no error) | npm path: make sure the package name is in the config `plugins` array (`opencode plugin list` shows it). Directory path: make sure `plugins/<name>/index.js` exists (OpenCode ignores `package.json#main`) |
|
|
442
462
|
| Plugin code changes ignored | `opencode reload` only re-runs `setup`; it does **not** re-import the module from the same path. Upgrade with `opencode plugin update opencode-feishu-plugin`, or restart the service |
|
|
443
|
-
| Multiple long connections / duplicate replies | Set `gatewayLocation` to your usual working directory |
|
|
463
|
+
| Multiple long connections / duplicate replies | Set `gatewayLocation` to your usual working directory (subdirectories also match) |
|
|
464
|
+
| **No response at all**, and no "long connection started" / "plugin ready" in the log | Almost always `gatewayLocation` does not match the directory where you actually opened opencode. The plugin emits a `warn` about "no loaded location matched" after ~2s; you can also set `logLevel: "debug"` to see `skipping non-gateway location`. If still stuck, leave `gatewayLocation` empty to rule it out |
|
|
444
465
|
| No approval cards | The session did not originate from Feishu (no mapping); by design the plugin does not take it over |
|
|
445
466
|
| "Invalid credentials" on button tap | Token expired (default 10 min) or the tapper is not allow-listed |
|
|
446
467
|
| Card content truncated | Feishu card limit is ~30KB; the plugin truncates and marks it. Very long sessions drop the oldest blocks from the card (full content stays in the session) |
|
|
@@ -450,6 +471,7 @@ otherwise (per session preset) → ask ─────────────
|
|
|
450
471
|
| No plugin logs | Plugin stderr is discarded in service mode; set `logFile: true` and read `<configDir>/plugins/feishu.log` |
|
|
451
472
|
| Main chat replies with a hint card | Expected: the main chat is management-only. Use `/new` and work inside a topic; set `threadRouting: false` to revert |
|
|
452
473
|
| Session looks stuck and messages only queue | The watchdog auto-interrupts it after `staleExecutionMs` (default 5 min) and cancels the queue, then sends a notice card; you can also tap the card's "⏹ force stop" or send `/stop` |
|
|
474
|
+
| **Bot goes completely silent after ~1 hour idle** (no "long connection started" in the log) | OpenCode evicted the idle Location (hardcoded 60-min TTL). The built-in keep-alive is on by default; if it still happens (service restart / long sleep / keep-alive disabled), one session-scoped request re-creates it: `opencode api get /api/session/{id}`, or set up the external cron described under "Location keep-alive" |
|
|
453
475
|
| Switched `/model` but older messages still show the old model | Expected: a switch only affects **subsequent** replies; history keeps each message's model. The receipt / run-card footer / `/current` all show the read-back truth |
|
|
454
476
|
|
|
455
477
|
---
|
package/README.md
CHANGED
|
@@ -161,6 +161,7 @@ opencode mcp list # 顺带确认服务健康
|
|
|
161
161
|
|
|
162
162
|

|
|
163
163
|
|
|
164
|
+
- **数据源**:优先插件原生 `ctx.session.list()`(V2 运行时通常未暴露)→ **本机 HTTP `GET /api/session`**(与 opencode 同机,列出**全量**会话,含 TUI / Web 里开的)→ `SessionMap` 回退(仅机器人自己的会话)。进入外部会话时会补一条映射,审批 / 失败通知照常。
|
|
164
165
|
- 每条显示标题 / 短 id / 相对时间 / 是否已绑话题 / 目录,当前会话标「← 当前」;已绑话题的按钮显示「▶️ 再开」,其余为「▶️ 进入」;底部可翻页 + 「➕ 新建会话」。
|
|
165
166
|
- **「▶️ 进入话题」**:在主聊天流发一张恢复卡(含会话摘要),**直接回复这张卡**即续聊该历史会话。
|
|
166
167
|
- `/resume [序号]` 跳过列表直达,同一套「发恢复卡 → 回复即续聊」流程。
|
|
@@ -210,7 +211,12 @@ opencode mcp list # 顺带确认服务健康
|
|
|
210
211
|
|
|
211
212
|
### 表单 / 提问(`question` 工具)
|
|
212
213
|
|
|
213
|
-
agent 调 `question` 等 form
|
|
214
|
+
agent 调 `question` 等 form 类交互时,插件把它转成飞书卡片:
|
|
215
|
+
|
|
216
|
+
- **两种作答方式等价**:直接点选项按钮,或**直接在话题里发文字**(无需先点「✍️ 直接回复答案」)。文本会智能匹配——命中选项 label/value 用选项值,`boolean` 认「是/否、yes/no、1/0」,`number`/`integer` 转数值,多选按顿号/逗号拆分,其余视为**手动输入**。
|
|
217
|
+
- 多字段表单可以混合作答:点几个按钮 + 补一条文字,填满即自动提交。
|
|
218
|
+
- **作答 / 取消后卡片会被撤回**(不再残留待填卡);若超出飞书撤回时限,降级为「已提交 / 已取消」结果卡。
|
|
219
|
+
- **没有这层转发,agent 一反问飞书会话就会永久卡住**——这也是会话卡死的常见原因。
|
|
214
220
|
|
|
215
221
|
### 卡片内容守卫(表格超限降级)
|
|
216
222
|
|
|
@@ -243,7 +249,7 @@ agent 调 `question` 等 form 类交互时,插件把它转成飞书卡片:
|
|
|
243
249
|
| `recentDirsLimit` / `recentModelsLimit` | number | `5` | 表单「最近使用」条数(1–20) |
|
|
244
250
|
| `logLevel` | `debug`\|`info`\|`warn`\|`error` | `info` | 日志级别 |
|
|
245
251
|
| `logFile` | string \| boolean | — | `true` = 写 `<configDir>/plugins/feishu.log`;服务模式建议开启 |
|
|
246
|
-
| `gatewayLocation` | string | — | 只在该 location
|
|
252
|
+
| `gatewayLocation` | string | — | 只在该 location(或其**子目录**)启动网关;`~` 自动展开、相对路径/尾斜杠会归一化。留空 = 任意 location 生效 |
|
|
247
253
|
| `approvalTtlMs` | number | `600000` | 审批 token / 卡片有效期 |
|
|
248
254
|
| `staleExecutionMs` | number | `300000` | 看门狗阈值(夹取 1–60 分钟) |
|
|
249
255
|
| `maxResourcesShown` | number | `8` | 审批卡最多展示的资源行数 |
|
|
@@ -255,6 +261,8 @@ agent 调 `question` 等 form 类交互时,插件把它转成飞书卡片:
|
|
|
255
261
|
| `topicStatusInTitle` | boolean | `false` | 是否在根卡标题加状态 emoji 前缀 |
|
|
256
262
|
| `topicStatusThrottleMs` | number | `1000` | 根卡状态刷新最小间隔(500–10000) |
|
|
257
263
|
| `cardMaxTables` | number | `4` | 单卡最多保留的 markdown 表格数(1–5);超出按整卡累计降级为围栏代码块,避免飞书 400 `code=230099` |
|
|
264
|
+
| `keepalive` | boolean | `true` | **位置保活**:周期性向 opencode 发一次活动,阻止 60 分钟空闲回收 location(会关掉飞书长连接、机器人失联) |
|
|
265
|
+
| `keepaliveIntervalMs` | number | `1200000` | 保活间隔(默认 20 分钟,夹取 5–45);必须显著小于 opencode 硬编码的 60 分钟 TTL |
|
|
258
266
|
|
|
259
267
|
---
|
|
260
268
|
|
|
@@ -274,6 +282,24 @@ permission.evaluate (插件 hook) permission.asked (事件流)
|
|
|
274
282
|
- **三重单人边界**:可用范围「仅本人」+ 不申请群权限 + 代码层 open_id 白名单。
|
|
275
283
|
- **`always` 语义**:仅当请求带 `save[]` 时才持久化,否则等价于「允许一次」。
|
|
276
284
|
|
|
285
|
+
### 位置保活(防空闲失联,默认开启)
|
|
286
|
+
|
|
287
|
+
opencode 会**回收空闲的 location**,这会连带卸载插件、关闭飞书长连接:
|
|
288
|
+
|
|
289
|
+
| 机制 | 位置 | 触发条件 | 表现 |
|
|
290
|
+
|---|---|---|---|
|
|
291
|
+
| LayerMap `idleTimeToLive` | `packages/core/src/location-services.ts`(硬编码 `60 minutes`) | 60 分钟内无**会话级请求** | location 服务被销毁(静默) |
|
|
292
|
+
| `@opencode/LocationActivity` | 同为硬编码 60 分钟 | 60 分钟内无**带 location 的 durable 事件** | 先 interrupt 活动会话,再 `invalidate(location)`,日志 `location services evicted` |
|
|
293
|
+
|
|
294
|
+
两者都会让插件被 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 无配置项)。
|
|
295
|
+
|
|
296
|
+
插件内置双通道保活(默认每 20 分钟):
|
|
297
|
+
|
|
298
|
+
1. **会话级 `GET /api/session/{id}`** → `locations.get()` 续期 LayerMap;若已被回收,**该请求会重建 location**(插件重新加载、长连接重连);
|
|
299
|
+
2. **创建 + 立即删除探针会话** → `session.created` 事件续期 `LocationActivity`。
|
|
300
|
+
|
|
301
|
+
> **外部兜底建议**:插件自身被回收后无法自救(定时器随插件销毁),且**服务重启后**也不会自动加载。可再加一条 cron / systemd timer 每 15–30 分钟做一次「会话级 GET」作为唤起器(`keepalive-feishu.sh` 示例见仓库 `scripts/`),覆盖重启 / 长时间休眠场景。
|
|
302
|
+
|
|
277
303
|
---
|
|
278
304
|
|
|
279
305
|
## 六、故障排查
|
|
@@ -284,12 +310,14 @@ permission.evaluate (插件 hook) permission.asked (事件流)
|
|
|
284
310
|
| 改了 `feishu.json` 不生效 | 确认路径,然后 `opencode reload` |
|
|
285
311
|
| 插件完全没被加载 | npm 方式确认包名在 `plugins` 数组;目录方式确认 `plugins/<名>/index.js` 存在 |
|
|
286
312
|
| 改了插件代码不生效 | `opencode reload` 不会重新 import 同路径模块;用 `opencode plugin update` 或重启服务 |
|
|
287
|
-
| 出现多个长连接 / 重复回复 | 设置 `gatewayLocation`
|
|
313
|
+
| 出现多个长连接 / 重复回复 | 设置 `gatewayLocation` 为常用工作目录(其子目录也会命中) |
|
|
314
|
+
| **完全无响应**,且日志中没有任何「长连接已启动」/「飞书插件已就绪」 | 多半是 `gatewayLocation` 与实际打开 opencode 的目录不匹配。插件会在延迟约 2 秒后用 `warn` 打出「已加载的 location 均未命中」;也可临时设 `logLevel: "debug"` 查看 `跳过非网关 location`。确认无误仍无响应就先**留空** `gatewayLocation` 排除该项 |
|
|
288
315
|
| 审批卡收不到 | 该会话不是从飞书发起的(无映射),插件按设计不接管 |
|
|
289
316
|
| 点按钮提示凭证无效 | token 过期(默认 10 分钟)或点击者不在白名单 |
|
|
290
317
|
| 卡片内容被截断 | 飞书卡片上限约 30KB,超长会话丢弃卡片上最旧块(完整内容仍在会话里) |
|
|
291
318
|
| 建会话后没看到话题 | 表单卡会改写为「✅ 已创建 · …」并附手动创建话题指引 |
|
|
292
319
|
| 会话像卡死、发消息只排队 | 看门狗默认 5 分钟后自动中断;也可点「⏹ 强制停止」或发 `/stop` |
|
|
320
|
+
| **空闲约 1 小时后机器人完全失联**(日志无「长连接已启动」) | opencode 回收了空闲 location(60 分钟硬编码 TTL)。内置保活默认开启;若仍发生(服务重启 / 长休眠 / 保活被关),用 `opencode api get /api/session/{id}` 任一本地会话即可唤起,或按上方「位置保活」配置外部 cron |
|
|
293
321
|
| 看不到插件日志 | 服务模式下 stderr 被丢弃,设 `logFile: true` |
|
|
294
322
|
| 切了 `/model` 但历史还是旧模型 | 预期行为:切换只影响后续回复,历史消息保留各自当时的模型 |
|
|
295
323
|
|