opencode-feishu-plugin 0.2.0 → 0.2.1
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 +6 -4
- package/README.md +7 -4
- 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
|
+
> Still recommended (optional, not required): a cron / systemd timer doing one session-scoped GET every 15–30 minutes, covering the extreme case where the opencode process is down and nothing is used for a long time. See `scripts/keepalive-feishu.sh`. Set `keepalive: false` to disable all keep-alive.
|
|
423
425
|
|
|
424
426
|
---
|
|
425
427
|
|
package/README.md
CHANGED
|
@@ -293,12 +293,15 @@ 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 timer 每 15–30 分钟做一次「会话级 GET」,覆盖「opencode 进程整个挂掉且长时间无人使用」的极端场景。仓库提供 `scripts/keepalive-feishu.sh` 一键脚本;`keepalive: false` 可关闭全部保活。
|
|
302
305
|
|
|
303
306
|
---
|
|
304
307
|
|
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.1",
|
|
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",
|