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 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 a two-channel keep-alive (default every 20 min):
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}`** → `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`.
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
- > **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.
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
- 插件内置双通道保活(默认每 20 分钟):
296
+ 插件内置两道防线(都是默认开启,**无需任何外部脚本**):
297
297
 
298
- 1. **会话级 `GET /api/session/{id}`** → `locations.get()` 续期 LayerMap;若已被回收,**该请求会重建 location**(插件重新加载、长连接重连);
299
- 2. **创建 + 立即删除探针会话** → `session.created` 事件续期 `LocationActivity`。
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
- > **外部兜底建议**:插件自身被回收后无法自救(定时器随插件销毁),且**服务重启后**也不会自动加载。可再加一条 cron / systemd timer 每 15–30 分钟做一次「会话级 GET」作为唤起器(`keepalive-feishu.sh` 示例见仓库 `scripts/`),覆盖重启 / 长时间休眠场景。
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
- ...matched ? {} : { onSettled: () => logSink?.close() }
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.0",
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",