opencode-feishu-plugin 0.1.4 → 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 +27 -4
- package/README.md +31 -1
- package/dist/index.js +354 -37
- 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
|
|
|
@@ -400,6 +400,28 @@ 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 two built-in layers (both on by default, **no external script required**):
|
|
418
|
+
|
|
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.
|
|
423
|
+
|
|
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.
|
|
403
425
|
|
|
404
426
|
---
|
|
405
427
|
|
|
@@ -451,6 +473,7 @@ otherwise (per session preset) → ask ─────────────
|
|
|
451
473
|
| No plugin logs | Plugin stderr is discarded in service mode; set `logFile: true` and read `<configDir>/plugins/feishu.log` |
|
|
452
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 |
|
|
453
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` |
|
|
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 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" |
|
|
454
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 |
|
|
455
478
|
|
|
456
479
|
---
|
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
|
|
|
@@ -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,27 @@ 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
|
+
插件内置两道防线(都是默认开启,**无需任何外部脚本**):
|
|
297
|
+
|
|
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 秒延迟,重启后恢复很快。
|
|
303
|
+
|
|
304
|
+
> 仍建议(可选,非必需):用 cron / systemd timer 每 15–30 分钟做一次「会话级 GET」,覆盖「opencode 进程整个挂掉且长时间无人使用」的极端场景。仓库提供 `scripts/keepalive-feishu.sh` 一键脚本;`keepalive: false` 可关闭全部保活。
|
|
305
|
+
|
|
277
306
|
---
|
|
278
307
|
|
|
279
308
|
## 六、故障排查
|
|
@@ -291,6 +320,7 @@ permission.evaluate (插件 hook) permission.asked (事件流)
|
|
|
291
320
|
| 卡片内容被截断 | 飞书卡片上限约 30KB,超长会话丢弃卡片上最旧块(完整内容仍在会话里) |
|
|
292
321
|
| 建会话后没看到话题 | 表单卡会改写为「✅ 已创建 · …」并附手动创建话题指引 |
|
|
293
322
|
| 会话像卡死、发消息只排队 | 看门狗默认 5 分钟后自动中断;也可点「⏹ 强制停止」或发 `/stop` |
|
|
323
|
+
| **空闲约 1 小时后机器人完全失联**(日志无「长连接已启动」) | opencode 回收了空闲 location(60 分钟硬编码 TTL)。内置保活默认开启;若仍发生(服务重启 / 长休眠 / 保活被关),用 `opencode api get /api/session/{id}` 任一本地会话即可唤起,或按上方「位置保活」配置外部 cron |
|
|
294
324
|
| 看不到插件日志 | 服务模式下 stderr 被丢弃,设 `logFile: true` |
|
|
295
325
|
| 切了 `/model` 但历史还是旧模型 | 预期行为:切换只影响后续回复,历史消息保留各自当时的模型 |
|
|
296
326
|
|
package/dist/index.js
CHANGED
|
@@ -144024,6 +144024,12 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
|
|
|
144024
144024
|
const topicStatusInTitle = asBoolean(merged.topicStatusInTitle, false);
|
|
144025
144025
|
const topicStatusThrottleMs = clamp4(asNumber(merged.topicStatusThrottleMs, 1e3), 500, 1e4);
|
|
144026
144026
|
const cardMaxTables = clamp4(asNumber(merged.cardMaxTables, 4), 1, 5);
|
|
144027
|
+
const keepalive = asBoolean(merged.keepalive, true);
|
|
144028
|
+
const keepaliveIntervalMs = clamp4(
|
|
144029
|
+
asNumber(merged.keepaliveIntervalMs, 20 * 60 * 1e3),
|
|
144030
|
+
5 * 60 * 1e3,
|
|
144031
|
+
45 * 60 * 1e3
|
|
144032
|
+
);
|
|
144027
144033
|
const domain = merged.domain === "lark" ? "lark" : "feishu";
|
|
144028
144034
|
const logFile = resolveLogFile(merged.logFile, env, deps);
|
|
144029
144035
|
const gatewayLocation = normalizeGatewayLocation(asString(merged.gatewayLocation));
|
|
@@ -144071,7 +144077,9 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
|
|
|
144071
144077
|
topicStatus,
|
|
144072
144078
|
topicStatusInTitle,
|
|
144073
144079
|
topicStatusThrottleMs,
|
|
144074
|
-
cardMaxTables
|
|
144080
|
+
cardMaxTables,
|
|
144081
|
+
keepalive,
|
|
144082
|
+
keepaliveIntervalMs
|
|
144075
144083
|
};
|
|
144076
144084
|
}
|
|
144077
144085
|
function loadConfigFile(env, deps, warnings) {
|
|
@@ -145125,10 +145133,18 @@ function createFeishuSender(client, log2, options2 = {}) {
|
|
|
145125
145133
|
}
|
|
145126
145134
|
},
|
|
145127
145135
|
async deleteMessage(messageId) {
|
|
145128
|
-
if (!messageId) return;
|
|
145136
|
+
if (!messageId) return { ok: false, error: "missing messageId" };
|
|
145129
145137
|
try {
|
|
145130
|
-
await client.im.message.delete({ path: { message_id: messageId } });
|
|
145131
|
-
|
|
145138
|
+
const res = await client.im.message.delete({ path: { message_id: messageId } });
|
|
145139
|
+
if (res?.code && res.code !== 0) {
|
|
145140
|
+
log2.debug("\u64A4\u56DE\u6D88\u606F\u5931\u8D25", { messageId, code: res.code, msg: res.msg });
|
|
145141
|
+
return { ok: false, error: `code=${res.code} msg=${res.msg ?? ""}` };
|
|
145142
|
+
}
|
|
145143
|
+
return { ok: true };
|
|
145144
|
+
} catch (err2) {
|
|
145145
|
+
const error = describeLarkError(err2);
|
|
145146
|
+
log2.debug("\u64A4\u56DE\u6D88\u606F\u5F02\u5E38", { messageId, error });
|
|
145147
|
+
return { ok: false, error };
|
|
145132
145148
|
}
|
|
145133
145149
|
}
|
|
145134
145150
|
};
|
|
@@ -145304,6 +145320,25 @@ var SessionMap = class {
|
|
|
145304
145320
|
await this.persist(chatId, record2);
|
|
145305
145321
|
await this.safeSet(`${SESSION_KEY_PREFIX}${sessionID}`, serializeSession(link2));
|
|
145306
145322
|
}
|
|
145323
|
+
/**
|
|
145324
|
+
* 为**外部来源**的会话(TUI/Web,无飞书映射)补一条会话索引(P7.5)。
|
|
145325
|
+
*
|
|
145326
|
+
* `/ls` 列出 opencode 全量会话后,用户「进入话题」的会话需要审批投递、
|
|
145327
|
+
* 跨 location 路由(`dir`)与失败通知的目标。已存在则只补缺失字段,
|
|
145328
|
+
* **不覆盖** perm/gateMode/model 等既有元数据。
|
|
145329
|
+
*/
|
|
145330
|
+
async ensureSessionLink(sessionID, input) {
|
|
145331
|
+
if (!sessionID || !input.chatId) return;
|
|
145332
|
+
const existing = await this.resolveBySession(sessionID);
|
|
145333
|
+
const link2 = {
|
|
145334
|
+
...existing,
|
|
145335
|
+
chatId: input.chatId,
|
|
145336
|
+
openId: input.openId,
|
|
145337
|
+
...input.directory ? { dir: input.directory } : {}
|
|
145338
|
+
};
|
|
145339
|
+
this.remember(sessionID, link2);
|
|
145340
|
+
await this.safeSet(`${SESSION_KEY_PREFIX}${sessionID}`, serializeSession(link2));
|
|
145341
|
+
}
|
|
145307
145342
|
/** 切换当前会话;会话不属于该 chat 时返回 false。 */
|
|
145308
145343
|
async setActive(chatId, sessionID) {
|
|
145309
145344
|
const record2 = await this.loadChat(chatId);
|
|
@@ -146813,7 +146848,7 @@ var FormRelay = class {
|
|
|
146813
146848
|
return false;
|
|
146814
146849
|
}
|
|
146815
146850
|
const field = pending.form.fields.find((f) => f.key === fieldKey);
|
|
146816
|
-
pending.answers[fieldKey] =
|
|
146851
|
+
pending.answers[fieldKey] = coerceAnswer(field, text);
|
|
146817
146852
|
pending.awaitingField = void 0;
|
|
146818
146853
|
this.awaiting.delete(sessionID);
|
|
146819
146854
|
if (isComplete(pending.form, pending.answers)) {
|
|
@@ -146837,7 +146872,7 @@ var FormRelay = class {
|
|
|
146837
146872
|
}
|
|
146838
146873
|
this.awaiting.delete(sessionID ?? pending.sessionID);
|
|
146839
146874
|
this.forms.delete(id2);
|
|
146840
|
-
void this.
|
|
146875
|
+
void this.finish(pending, "answered");
|
|
146841
146876
|
}
|
|
146842
146877
|
/** 处理 `form.cancelled`。 */
|
|
146843
146878
|
onCancelled(data) {
|
|
@@ -146847,7 +146882,7 @@ var FormRelay = class {
|
|
|
146847
146882
|
if (!pending) return;
|
|
146848
146883
|
this.awaiting.delete(sessionID ?? pending.sessionID);
|
|
146849
146884
|
this.forms.delete(id2);
|
|
146850
|
-
void this.
|
|
146885
|
+
void this.finish(pending, "cancelled");
|
|
146851
146886
|
}
|
|
146852
146887
|
dispose() {
|
|
146853
146888
|
this.disposed = true;
|
|
@@ -146868,6 +146903,13 @@ var FormRelay = class {
|
|
|
146868
146903
|
error: errorMessage(err2)
|
|
146869
146904
|
});
|
|
146870
146905
|
pending.settled = false;
|
|
146906
|
+
const missing2 = pending.form.fields.find(
|
|
146907
|
+
(f) => f.hidden !== true && pending.answers[f.key] === void 0
|
|
146908
|
+
);
|
|
146909
|
+
if (missing2) {
|
|
146910
|
+
pending.awaitingField = missing2.key;
|
|
146911
|
+
this.awaiting.set(pending.sessionID, pending.form.id);
|
|
146912
|
+
}
|
|
146871
146913
|
this.forms.set(pending.form.id, pending);
|
|
146872
146914
|
await this.patch(
|
|
146873
146915
|
pending,
|
|
@@ -146876,7 +146918,20 @@ var FormRelay = class {
|
|
|
146876
146918
|
return;
|
|
146877
146919
|
}
|
|
146878
146920
|
this.forms.delete(pending.form.id);
|
|
146879
|
-
await this.
|
|
146921
|
+
await this.finish(pending, "answered");
|
|
146922
|
+
}
|
|
146923
|
+
/**
|
|
146924
|
+
* 表单收敛:**优先撤回卡片**(作答/取消后不再残留待填卡),撤回失败(超时限/无权限)
|
|
146925
|
+
* 才降级 patch 成结果卡,避免卡片永久停在待填态。
|
|
146926
|
+
*/
|
|
146927
|
+
async finish(pending, outcome) {
|
|
146928
|
+
if (this.disposed) return;
|
|
146929
|
+
const res = await this.deps.sender.deleteMessage(pending.messageId);
|
|
146930
|
+
if (res.ok) {
|
|
146931
|
+
this.deps.log.debug("\u8868\u5355\u5361\u5DF2\u64A4\u56DE", { formID: pending.form.id, outcome });
|
|
146932
|
+
return;
|
|
146933
|
+
}
|
|
146934
|
+
await this.patch(pending, buildFormResolvedCard(pending.form, pending.answers, outcome));
|
|
146880
146935
|
}
|
|
146881
146936
|
async patch(pending, card) {
|
|
146882
146937
|
if (this.disposed) return;
|
|
@@ -146890,17 +146945,47 @@ function isFormValue(value3) {
|
|
|
146890
146945
|
if (typeof value3 === "string" || typeof value3 === "number" || typeof value3 === "boolean") return true;
|
|
146891
146946
|
return Array.isArray(value3) && value3.every((x) => typeof x === "string");
|
|
146892
146947
|
}
|
|
146893
|
-
function
|
|
146948
|
+
function coerceAnswer(field, text) {
|
|
146894
146949
|
const trimmed = text.trim();
|
|
146895
|
-
if (
|
|
146896
|
-
|
|
146897
|
-
|
|
146898
|
-
|
|
146899
|
-
|
|
146900
|
-
|
|
146901
|
-
|
|
146950
|
+
if (!field) return trimmed;
|
|
146951
|
+
switch (field.type) {
|
|
146952
|
+
case "number": {
|
|
146953
|
+
const n = Number(trimmed);
|
|
146954
|
+
return trimmed !== "" && Number.isFinite(n) ? n : trimmed;
|
|
146955
|
+
}
|
|
146956
|
+
case "integer": {
|
|
146957
|
+
const n = Number.parseInt(trimmed, 10);
|
|
146958
|
+
return Number.isFinite(n) && String(n) === trimmed ? n : trimmed;
|
|
146959
|
+
}
|
|
146960
|
+
case "boolean": {
|
|
146961
|
+
const parsed = parseBooleanText(trimmed);
|
|
146962
|
+
return parsed === void 0 ? trimmed : parsed;
|
|
146963
|
+
}
|
|
146964
|
+
case "multiselect": {
|
|
146965
|
+
const parts = trimmed.split(/[、,,;;\n]+/).map((s) => s.trim()).filter(Boolean);
|
|
146966
|
+
if (parts.length === 0) return trimmed;
|
|
146967
|
+
return parts.map((p) => matchOption(field, p) ?? p);
|
|
146968
|
+
}
|
|
146969
|
+
default:
|
|
146970
|
+
return matchOption(field, trimmed) ?? trimmed;
|
|
146902
146971
|
}
|
|
146903
|
-
|
|
146972
|
+
}
|
|
146973
|
+
function matchOption(field, text) {
|
|
146974
|
+
if (!field.options?.length) return void 0;
|
|
146975
|
+
const trimmed = text.trim();
|
|
146976
|
+
const lower = trimmed.toLowerCase();
|
|
146977
|
+
const hit = field.options.find(
|
|
146978
|
+
(o) => o.value === trimmed || o.label.trim() === trimmed || o.label.trim().toLowerCase() === lower
|
|
146979
|
+
);
|
|
146980
|
+
return hit?.value;
|
|
146981
|
+
}
|
|
146982
|
+
var TRUE_WORDS = /* @__PURE__ */ new Set(["\u662F", "\u5BF9", "\u597D", "\u53EF\u4EE5", "\u5141\u8BB8", "\u786E\u8BA4", "\u540C\u610F", "yes", "y", "true", "1"]);
|
|
146983
|
+
var FALSE_WORDS = /* @__PURE__ */ new Set(["\u5426", "\u4E0D", "\u4E0D\u884C", "\u62D2\u7EDD", "\u53D6\u6D88", "\u4E0D\u8981", "no", "n", "false", "0"]);
|
|
146984
|
+
function parseBooleanText(text) {
|
|
146985
|
+
const lower = text.trim().toLowerCase();
|
|
146986
|
+
if (TRUE_WORDS.has(lower)) return true;
|
|
146987
|
+
if (FALSE_WORDS.has(lower)) return false;
|
|
146988
|
+
return void 0;
|
|
146904
146989
|
}
|
|
146905
146990
|
function serviceStatePath(env = process.env) {
|
|
146906
146991
|
const base2 = env["XDG_STATE_HOME"] ?? join(homedir(), ".local", "state");
|
|
@@ -146960,6 +147045,173 @@ async function replyFormOverHttp(input, deps) {
|
|
|
146960
147045
|
}
|
|
146961
147046
|
}
|
|
146962
147047
|
|
|
147048
|
+
// src/session/session-list-http.ts
|
|
147049
|
+
async function listSessionsOverHttp(input, deps) {
|
|
147050
|
+
const discover = deps.discover ?? (() => discoverLocalService());
|
|
147051
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
147052
|
+
const service3 = await discover();
|
|
147053
|
+
if (!service3) {
|
|
147054
|
+
deps.log.debug("\u672A\u53D1\u73B0\u672C\u673A opencode \u670D\u52A1\uFF0CHTTP \u4F1A\u8BDD\u5217\u8868\u8DF3\u8FC7");
|
|
147055
|
+
return void 0;
|
|
147056
|
+
}
|
|
147057
|
+
const params = new URLSearchParams();
|
|
147058
|
+
params.set("limit", String(input.limit ?? 200));
|
|
147059
|
+
params.set("order", input.order ?? "desc");
|
|
147060
|
+
if (input.rootsOnly ?? true) params.set("parentID", "null");
|
|
147061
|
+
try {
|
|
147062
|
+
const response2 = await doFetch(`${service3.url}/api/session?${params.toString()}`, {
|
|
147063
|
+
method: "GET",
|
|
147064
|
+
headers: { ...authHeaders(service3) },
|
|
147065
|
+
signal: AbortSignal.timeout(deps.timeoutMs ?? 8e3)
|
|
147066
|
+
});
|
|
147067
|
+
if (!response2.ok) {
|
|
147068
|
+
deps.log.debug("HTTP \u4F1A\u8BDD\u5217\u8868\u5931\u8D25", { status: response2.status });
|
|
147069
|
+
return void 0;
|
|
147070
|
+
}
|
|
147071
|
+
return await response2.json();
|
|
147072
|
+
} catch (err2) {
|
|
147073
|
+
deps.log.debug("HTTP \u4F1A\u8BDD\u5217\u8868\u5F02\u5E38", { error: errorMessage(err2) });
|
|
147074
|
+
return void 0;
|
|
147075
|
+
}
|
|
147076
|
+
}
|
|
147077
|
+
|
|
147078
|
+
// src/session/keepalive.ts
|
|
147079
|
+
var KEEPALIVE_SESSION_TITLE = "__opencode_feishu_keepalive__";
|
|
147080
|
+
async function touchLocationOverHttp(directory, deps) {
|
|
147081
|
+
const discover = deps.discover ?? (() => discoverLocalService());
|
|
147082
|
+
const doFetch = deps.fetchImpl ?? fetch;
|
|
147083
|
+
const service3 = await discover();
|
|
147084
|
+
if (!service3) {
|
|
147085
|
+
deps.log.debug("\u4FDD\u6D3B\u8DF3\u8FC7\uFF1A\u672A\u53D1\u73B0\u672C\u673A opencode \u670D\u52A1");
|
|
147086
|
+
return false;
|
|
147087
|
+
}
|
|
147088
|
+
const headers = {
|
|
147089
|
+
"content-type": "application/json",
|
|
147090
|
+
...authHeaders(service3)
|
|
147091
|
+
};
|
|
147092
|
+
const timeout3 = deps.timeoutMs ?? 8e3;
|
|
147093
|
+
const signal = () => AbortSignal.timeout(timeout3);
|
|
147094
|
+
let touched = false;
|
|
147095
|
+
try {
|
|
147096
|
+
const params = new URLSearchParams({ limit: "1", parentID: "null" });
|
|
147097
|
+
if (directory) params.set("directory", directory);
|
|
147098
|
+
const listRes = await doFetch(`${service3.url}/api/session?${params.toString()}`, {
|
|
147099
|
+
method: "GET",
|
|
147100
|
+
headers,
|
|
147101
|
+
signal: signal()
|
|
147102
|
+
});
|
|
147103
|
+
if (listRes.ok) {
|
|
147104
|
+
const list = await listRes.json().catch(() => void 0);
|
|
147105
|
+
const target = list?.data?.[0]?.id;
|
|
147106
|
+
if (target) {
|
|
147107
|
+
const getRes = await doFetch(`${service3.url}/api/session/${encodeURIComponent(target)}`, {
|
|
147108
|
+
method: "GET",
|
|
147109
|
+
headers,
|
|
147110
|
+
signal: signal()
|
|
147111
|
+
});
|
|
147112
|
+
touched = getRes.ok;
|
|
147113
|
+
}
|
|
147114
|
+
}
|
|
147115
|
+
} catch (err2) {
|
|
147116
|
+
deps.log.debug("\u4FDD\u6D3B\u4F1A\u8BDD\u7EA7\u7EED\u671F\u5931\u8D25", { directory, error: errorMessage(err2) });
|
|
147117
|
+
}
|
|
147118
|
+
let sessionID;
|
|
147119
|
+
try {
|
|
147120
|
+
const response2 = await doFetch(`${service3.url}/api/session`, {
|
|
147121
|
+
method: "POST",
|
|
147122
|
+
headers,
|
|
147123
|
+
body: JSON.stringify({
|
|
147124
|
+
title: KEEPALIVE_SESSION_TITLE,
|
|
147125
|
+
...directory ? { location: { directory } } : {}
|
|
147126
|
+
}),
|
|
147127
|
+
signal: signal()
|
|
147128
|
+
});
|
|
147129
|
+
if (response2.ok) {
|
|
147130
|
+
const body = await response2.json().catch(() => void 0);
|
|
147131
|
+
sessionID = body?.data?.id;
|
|
147132
|
+
touched = true;
|
|
147133
|
+
} else {
|
|
147134
|
+
deps.log.debug("\u4FDD\u6D3B\u63A2\u9488\u521B\u5EFA\u5931\u8D25", { status: response2.status, directory });
|
|
147135
|
+
}
|
|
147136
|
+
} catch (err2) {
|
|
147137
|
+
deps.log.debug("\u4FDD\u6D3B\u63A2\u9488\u521B\u5EFA\u5F02\u5E38", { directory, error: errorMessage(err2) });
|
|
147138
|
+
}
|
|
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
|
+
}
|
|
147149
|
+
try {
|
|
147150
|
+
await doFetch(`${service3.url}/api/session/${encodeURIComponent(sessionID)}`, {
|
|
147151
|
+
method: "DELETE",
|
|
147152
|
+
headers,
|
|
147153
|
+
signal: signal()
|
|
147154
|
+
});
|
|
147155
|
+
} catch {
|
|
147156
|
+
}
|
|
147157
|
+
}
|
|
147158
|
+
if (touched) deps.log.debug("\u4FDD\u6D3B\u5FC3\u8DF3\u5DF2\u53D1\u9001", { directory, probe: sessionID ?? "(none)" });
|
|
147159
|
+
return touched;
|
|
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
|
+
}
|
|
147199
|
+
function startKeepalive(deps) {
|
|
147200
|
+
const setIntervalImpl = deps.setIntervalImpl ?? setInterval;
|
|
147201
|
+
const clearIntervalImpl = deps.clearIntervalImpl ?? clearInterval;
|
|
147202
|
+
const touch = deps.touch ?? ((dir) => touchLocationOverHttp(dir, { log: deps.log }));
|
|
147203
|
+
const timer = setIntervalImpl(() => {
|
|
147204
|
+
void touch(deps.directory).catch((err2) => {
|
|
147205
|
+
deps.log.debug("\u4FDD\u6D3B\u5FC3\u8DF3\u5F02\u5E38", { error: errorMessage(err2) });
|
|
147206
|
+
});
|
|
147207
|
+
}, deps.intervalMs);
|
|
147208
|
+
timer.unref?.();
|
|
147209
|
+
deps.log.info("\u4F4D\u7F6E\u4FDD\u6D3B\u5DF2\u542F\u52A8", { directory: deps.directory, intervalMs: deps.intervalMs });
|
|
147210
|
+
return () => {
|
|
147211
|
+
clearIntervalImpl(timer);
|
|
147212
|
+
};
|
|
147213
|
+
}
|
|
147214
|
+
|
|
146963
147215
|
// src/feishu/commands.ts
|
|
146964
147216
|
var ALIASES = {
|
|
146965
147217
|
new: "new",
|
|
@@ -149452,28 +149704,53 @@ async function cmdSessions(ctx, message, page = 0) {
|
|
|
149452
149704
|
}
|
|
149453
149705
|
}
|
|
149454
149706
|
async function loadSessionEntries(ctx, chatId) {
|
|
149455
|
-
|
|
149456
|
-
|
|
149457
|
-
|
|
149458
|
-
|
|
149459
|
-
if (normalized) {
|
|
149460
|
-
ctx.deps.log.info("\u5217\u51FA\u5168\u90E8\u4F1A\u8BDD", { count: normalized.length, source: "session.list" });
|
|
149461
|
-
return normalized;
|
|
149462
|
-
}
|
|
149463
|
-
ctx.deps.log.warn("session.list \u8FD4\u56DE\u5F62\u72B6\u65E0\u6CD5\u8BC6\u522B\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", { chatId });
|
|
149464
|
-
} catch (err2) {
|
|
149465
|
-
ctx.deps.log.warn("session.list \u8C03\u7528\u5931\u8D25\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", {
|
|
149466
|
-
chatId,
|
|
149467
|
-
error: errorMessage(err2)
|
|
149468
|
-
});
|
|
149469
|
-
}
|
|
149470
|
-
} else {
|
|
149471
|
-
ctx.deps.log.warn("\u8FD0\u884C\u65F6\u672A\u66B4\u9732 session.list\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", { chatId });
|
|
149472
|
-
}
|
|
149707
|
+
const viaApi = await loadViaSessionListApi(ctx, chatId);
|
|
149708
|
+
if (viaApi) return viaApi;
|
|
149709
|
+
const viaHttp = await loadViaHttp(ctx, chatId);
|
|
149710
|
+
if (viaHttp) return viaHttp;
|
|
149473
149711
|
const fallback = fallbackEntries(await ctx.deps.sessionMap.listSessions(chatId));
|
|
149474
149712
|
ctx.deps.log.warn("\u4F1A\u8BDD\u5217\u8868\u56DE\u9000\u5230 SessionMap", { chatId, count: fallback.length });
|
|
149475
149713
|
return fallback;
|
|
149476
149714
|
}
|
|
149715
|
+
async function loadViaSessionListApi(ctx, chatId) {
|
|
149716
|
+
if (!ctx.deps.listAllSessions) {
|
|
149717
|
+
ctx.deps.log.warn("\u8FD0\u884C\u65F6\u672A\u66B4\u9732 session.list\uFF0C\u5C1D\u8BD5 HTTP \u515C\u5E95", { chatId });
|
|
149718
|
+
return void 0;
|
|
149719
|
+
}
|
|
149720
|
+
try {
|
|
149721
|
+
const raw = await ctx.deps.listAllSessions();
|
|
149722
|
+
const normalized = normalizeSessionList(raw);
|
|
149723
|
+
if (normalized) {
|
|
149724
|
+
ctx.deps.log.info("\u5217\u51FA\u5168\u90E8\u4F1A\u8BDD", { count: normalized.length, source: "session.list" });
|
|
149725
|
+
return normalized;
|
|
149726
|
+
}
|
|
149727
|
+
ctx.deps.log.warn("session.list \u8FD4\u56DE\u5F62\u72B6\u65E0\u6CD5\u8BC6\u522B\uFF0C\u5C1D\u8BD5 HTTP \u515C\u5E95", { chatId });
|
|
149728
|
+
} catch (err2) {
|
|
149729
|
+
ctx.deps.log.warn("session.list \u8C03\u7528\u5931\u8D25\uFF0C\u5C1D\u8BD5 HTTP \u515C\u5E95", {
|
|
149730
|
+
chatId,
|
|
149731
|
+
error: errorMessage(err2)
|
|
149732
|
+
});
|
|
149733
|
+
}
|
|
149734
|
+
return void 0;
|
|
149735
|
+
}
|
|
149736
|
+
async function loadViaHttp(ctx, chatId) {
|
|
149737
|
+
if (!ctx.deps.listAllSessionsHttp) return void 0;
|
|
149738
|
+
try {
|
|
149739
|
+
const raw = await ctx.deps.listAllSessionsHttp();
|
|
149740
|
+
const normalized = normalizeSessionList(raw);
|
|
149741
|
+
if (normalized) {
|
|
149742
|
+
ctx.deps.log.info("\u5217\u51FA\u5168\u90E8\u4F1A\u8BDD", { count: normalized.length, source: "http" });
|
|
149743
|
+
return normalized;
|
|
149744
|
+
}
|
|
149745
|
+
ctx.deps.log.warn("HTTP \u4F1A\u8BDD\u5217\u8868\u5F62\u72B6\u65E0\u6CD5\u8BC6\u522B\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", { chatId });
|
|
149746
|
+
} catch (err2) {
|
|
149747
|
+
ctx.deps.log.warn("HTTP \u4F1A\u8BDD\u5217\u8868\u8C03\u7528\u5931\u8D25\uFF0C\u56DE\u9000 SessionMap \u5217\u8868", {
|
|
149748
|
+
chatId,
|
|
149749
|
+
error: errorMessage(err2)
|
|
149750
|
+
});
|
|
149751
|
+
}
|
|
149752
|
+
return void 0;
|
|
149753
|
+
}
|
|
149477
149754
|
async function buildListCard(ctx, chatId, entries3, page, activeID) {
|
|
149478
149755
|
const pageSize = ctx.sessionPageSize;
|
|
149479
149756
|
const pageCount = Math.max(1, Math.ceil(entries3.length / pageSize));
|
|
@@ -149510,6 +149787,13 @@ async function enterSessionThread(ctx, input) {
|
|
|
149510
149787
|
const info = input.info;
|
|
149511
149788
|
const link2 = await ctx.deps.sessionMap.resolveBySession(sessionID);
|
|
149512
149789
|
const dir = info?.directory ?? link2?.dir;
|
|
149790
|
+
if (!link2) {
|
|
149791
|
+
await ctx.deps.sessionMap.ensureSessionLink(sessionID, {
|
|
149792
|
+
chatId,
|
|
149793
|
+
openId: operatorOpenId,
|
|
149794
|
+
...dir ? { directory: dir } : {}
|
|
149795
|
+
});
|
|
149796
|
+
}
|
|
149513
149797
|
const showSummary = (ctx.deps.resumeSummary ?? true) && Boolean(ctx.deps.summarizeSession);
|
|
149514
149798
|
const onLimit = (report) => {
|
|
149515
149799
|
ctx.deps.log.warn("\u4F1A\u8BDD\u6062\u590D\u5361\u5185\u5BB9\u8D85\u9650\uFF0C\u5DF2\u964D\u7EA7", { sessionID, ...report });
|
|
@@ -150680,24 +150964,37 @@ var index_default = plugin_exports.define({
|
|
|
150680
150964
|
hasAppSecret: hasSecret(config.appSecret)
|
|
150681
150965
|
});
|
|
150682
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
|
+
}
|
|
150683
150978
|
if (config.gatewayLocation) {
|
|
150684
150979
|
const matched = Boolean(here) && isUnder(here, config.gatewayLocation);
|
|
150980
|
+
const keepSink = matched || startedWatchdog;
|
|
150685
150981
|
trackGatewayLocationSeen({
|
|
150686
150982
|
here: here ?? "(unknown)",
|
|
150687
150983
|
expected: config.gatewayLocation,
|
|
150688
150984
|
matched,
|
|
150689
150985
|
warn: (message) => log2.warn(message),
|
|
150690
|
-
...
|
|
150986
|
+
...keepSink ? {} : { onSettled: () => logSink?.close() }
|
|
150691
150987
|
});
|
|
150692
150988
|
if (!matched) {
|
|
150693
150989
|
log2.debug("\u8DF3\u8FC7\u975E\u7F51\u5173 location", { here, expected: config.gatewayLocation });
|
|
150990
|
+
if (!keepSink) logSink?.close();
|
|
150694
150991
|
return async () => {
|
|
150695
150992
|
};
|
|
150696
150993
|
}
|
|
150697
150994
|
}
|
|
150698
150995
|
if (!acquireProcessGuard()) {
|
|
150699
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");
|
|
150700
|
-
logSink?.close();
|
|
150997
|
+
if (!startedWatchdog) logSink?.close();
|
|
150701
150998
|
return async () => {
|
|
150702
150999
|
};
|
|
150703
151000
|
}
|
|
@@ -151009,6 +151306,9 @@ async function start(ctx, config, log2, logSink) {
|
|
|
151009
151306
|
sessionPageSize: config.sessionPageSize,
|
|
151010
151307
|
threadRouting: config.threadRouting,
|
|
151011
151308
|
listAllSessions: listAllSessionsRaw,
|
|
151309
|
+
// `ctx.session.list` 在 V2 运行时未暴露 → 本机 HTTP `GET /api/session`
|
|
151310
|
+
// 兜底列出全量会话(含 TUI/Web 来源)。
|
|
151311
|
+
listAllSessionsHttp: () => listSessionsOverHttp({}, { log: log2 }),
|
|
151012
151312
|
getSessionInfo: getSessionInfoRaw,
|
|
151013
151313
|
resumeSummary: config.resumeSummary,
|
|
151014
151314
|
resumeSummaryTimeoutMs: config.resumeSummaryTimeoutMs,
|
|
@@ -151245,6 +151545,22 @@ async function start(ctx, config, log2, logSink) {
|
|
|
151245
151545
|
const res = link2.replyMessageId ? await sender.replyCard(link2.replyMessageId, card) : await sender.sendCard(link2.chatId, card);
|
|
151246
151546
|
if (!res.ok) log2.warn("\u5361\u6B7B\u63D0\u793A\u5361\u53D1\u9001\u5931\u8D25", { sessionID, error: res.error ?? "unknown" });
|
|
151247
151547
|
}
|
|
151548
|
+
const here = ctx.location?.directory;
|
|
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
|
+
}
|
|
151559
|
+
const stopKeepalive = config.keepalive && keepaliveDirectory ? startKeepalive({
|
|
151560
|
+
log: log2,
|
|
151561
|
+
directory: keepaliveDirectory,
|
|
151562
|
+
intervalMs: config.keepaliveIntervalMs
|
|
151563
|
+
}) : void 0;
|
|
151248
151564
|
log2.info("\u98DE\u4E66\u63D2\u4EF6\u5DF2\u5C31\u7EEA");
|
|
151249
151565
|
let cleanedUp = false;
|
|
151250
151566
|
return async () => {
|
|
@@ -151253,6 +151569,7 @@ async function start(ctx, config, log2, logSink) {
|
|
|
151253
151569
|
log2.info("\u98DE\u4E66\u63D2\u4EF6\u5378\u8F7D\u4E2D");
|
|
151254
151570
|
abort.abort();
|
|
151255
151571
|
stopWatchdog();
|
|
151572
|
+
stopKeepalive?.();
|
|
151256
151573
|
await subscription.catch(() => void 0);
|
|
151257
151574
|
runs.dispose();
|
|
151258
151575
|
executions.clear();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-feishu-plugin",
|
|
3
|
-
"version": "0.1
|
|
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",
|