opencode-feishu-plugin 0.2.0 → 0.2.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.
- package/README.en.md +7 -5
- package/README.md +10 -5
- package/dist/index.js +71 -2
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -414,12 +414,14 @@ OpenCode **evicts idle Locations**, which unloads plugins and closes the Feishu
|
|
|
414
414
|
|
|
415
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
416
|
|
|
417
|
-
The plugin ships
|
|
417
|
+
The plugin ships two built-in layers (both on by default, **no external script required**):
|
|
418
418
|
|
|
419
|
-
1. **session-scoped `GET /api/session/{id}
|
|
420
|
-
2. **
|
|
419
|
+
1. **Gateway keep-alive** (every 20 min, `keepaliveIntervalMs`): ① a session-scoped `GET /api/session/{id}` → `locations.get()` renews the LayerMap entry, and **re-creates the Location** if it was evicted; ② create + GET + delete a probe session → renews `LocationActivity` (via the `session.created` event).
|
|
420
|
+
2. **Process-wide gateway watchdog** (same interval, exactly one timer per process): every Location's plugin instance registers it, and it performs a session-scoped GET against the gateway Location. Result:
|
|
421
|
+
- if the gateway instance was evicted, it is revived automatically as long as **any other Location** still has a loaded instance (e.g. you have a TUI/Web open in another project);
|
|
422
|
+
- after a **service restart**, the first use of any Location starts the watchdog, which fires an immediate probe after ~3s and brings the gateway back up.
|
|
421
423
|
|
|
422
|
-
> **
|
|
424
|
+
> **No external script / cron / systemd setup is required**: both layers run inside the plugin process. The only case neither can cover is "the opencode process is fully down and nothing is used for a long time" — no plugin can run then; the watchdog restores the gateway as soon as opencode is used again. Set `keepalive: false` to disable all keep-alive.
|
|
423
425
|
|
|
424
426
|
---
|
|
425
427
|
|
|
@@ -471,7 +473,7 @@ otherwise (per session preset) → ask ─────────────
|
|
|
471
473
|
| No plugin logs | Plugin stderr is discarded in service mode; set `logFile: true` and read `<configDir>/plugins/feishu.log` |
|
|
472
474
|
| 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 |
|
|
473
475
|
| 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
|
|
476
|
+
| **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 + process-wide watchdog are on by default and restore it automatically; you can also force it manually with one session-scoped request: `opencode api get /api/session/{id}`. If it never recovers, check that `keepalive` is not set to `false` |
|
|
475
477
|
| 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 |
|
|
476
478
|
|
|
477
479
|
---
|
package/README.md
CHANGED
|
@@ -293,12 +293,17 @@ opencode 会**回收空闲的 location**,这会连带卸载插件、关闭飞
|
|
|
293
293
|
|
|
294
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
295
|
|
|
296
|
-
|
|
296
|
+
插件内置两道防线(都是默认开启,**无需任何外部脚本**):
|
|
297
297
|
|
|
298
|
-
1.
|
|
299
|
-
2.
|
|
298
|
+
1. **网关保活**(每 20 分钟,`keepaliveIntervalMs`):① 会话级 `GET /api/session/{id}` → `locations.get()` 续期 LayerMap,若已被回收则**重建 location**;② 创建 + GET + 删除一个探针会话 → 续期 `LocationActivity`(`session.created` 事件)。
|
|
299
|
+
2. **进程级网关看门狗**(同样每 20 分钟,每进程仅一个定时器):**任意** location 的插件实例都会登记,周期性对网关 location 做会话级 GET。效果:
|
|
300
|
+
- 网关实例即使已被回收,只要进程里还有**别的** location 存活(例如你在别的项目里开了 TUI/Web),网关会被自动救活;
|
|
301
|
+
- **服务重启后**,你第一次使用任意 location 时看门狗即启动(并在约 3 秒后立即探测一次),网关随之上线;
|
|
302
|
+
- 首次探测带 3 秒延迟,重启后恢复很快。
|
|
300
303
|
|
|
301
|
-
>
|
|
304
|
+
> **不需要任何外部脚本 / cron / systemd 配置**:以上两道防线都在插件进程内完成。
|
|
305
|
+
> 唯一无法覆盖的是「opencode 进程整个挂掉且长时间无人使用」——此时任何插件都无从执行;
|
|
306
|
+
> 重新使用 opencode 时会由看门狗自动恢复。`keepalive: false` 可关闭全部保活。
|
|
302
307
|
|
|
303
308
|
---
|
|
304
309
|
|
|
@@ -317,7 +322,7 @@ opencode 会**回收空闲的 location**,这会连带卸载插件、关闭飞
|
|
|
317
322
|
| 卡片内容被截断 | 飞书卡片上限约 30KB,超长会话丢弃卡片上最旧块(完整内容仍在会话里) |
|
|
318
323
|
| 建会话后没看到话题 | 表单卡会改写为「✅ 已创建 · …」并附手动创建话题指引 |
|
|
319
324
|
| 会话像卡死、发消息只排队 | 看门狗默认 5 分钟后自动中断;也可点「⏹ 强制停止」或发 `/stop` |
|
|
320
|
-
| **空闲约 1 小时后机器人完全失联**(日志无「长连接已启动」) | opencode 回收了空闲 location(60 分钟硬编码 TTL
|
|
325
|
+
| **空闲约 1 小时后机器人完全失联**(日志无「长连接已启动」) | opencode 回收了空闲 location(60 分钟硬编码 TTL)。内置保活 + 进程级看门狗默认开启,会自动恢复;也可手动用 `opencode api get /api/session/{id}`(任一本地会话)立即唤起。若长期不恢复,确认 `keepalive` 未被设为 `false` |
|
|
321
326
|
| 看不到插件日志 | 服务模式下 stderr 被丢弃,设 `logFile: true` |
|
|
322
327
|
| 切了 `/model` 但历史还是旧模型 | 预期行为:切换只影响后续回复,历史消息保留各自当时的模型 |
|
|
323
328
|
|
package/dist/index.js
CHANGED
|
@@ -147137,6 +147137,15 @@ async function touchLocationOverHttp(directory, deps) {
|
|
|
147137
147137
|
deps.log.debug("\u4FDD\u6D3B\u63A2\u9488\u521B\u5EFA\u5F02\u5E38", { directory, error: errorMessage(err2) });
|
|
147138
147138
|
}
|
|
147139
147139
|
if (sessionID) {
|
|
147140
|
+
try {
|
|
147141
|
+
const getRes = await doFetch(`${service3.url}/api/session/${encodeURIComponent(sessionID)}`, {
|
|
147142
|
+
method: "GET",
|
|
147143
|
+
headers,
|
|
147144
|
+
signal: signal()
|
|
147145
|
+
});
|
|
147146
|
+
touched = touched || getRes.ok;
|
|
147147
|
+
} catch {
|
|
147148
|
+
}
|
|
147140
147149
|
try {
|
|
147141
147150
|
await doFetch(`${service3.url}/api/session/${encodeURIComponent(sessionID)}`, {
|
|
147142
147151
|
method: "DELETE",
|
|
@@ -147149,6 +147158,44 @@ async function touchLocationOverHttp(directory, deps) {
|
|
|
147149
147158
|
if (touched) deps.log.debug("\u4FDD\u6D3B\u5FC3\u8DF3\u5DF2\u53D1\u9001", { directory, probe: sessionID ?? "(none)" });
|
|
147150
147159
|
return touched;
|
|
147151
147160
|
}
|
|
147161
|
+
var WATCHDOG_SLOT = /* @__PURE__ */ Symbol.for("opencode-feishu-v2/gateway-watchdog");
|
|
147162
|
+
function ensureGatewayWatchdog(input) {
|
|
147163
|
+
const g = globalThis;
|
|
147164
|
+
const existing = g[WATCHDOG_SLOT];
|
|
147165
|
+
if (existing) {
|
|
147166
|
+
if (input.authoritative && existing.target !== input.directory) {
|
|
147167
|
+
existing.target = input.directory;
|
|
147168
|
+
input.log.debug("\u7F51\u5173\u770B\u95E8\u72D7\u76EE\u6807\u66F4\u65B0\u4E3A\u7F51\u5173 location", { directory: input.directory });
|
|
147169
|
+
}
|
|
147170
|
+
return false;
|
|
147171
|
+
}
|
|
147172
|
+
const setIntervalImpl = input.setIntervalImpl ?? setInterval;
|
|
147173
|
+
input.clearIntervalImpl ?? clearInterval;
|
|
147174
|
+
const setTimeoutImpl = input.setTimeoutImpl ?? setTimeout;
|
|
147175
|
+
input.clearTimeoutImpl ?? clearTimeout;
|
|
147176
|
+
const probe = input.probe ?? ((dir) => touchLocationOverHttp(dir, { log: input.log }));
|
|
147177
|
+
const slot = {
|
|
147178
|
+
target: input.directory,
|
|
147179
|
+
timer: void 0,
|
|
147180
|
+
initial: void 0
|
|
147181
|
+
};
|
|
147182
|
+
const run2 = () => {
|
|
147183
|
+
void probe(slot.target).catch((err2) => {
|
|
147184
|
+
input.log.debug("\u7F51\u5173\u770B\u95E8\u72D7\u63A2\u6D4B\u5F02\u5E38", { error: errorMessage(err2) });
|
|
147185
|
+
});
|
|
147186
|
+
};
|
|
147187
|
+
const delay3 = input.immediateDelayMs ?? 3e3;
|
|
147188
|
+
if (delay3 > 0) slot.initial = setTimeoutImpl(run2, delay3);
|
|
147189
|
+
slot.timer = setIntervalImpl(run2, input.intervalMs);
|
|
147190
|
+
slot.timer.unref?.();
|
|
147191
|
+
slot.initial?.unref?.();
|
|
147192
|
+
g[WATCHDOG_SLOT] = slot;
|
|
147193
|
+
input.log.info("\u7F51\u5173\u770B\u95E8\u72D7\u5DF2\u542F\u52A8\uFF08\u8FDB\u7A0B\u7EA7\uFF09", {
|
|
147194
|
+
directory: input.directory,
|
|
147195
|
+
intervalMs: input.intervalMs
|
|
147196
|
+
});
|
|
147197
|
+
return true;
|
|
147198
|
+
}
|
|
147152
147199
|
function startKeepalive(deps) {
|
|
147153
147200
|
const setIntervalImpl = deps.setIntervalImpl ?? setInterval;
|
|
147154
147201
|
const clearIntervalImpl = deps.clearIntervalImpl ?? clearInterval;
|
|
@@ -150917,24 +150964,37 @@ var index_default = plugin_exports.define({
|
|
|
150917
150964
|
hasAppSecret: hasSecret(config.appSecret)
|
|
150918
150965
|
});
|
|
150919
150966
|
const here = ctx.location?.directory;
|
|
150967
|
+
let startedWatchdog = false;
|
|
150968
|
+
if (config.keepalive) {
|
|
150969
|
+
const watchdogTarget = config.gatewayLocation ?? here;
|
|
150970
|
+
if (watchdogTarget) {
|
|
150971
|
+
startedWatchdog = ensureGatewayWatchdog({
|
|
150972
|
+
log: log2,
|
|
150973
|
+
directory: watchdogTarget,
|
|
150974
|
+
intervalMs: config.keepaliveIntervalMs
|
|
150975
|
+
});
|
|
150976
|
+
}
|
|
150977
|
+
}
|
|
150920
150978
|
if (config.gatewayLocation) {
|
|
150921
150979
|
const matched = Boolean(here) && isUnder(here, config.gatewayLocation);
|
|
150980
|
+
const keepSink = matched || startedWatchdog;
|
|
150922
150981
|
trackGatewayLocationSeen({
|
|
150923
150982
|
here: here ?? "(unknown)",
|
|
150924
150983
|
expected: config.gatewayLocation,
|
|
150925
150984
|
matched,
|
|
150926
150985
|
warn: (message) => log2.warn(message),
|
|
150927
|
-
...
|
|
150986
|
+
...keepSink ? {} : { onSettled: () => logSink?.close() }
|
|
150928
150987
|
});
|
|
150929
150988
|
if (!matched) {
|
|
150930
150989
|
log2.debug("\u8DF3\u8FC7\u975E\u7F51\u5173 location", { here, expected: config.gatewayLocation });
|
|
150990
|
+
if (!keepSink) logSink?.close();
|
|
150931
150991
|
return async () => {
|
|
150932
150992
|
};
|
|
150933
150993
|
}
|
|
150934
150994
|
}
|
|
150935
150995
|
if (!acquireProcessGuard()) {
|
|
150936
150996
|
log2.debug("\u68C0\u6D4B\u5230\u540C\u8FDB\u7A0B\u91CD\u590D setup\uFF0C\u8DF3\u8FC7\u542F\u52A8\uFF08\u4EC5\u9996\u4E2A\u5B9E\u4F8B\u751F\u6548\uFF09");
|
|
150937
|
-
logSink?.close();
|
|
150997
|
+
if (!startedWatchdog) logSink?.close();
|
|
150938
150998
|
return async () => {
|
|
150939
150999
|
};
|
|
150940
151000
|
}
|
|
@@ -151487,6 +151547,15 @@ async function start(ctx, config, log2, logSink) {
|
|
|
151487
151547
|
}
|
|
151488
151548
|
const here = ctx.location?.directory;
|
|
151489
151549
|
const keepaliveDirectory = here ?? config.gatewayLocation;
|
|
151550
|
+
if (config.keepalive && keepaliveDirectory) {
|
|
151551
|
+
ensureGatewayWatchdog({
|
|
151552
|
+
log: log2,
|
|
151553
|
+
directory: keepaliveDirectory,
|
|
151554
|
+
intervalMs: config.keepaliveIntervalMs,
|
|
151555
|
+
authoritative: true,
|
|
151556
|
+
immediateDelayMs: 0
|
|
151557
|
+
});
|
|
151558
|
+
}
|
|
151490
151559
|
const stopKeepalive = config.keepalive && keepaliveDirectory ? startKeepalive({
|
|
151491
151560
|
log: log2,
|
|
151492
151561
|
directory: keepaliveDirectory,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-feishu-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Feishu/Lark plugin for OpenCode V2 — single-user topics as sessions, in-card permission approvals, streaming cards. Minimal permissions, no public endpoint.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|