opencode-feishu-plugin 0.2.11 → 0.2.12
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 +2 -2
- package/README.md +2 -2
- package/dist/index.js +93 -12
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -173,7 +173,7 @@ When the agent asks via `question` or other form tools, the form becomes a Feish
|
|
|
173
173
|
| `logLevel` | `debug`\|`info`\|`warn`\|`error` | `info` | Log level |
|
|
174
174
|
| `logFile` | string \| boolean | — | `true` = write `<configDir>/plugins/feishu.log`; recommended in server mode |
|
|
175
175
|
| `approvalTtlMs` | number | `600000` | Approval token / card validity |
|
|
176
|
-
| `staleExecutionMs` | number | `300000` | Watchdog threshold (
|
|
176
|
+
| `staleExecutionMs` | number | `300000` | Watchdog threshold (0–60 min; **0 = disabled**; sessions waiting on a form / pending approval are never killed) |
|
|
177
177
|
| `gatewayLocation` | string | — | Only start the gateway at this location (or its subdirectories); empty = any location |
|
|
178
178
|
|
|
179
179
|
Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepalive*`, `gatewayMatchGraceMs`) → [docs/advanced.en.md](./docs/advanced.en.md#full-configuration).
|
|
@@ -188,7 +188,7 @@ Full config (including `cardMaxTables`, `topicStatus*`, `resumeSummary*`, `keepa
|
|
|
188
188
|
| Plugin not loaded at all | npm: confirm the package name is in the `plugins` array; directory: confirm `plugins/<name>/index.js` exists |
|
|
189
189
|
| No approval cards | That session wasn't started from Feishu (no mapping); by design the plugin doesn't take it over |
|
|
190
190
|
| Button click says invalid credential | Token expired (10 min default) or clicker not in allowlist |
|
|
191
|
-
| Session seems stuck, messages only queue | Watchdog auto-interrupts after 5 min; or tap "⏹ force stop" / send `/stop` |
|
|
191
|
+
| Session seems stuck, messages only queue | Watchdog auto-interrupts after 5 min (never while a form / approval is pending; `staleExecutionMs: 0` disables it); or tap "⏹ force stop" / send `/stop` |
|
|
192
192
|
| Bot goes silent after ~1h idle | opencode recycles idle locations; built-in keepalive restores automatically, see [docs/advanced.en.md](./docs/advanced.en.md#location-keep-alive) |
|
|
193
193
|
| Can't see plugin logs | stderr is discarded in server mode; set `logFile: true` |
|
|
194
194
|
| Multiple long connections / duplicate replies | Set `gatewayLocation` to a common working directory, see [docs/advanced.en.md](./docs/advanced.en.md#multiple-instances-and-gateway-election) |
|
package/README.md
CHANGED
|
@@ -173,7 +173,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
|
|
|
173
173
|
| `logLevel` | `debug`\|`info`\|`warn`\|`error` | `info` | 日志级别 |
|
|
174
174
|
| `logFile` | string \| boolean | — | `true` = 写 `<configDir>/plugins/feishu.log`,服务模式建议开启 |
|
|
175
175
|
| `approvalTtlMs` | number | `600000` | 审批 token / 卡片有效期 |
|
|
176
|
-
| `staleExecutionMs` | number | `300000` | 看门狗阈值(
|
|
176
|
+
| `staleExecutionMs` | number | `300000` | 看门狗阈值(0–60 分钟;**0 = 关闭看门狗**;待答表单 / 未决审批期间不判卡死) |
|
|
177
177
|
| `gatewayLocation` | string | — | 只在该 location(及其子目录)启动网关;留空 = 任意 location 生效 |
|
|
178
178
|
|
|
179
179
|
完整配置(含 `cardMaxTables`、`topicStatus*`、`resumeSummary*`、`keepalive*`、`gatewayMatchGraceMs` 等进阶项)见 [docs/advanced.md](./docs/advanced.md#完整配置项)。
|
|
@@ -188,7 +188,7 @@ agent 反问时表单会变成飞书卡片,**点按钮或在话题里直接发
|
|
|
188
188
|
| 插件完全没被加载 | npm 方式确认包名在 `plugins` 数组;目录方式确认 `plugins/<名>/index.js` 存在 |
|
|
189
189
|
| 审批卡收不到 | 该会话不是从飞书发起的(无映射),插件按设计不接管 |
|
|
190
190
|
| 点按钮提示凭证无效 | token 过期(默认 10 分钟)或点击者不在白名单 |
|
|
191
|
-
| 会话像卡死、只排队 | 看门狗默认 5
|
|
191
|
+
| 会话像卡死、只排队 | 看门狗默认 5 分钟后自动中断(**待答表单 / 未决审批期间不中断**;`staleExecutionMs: 0` 可关闭);也可点「⏹ 强制停止」或发 `/stop` |
|
|
192
192
|
| 空闲约 1 小时后失联 | opencode 会回收空闲 location;内置保活默认开启会自动恢复,见 [docs/advanced.md](./docs/advanced.md#位置保活) |
|
|
193
193
|
| 看不到插件日志 | 服务模式下 stderr 被丢弃,设 `logFile: true` |
|
|
194
194
|
| 多个长连接 / 重复回复 | 设置 `gatewayLocation` 为常用工作目录,见 [docs/advanced.md](./docs/advanced.md#多实例与网关选举) |
|
package/dist/index.js
CHANGED
|
@@ -144008,7 +144008,7 @@ function resolveConfig2(raw, env = process.env, deps = {}) {
|
|
|
144008
144008
|
const topicGuidance = asBoolean(merged.topicGuidance, true);
|
|
144009
144009
|
const throttle2 = clamp4(asNumber(merged.streamThrottleMs, 400), 400, 6e4);
|
|
144010
144010
|
const approvalTtlMs = clamp4(asNumber(merged.approvalTtlMs, 10 * 60 * 1e3), 3e4, 24 * 60 * 60 * 1e3);
|
|
144011
|
-
const staleExecutionMs = clamp4(asNumber(merged.staleExecutionMs, 5 * 60 * 1e3),
|
|
144011
|
+
const staleExecutionMs = clamp4(asNumber(merged.staleExecutionMs, 5 * 60 * 1e3), 0, 60 * 60 * 1e3);
|
|
144012
144012
|
const maxResourcesShown = clamp4(asNumber(merged.maxResourcesShown, 8), 1, 50);
|
|
144013
144013
|
const allowedRootsRaw = asStringArray(merged.allowedRoots);
|
|
144014
144014
|
const allowedRootsCandidates = (allowedRootsRaw.length > 0 ? allowedRootsRaw : [...DEFAULT_ALLOWED_ROOTS]).filter(isAbsolute);
|
|
@@ -144507,6 +144507,18 @@ var TtlMap = class {
|
|
|
144507
144507
|
}
|
|
144508
144508
|
this.data.delete(key);
|
|
144509
144509
|
}
|
|
144510
|
+
/** 遍历未过期条目(惰性清理已过期项;用于按会话反查 pending 状态)。 */
|
|
144511
|
+
entries() {
|
|
144512
|
+
const out = [];
|
|
144513
|
+
for (const [key, hit] of this.data) {
|
|
144514
|
+
if (hit.expiresAt <= this.now()) {
|
|
144515
|
+
this.delete(key);
|
|
144516
|
+
continue;
|
|
144517
|
+
}
|
|
144518
|
+
out.push([key, hit.value]);
|
|
144519
|
+
}
|
|
144520
|
+
return out;
|
|
144521
|
+
}
|
|
144510
144522
|
clear() {
|
|
144511
144523
|
for (const timer of this.timers.values()) clearTimeout(timer);
|
|
144512
144524
|
this.timers.clear();
|
|
@@ -145940,14 +145952,17 @@ var ExecutionTracker = class {
|
|
|
145940
145952
|
/**
|
|
145941
145953
|
* 返回「正在跑但超过 maxIdleMs 无任何活动」的 sessionID,并将其从运行态移除。
|
|
145942
145954
|
* 用于插件层看门狗:事件丢失或交互工具(question/permission)永久挂起时兜底放开排队。
|
|
145955
|
+
*
|
|
145956
|
+
* `shouldSkip` 返回 true 的会话视为**合法等待**(如待答表单 / 未决审批):
|
|
145957
|
+
* 既不返回、也不移出运行态(避免在等待期被误杀,也避免丢追踪导致后续排队判定失真)。
|
|
145943
145958
|
*/
|
|
145944
|
-
stale(maxIdleMs, now3 = Date.now()) {
|
|
145959
|
+
stale(maxIdleMs, now3 = Date.now(), shouldSkip) {
|
|
145945
145960
|
const out = [];
|
|
145946
145961
|
for (const [sessionID, at] of this.running) {
|
|
145947
|
-
if (now3 - at
|
|
145948
|
-
|
|
145949
|
-
|
|
145950
|
-
|
|
145962
|
+
if (now3 - at < maxIdleMs) continue;
|
|
145963
|
+
if (shouldSkip?.(sessionID)) continue;
|
|
145964
|
+
this.running.delete(sessionID);
|
|
145965
|
+
out.push(sessionID);
|
|
145951
145966
|
}
|
|
145952
145967
|
return out;
|
|
145953
145968
|
}
|
|
@@ -145955,6 +145970,41 @@ var ExecutionTracker = class {
|
|
|
145955
145970
|
this.running.clear();
|
|
145956
145971
|
}
|
|
145957
145972
|
};
|
|
145973
|
+
var SessionParentLinks = class {
|
|
145974
|
+
constructor(maxEntries = 2e3) {
|
|
145975
|
+
this.maxEntries = maxEntries;
|
|
145976
|
+
}
|
|
145977
|
+
maxEntries;
|
|
145978
|
+
parents = /* @__PURE__ */ new Map();
|
|
145979
|
+
/** 登记/清理某会话的父链接(无 parentID = 顶级会话,清掉旧链接)。 */
|
|
145980
|
+
remember(sessionID, parentID) {
|
|
145981
|
+
if (!sessionID) return;
|
|
145982
|
+
if (!parentID) {
|
|
145983
|
+
this.parents.delete(sessionID);
|
|
145984
|
+
return;
|
|
145985
|
+
}
|
|
145986
|
+
this.parents.delete(sessionID);
|
|
145987
|
+
this.parents.set(sessionID, parentID);
|
|
145988
|
+
while (this.parents.size > this.maxEntries) {
|
|
145989
|
+
const oldest = this.parents.keys().next().value;
|
|
145990
|
+
if (oldest === void 0) break;
|
|
145991
|
+
this.parents.delete(oldest);
|
|
145992
|
+
}
|
|
145993
|
+
}
|
|
145994
|
+
parentOf(sessionID) {
|
|
145995
|
+
return this.parents.get(sessionID);
|
|
145996
|
+
}
|
|
145997
|
+
/** 沿父链逐级回调(含自身;防环 + 深度上限)。 */
|
|
145998
|
+
walk(sessionID, visit) {
|
|
145999
|
+
let current = sessionID;
|
|
146000
|
+
const seen = /* @__PURE__ */ new Set();
|
|
146001
|
+
for (let depth = 0; current && depth < 16 && !seen.has(current); depth += 1) {
|
|
146002
|
+
seen.add(current);
|
|
146003
|
+
visit(current);
|
|
146004
|
+
current = this.parents.get(current);
|
|
146005
|
+
}
|
|
146006
|
+
}
|
|
146007
|
+
};
|
|
145958
146008
|
|
|
145959
146009
|
// src/utils/throttle.ts
|
|
145960
146010
|
function createThrottler(options2) {
|
|
@@ -146735,12 +146785,13 @@ function createRunController(deps) {
|
|
|
146735
146785
|
hasActive(sessionID) {
|
|
146736
146786
|
return sessions.get(sessionID)?.active !== void 0;
|
|
146737
146787
|
},
|
|
146738
|
-
staleQueued(maxIdleMs, now3 = Date.now()) {
|
|
146788
|
+
staleQueued(maxIdleMs, now3 = Date.now(), shouldSkip) {
|
|
146739
146789
|
const out = [];
|
|
146740
146790
|
for (const [sessionID, runs] of sessions) {
|
|
146741
146791
|
if (runs.active) continue;
|
|
146742
146792
|
const first = runs.queued[0];
|
|
146743
146793
|
if (!first || now3 - first.queuedAt < maxIdleMs) continue;
|
|
146794
|
+
if (shouldSkip?.(sessionID)) continue;
|
|
146744
146795
|
if (notifiedQueued.has(sessionID)) continue;
|
|
146745
146796
|
notifiedQueued.add(sessionID);
|
|
146746
146797
|
out.push(sessionID);
|
|
@@ -147251,6 +147302,13 @@ var FormRelay = class {
|
|
|
147251
147302
|
this.forms.delete(id2);
|
|
147252
147303
|
void this.finish(pending, "cancelled");
|
|
147253
147304
|
}
|
|
147305
|
+
/** 是否有尚未提交/取消的待答表单(看门狗判活:等表单属合法等待,不应判 stale)。 */
|
|
147306
|
+
hasPendingFor(sessionID) {
|
|
147307
|
+
for (const [, pending] of this.forms.entries()) {
|
|
147308
|
+
if (pending.sessionID === sessionID && !pending.settled) return true;
|
|
147309
|
+
}
|
|
147310
|
+
return false;
|
|
147311
|
+
}
|
|
147254
147312
|
dispose() {
|
|
147255
147313
|
this.disposed = true;
|
|
147256
147314
|
this.forms.clear();
|
|
@@ -149014,6 +149072,13 @@ var ApprovalManager = class {
|
|
|
149014
149072
|
parsed.decision === "reject" ? "\u5DF2\u62D2\u7EDD" : "\u5DF2\u5141\u8BB8"
|
|
149015
149073
|
);
|
|
149016
149074
|
}
|
|
149075
|
+
/** 该会话是否有未决审批(看门狗判活:等审批属合法等待,不应判 stale)。 */
|
|
149076
|
+
hasPendingFor(sessionID) {
|
|
149077
|
+
for (const [, card] of this.cards.entries()) {
|
|
149078
|
+
if (!card.resolved && card.input.sessionID === sessionID) return true;
|
|
149079
|
+
}
|
|
149080
|
+
return false;
|
|
149081
|
+
}
|
|
149017
149082
|
dispose() {
|
|
149018
149083
|
this.seenRequests.clear();
|
|
149019
149084
|
this.cards.clear();
|
|
@@ -150767,6 +150832,14 @@ async function routeEvent(event, deps) {
|
|
|
150767
150832
|
const touched = event.data?.sessionID;
|
|
150768
150833
|
if (typeof touched === "string") deps.touch(touched);
|
|
150769
150834
|
switch (event.type) {
|
|
150835
|
+
case "session.created": {
|
|
150836
|
+
const data = event.data;
|
|
150837
|
+
const childId = typeof data.sessionID === "string" && data.sessionID ? data.sessionID : typeof data.id === "string" && data.id ? data.id : "";
|
|
150838
|
+
if (!childId) break;
|
|
150839
|
+
const parent = typeof data.parentID === "string" && data.parentID ? data.parentID : typeof data.parentId === "string" && data.parentId ? data.parentId : void 0;
|
|
150840
|
+
deps.onSessionCreated?.(childId, parent);
|
|
150841
|
+
break;
|
|
150842
|
+
}
|
|
150770
150843
|
case "permission.asked":
|
|
150771
150844
|
void deps.onPermissionAsked(event.data).catch((err2) => deps.log.warn("\u5904\u7406 permission.asked \u5931\u8D25", { error: errorMessage(err2) }));
|
|
150772
150845
|
break;
|
|
@@ -151556,6 +151629,7 @@ async function start(ctx, config, log2, logSink) {
|
|
|
151556
151629
|
await owner.load().catch((err2) => log2.warn("owner \u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) }));
|
|
151557
151630
|
const dedup = new MessageDedup(storage, log2);
|
|
151558
151631
|
const executions = new ExecutionTracker();
|
|
151632
|
+
const sessionLinks = new SessionParentLinks();
|
|
151559
151633
|
const runs = createRunController({
|
|
151560
151634
|
sender,
|
|
151561
151635
|
log: log2,
|
|
@@ -152161,9 +152235,13 @@ ${downloadedAttachmentPrompt(message.attachment, outcome)}`;
|
|
|
152161
152235
|
})();
|
|
152162
152236
|
const eventDeps = {
|
|
152163
152237
|
log: log2,
|
|
152164
|
-
touch: (sessionID) => executions.touch(
|
|
152238
|
+
touch: (sessionID) => sessionLinks.walk(sessionID, (id2) => executions.touch(id2)),
|
|
152165
152239
|
markStarted: (sessionID) => executions.markStarted(sessionID),
|
|
152166
152240
|
markEnded: (sessionID) => executions.markEnded(sessionID),
|
|
152241
|
+
onSessionCreated: (sessionID, parentID) => {
|
|
152242
|
+
sessionLinks.remember(sessionID, parentID);
|
|
152243
|
+
log2.debug("session.created \u7236\u5B50\u767B\u8BB0", { sessionID, parentID: parentID ?? null });
|
|
152244
|
+
},
|
|
152167
152245
|
applyRun: (sessionID, event) => runs.apply(sessionID, event),
|
|
152168
152246
|
onPermissionAsked: (data) => approvals.onAsked(data),
|
|
152169
152247
|
onPermissionReplied: (data) => approvals.onReplied(data),
|
|
@@ -152186,13 +152264,16 @@ ${downloadedAttachmentPrompt(message.attachment, outcome)}`;
|
|
|
152186
152264
|
}
|
|
152187
152265
|
await sender.sendText(link2.chatId, text);
|
|
152188
152266
|
}
|
|
152189
|
-
const
|
|
152267
|
+
const hasPendingInteraction = (sessionID) => approvals.hasPendingFor(sessionID) || formRelay.hasPendingFor(sessionID);
|
|
152268
|
+
const stopWatchdog = config.staleExecutionMs > 0 ? startWatchdog({
|
|
152190
152269
|
log: log2,
|
|
152191
152270
|
staleExecutionMs: config.staleExecutionMs,
|
|
152192
|
-
staleExecutions: () => executions.stale(config.staleExecutionMs),
|
|
152193
|
-
staleQueued: () => runs.staleQueued(config.staleExecutionMs),
|
|
152271
|
+
staleExecutions: () => executions.stale(config.staleExecutionMs, Date.now(), hasPendingInteraction),
|
|
152272
|
+
staleQueued: () => runs.staleQueued(config.staleExecutionMs, Date.now(), hasPendingInteraction),
|
|
152194
152273
|
recover: (sessionID, reason) => recovery.recover(sessionID, reason)
|
|
152195
|
-
})
|
|
152274
|
+
}) : () => {
|
|
152275
|
+
log2.info("\u770B\u95E8\u72D7\u5DF2\u5173\u95ED\uFF08staleExecutionMs=0\uFF09");
|
|
152276
|
+
};
|
|
152196
152277
|
async function notifyStuck(sessionID, reason, ok) {
|
|
152197
152278
|
const link2 = await sessionMap.resolveBySession(sessionID);
|
|
152198
152279
|
if (!link2) return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-feishu-plugin",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.12",
|
|
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",
|