opencode-feishu-plugin 0.2.11 → 0.2.13
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 +174 -36
- 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) {
|
|
@@ -146575,13 +146625,49 @@ function trailingText(blocks) {
|
|
|
146575
146625
|
}
|
|
146576
146626
|
return parts.join("\n\n").trim();
|
|
146577
146627
|
}
|
|
146628
|
+
function isActivityEvent(type) {
|
|
146629
|
+
return type === "text.started" || type === "text.delta" || type === "text.ended" || type === "tool.input.started" || type === "tool.input.ended" || type === "tool.success" || type === "tool.error";
|
|
146630
|
+
}
|
|
146631
|
+
var QUEUED_DRAIN_GRACE_MS = 3e3;
|
|
146578
146632
|
function createRunController(deps) {
|
|
146579
146633
|
const sessions = /* @__PURE__ */ new Map();
|
|
146580
146634
|
const throttlers = /* @__PURE__ */ new Map();
|
|
146581
146635
|
const sessionModels = /* @__PURE__ */ new Map();
|
|
146582
146636
|
const notifiedQueued = /* @__PURE__ */ new Set();
|
|
146637
|
+
const drainTimers = /* @__PURE__ */ new Map();
|
|
146583
146638
|
const throttleMs = Math.max(400, deps.throttleMs);
|
|
146584
146639
|
let disposed = false;
|
|
146640
|
+
const cancelDrain = (sessionID) => {
|
|
146641
|
+
const timer = drainTimers.get(sessionID);
|
|
146642
|
+
if (timer) {
|
|
146643
|
+
clearTimeout(timer);
|
|
146644
|
+
drainTimers.delete(sessionID);
|
|
146645
|
+
}
|
|
146646
|
+
};
|
|
146647
|
+
const scheduleDrain = (sessionID, runs) => {
|
|
146648
|
+
if (runs.queued.length === 0) {
|
|
146649
|
+
cancelDrain(sessionID);
|
|
146650
|
+
return;
|
|
146651
|
+
}
|
|
146652
|
+
cancelDrain(sessionID);
|
|
146653
|
+
const timer = setTimeout(() => {
|
|
146654
|
+
drainTimers.delete(sessionID);
|
|
146655
|
+
if (disposed) return;
|
|
146656
|
+
const live = sessions.get(sessionID);
|
|
146657
|
+
if (!live || live.queued.length === 0) return;
|
|
146658
|
+
deps.log.info("\u6392\u961F\u5361\u6309\u300C\u5DF2\u968F\u672C\u8F6E\u5904\u7406\u300D\u6536\u5C3E\uFF08\u961F\u5217\u6D88\u606F\u5728\u5F53\u524D\u6267\u884C\u5185\u88AB\u6D88\u8D39\uFF09", {
|
|
146659
|
+
sessionID,
|
|
146660
|
+
count: live.queued.length
|
|
146661
|
+
});
|
|
146662
|
+
for (const card of live.queued) {
|
|
146663
|
+
card.state = reduce6(card.state, { type: "execution.succeeded" });
|
|
146664
|
+
finalize2(card);
|
|
146665
|
+
}
|
|
146666
|
+
live.queued.length = 0;
|
|
146667
|
+
}, QUEUED_DRAIN_GRACE_MS);
|
|
146668
|
+
timer.unref?.();
|
|
146669
|
+
drainTimers.set(sessionID, timer);
|
|
146670
|
+
};
|
|
146585
146671
|
const sessionRuns = (sessionID) => {
|
|
146586
146672
|
let runs = sessions.get(sessionID);
|
|
146587
146673
|
if (!runs) {
|
|
@@ -146689,6 +146775,7 @@ function createRunController(deps) {
|
|
|
146689
146775
|
if (!runs) return;
|
|
146690
146776
|
if (event.type === "execution.started") {
|
|
146691
146777
|
notifiedQueued.delete(sessionID);
|
|
146778
|
+
cancelDrain(sessionID);
|
|
146692
146779
|
if (runs.active) {
|
|
146693
146780
|
update2(runs.active, event, true);
|
|
146694
146781
|
return;
|
|
@@ -146696,51 +146783,64 @@ function createRunController(deps) {
|
|
|
146696
146783
|
const next = runs.queued.shift();
|
|
146697
146784
|
if (next) {
|
|
146698
146785
|
runs.active = next;
|
|
146786
|
+
deps.log.debug("\u6392\u961F\u5361\u664B\u5347\u4E3A\u8FD0\u884C\u5361", { sessionID, runID: next.runID });
|
|
146699
146787
|
update2(next, event, true);
|
|
146700
146788
|
}
|
|
146789
|
+
if (runs.queued.length > 0) scheduleDrain(sessionID, runs);
|
|
146701
146790
|
return;
|
|
146702
146791
|
}
|
|
146792
|
+
if (!runs.active && runs.queued.length > 0 && isActivityEvent(event.type)) {
|
|
146793
|
+
const next = runs.queued.shift();
|
|
146794
|
+
runs.active = next;
|
|
146795
|
+
deps.log.info("\u6536\u5230\u6D3B\u52A8\u4E8B\u4EF6\u4F46\u65E0\u8FD0\u884C\u5361\uFF1A\u664B\u5347\u961F\u9996\u6392\u961F\u5361", {
|
|
146796
|
+
sessionID,
|
|
146797
|
+
runID: next.runID,
|
|
146798
|
+
event: event.type
|
|
146799
|
+
});
|
|
146800
|
+
next.state = reduce6(next.state, { type: "execution.started" });
|
|
146801
|
+
}
|
|
146703
146802
|
const card = runs.active;
|
|
146704
|
-
if (
|
|
146705
|
-
|
|
146706
|
-
|
|
146707
|
-
|
|
146708
|
-
|
|
146709
|
-
|
|
146803
|
+
if (event.type === "execution.succeeded" || event.type === "execution.failed") {
|
|
146804
|
+
if (card) {
|
|
146805
|
+
if (event.type === "execution.succeeded") {
|
|
146806
|
+
const text = trailingText(card.state.blocks);
|
|
146807
|
+
const long = deps.sendFinalAnswer !== void 0 && deps.finalAnswer !== void 0 && text.length >= deps.finalAnswer.minChars;
|
|
146808
|
+
if (long) {
|
|
146809
|
+
card.state = reduce6(reduce6(card.state, { type: "final.separated" }), event);
|
|
146810
|
+
finalize2(card);
|
|
146811
|
+
runs.active = void 0;
|
|
146812
|
+
void deps.sendFinalAnswer({
|
|
146813
|
+
sessionID,
|
|
146814
|
+
chatId: card.chatId,
|
|
146815
|
+
...card.replyToMessageId ? { replyToMessageId: card.replyToMessageId } : {},
|
|
146816
|
+
text
|
|
146817
|
+
}).catch((err2) => {
|
|
146818
|
+
deps.log.warn("\u6700\u7EC8\u7B54\u6848\u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
|
|
146819
|
+
});
|
|
146820
|
+
scheduleDrain(sessionID, runs);
|
|
146821
|
+
return;
|
|
146822
|
+
}
|
|
146823
|
+
}
|
|
146824
|
+
update2(card, event, true);
|
|
146710
146825
|
finalize2(card);
|
|
146711
146826
|
runs.active = void 0;
|
|
146712
|
-
void deps.sendFinalAnswer({
|
|
146713
|
-
sessionID,
|
|
146714
|
-
chatId: card.chatId,
|
|
146715
|
-
...card.replyToMessageId ? { replyToMessageId: card.replyToMessageId } : {},
|
|
146716
|
-
text
|
|
146717
|
-
}).catch((err2) => {
|
|
146718
|
-
deps.log.warn("\u6700\u7EC8\u7B54\u6848\u53D1\u9001\u5931\u8D25", { sessionID, error: errorMessage(err2) });
|
|
146719
|
-
});
|
|
146720
|
-
return;
|
|
146721
146827
|
}
|
|
146722
|
-
|
|
146723
|
-
finalize2(card);
|
|
146724
|
-
runs.active = void 0;
|
|
146725
|
-
return;
|
|
146726
|
-
}
|
|
146727
|
-
if (event.type === "execution.failed") {
|
|
146728
|
-
update2(card, event, true);
|
|
146729
|
-
finalize2(card);
|
|
146730
|
-
runs.active = void 0;
|
|
146828
|
+
scheduleDrain(sessionID, runs);
|
|
146731
146829
|
return;
|
|
146732
146830
|
}
|
|
146831
|
+
if (!card) return;
|
|
146733
146832
|
update2(card, event, false);
|
|
146734
146833
|
},
|
|
146735
146834
|
hasActive(sessionID) {
|
|
146736
146835
|
return sessions.get(sessionID)?.active !== void 0;
|
|
146737
146836
|
},
|
|
146738
|
-
staleQueued(maxIdleMs, now3 = Date.now()) {
|
|
146837
|
+
staleQueued(maxIdleMs, now3 = Date.now(), shouldSkip) {
|
|
146739
146838
|
const out = [];
|
|
146740
146839
|
for (const [sessionID, runs] of sessions) {
|
|
146741
146840
|
if (runs.active) continue;
|
|
146742
146841
|
const first = runs.queued[0];
|
|
146743
146842
|
if (!first || now3 - first.queuedAt < maxIdleMs) continue;
|
|
146843
|
+
if (shouldSkip?.(sessionID)) continue;
|
|
146744
146844
|
if (notifiedQueued.has(sessionID)) continue;
|
|
146745
146845
|
notifiedQueued.add(sessionID);
|
|
146746
146846
|
out.push(sessionID);
|
|
@@ -146767,6 +146867,8 @@ function createRunController(deps) {
|
|
|
146767
146867
|
disposed = true;
|
|
146768
146868
|
sessionModels.clear();
|
|
146769
146869
|
notifiedQueued.clear();
|
|
146870
|
+
for (const timer of drainTimers.values()) clearTimeout(timer);
|
|
146871
|
+
drainTimers.clear();
|
|
146770
146872
|
for (const throttler of throttlers.values()) throttler.cancel();
|
|
146771
146873
|
throttlers.clear();
|
|
146772
146874
|
for (const runs of sessions.values()) {
|
|
@@ -147251,6 +147353,13 @@ var FormRelay = class {
|
|
|
147251
147353
|
this.forms.delete(id2);
|
|
147252
147354
|
void this.finish(pending, "cancelled");
|
|
147253
147355
|
}
|
|
147356
|
+
/** 是否有尚未提交/取消的待答表单(看门狗判活:等表单属合法等待,不应判 stale)。 */
|
|
147357
|
+
hasPendingFor(sessionID) {
|
|
147358
|
+
for (const [, pending] of this.forms.entries()) {
|
|
147359
|
+
if (pending.sessionID === sessionID && !pending.settled) return true;
|
|
147360
|
+
}
|
|
147361
|
+
return false;
|
|
147362
|
+
}
|
|
147254
147363
|
dispose() {
|
|
147255
147364
|
this.disposed = true;
|
|
147256
147365
|
this.forms.clear();
|
|
@@ -149014,6 +149123,13 @@ var ApprovalManager = class {
|
|
|
149014
149123
|
parsed.decision === "reject" ? "\u5DF2\u62D2\u7EDD" : "\u5DF2\u5141\u8BB8"
|
|
149015
149124
|
);
|
|
149016
149125
|
}
|
|
149126
|
+
/** 该会话是否有未决审批(看门狗判活:等审批属合法等待,不应判 stale)。 */
|
|
149127
|
+
hasPendingFor(sessionID) {
|
|
149128
|
+
for (const [, card] of this.cards.entries()) {
|
|
149129
|
+
if (!card.resolved && card.input.sessionID === sessionID) return true;
|
|
149130
|
+
}
|
|
149131
|
+
return false;
|
|
149132
|
+
}
|
|
149017
149133
|
dispose() {
|
|
149018
149134
|
this.seenRequests.clear();
|
|
149019
149135
|
this.cards.clear();
|
|
@@ -150767,6 +150883,14 @@ async function routeEvent(event, deps) {
|
|
|
150767
150883
|
const touched = event.data?.sessionID;
|
|
150768
150884
|
if (typeof touched === "string") deps.touch(touched);
|
|
150769
150885
|
switch (event.type) {
|
|
150886
|
+
case "session.created": {
|
|
150887
|
+
const data = event.data;
|
|
150888
|
+
const childId = typeof data.sessionID === "string" && data.sessionID ? data.sessionID : typeof data.id === "string" && data.id ? data.id : "";
|
|
150889
|
+
if (!childId) break;
|
|
150890
|
+
const parent = typeof data.parentID === "string" && data.parentID ? data.parentID : typeof data.parentId === "string" && data.parentId ? data.parentId : void 0;
|
|
150891
|
+
deps.onSessionCreated?.(childId, parent);
|
|
150892
|
+
break;
|
|
150893
|
+
}
|
|
150770
150894
|
case "permission.asked":
|
|
150771
150895
|
void deps.onPermissionAsked(event.data).catch((err2) => deps.log.warn("\u5904\u7406 permission.asked \u5931\u8D25", { error: errorMessage(err2) }));
|
|
150772
150896
|
break;
|
|
@@ -150843,18 +150967,21 @@ async function routeEvent(event, deps) {
|
|
|
150843
150967
|
}
|
|
150844
150968
|
case "session.execution.started": {
|
|
150845
150969
|
const data = event.data;
|
|
150970
|
+
deps.log.debug("execution.started", { sessionID: data.sessionID });
|
|
150846
150971
|
deps.markStarted(data.sessionID);
|
|
150847
150972
|
deps.applyRun(data.sessionID, { type: "execution.started" });
|
|
150848
150973
|
break;
|
|
150849
150974
|
}
|
|
150850
150975
|
case "session.execution.succeeded": {
|
|
150851
150976
|
const data = event.data;
|
|
150977
|
+
deps.log.debug("execution.succeeded", { sessionID: data.sessionID });
|
|
150852
150978
|
deps.markEnded(data.sessionID);
|
|
150853
150979
|
deps.applyRun(data.sessionID, { type: "execution.succeeded" });
|
|
150854
150980
|
break;
|
|
150855
150981
|
}
|
|
150856
150982
|
case "session.execution.failed": {
|
|
150857
150983
|
const data = event.data;
|
|
150984
|
+
deps.log.debug("execution.failed", { sessionID: data.sessionID });
|
|
150858
150985
|
deps.markEnded(data.sessionID);
|
|
150859
150986
|
deps.applyRun(data.sessionID, { type: "execution.failed", error: extractErrorText(data.error) });
|
|
150860
150987
|
void deps.notifyFailure(data.sessionID, data.error);
|
|
@@ -150862,6 +150989,7 @@ async function routeEvent(event, deps) {
|
|
|
150862
150989
|
}
|
|
150863
150990
|
case "session.execution.interrupted": {
|
|
150864
150991
|
const data = event.data;
|
|
150992
|
+
deps.log.debug("execution.interrupted", { sessionID: data.sessionID, reason: data.reason });
|
|
150865
150993
|
deps.markEnded(data.sessionID);
|
|
150866
150994
|
deps.applyRun(data.sessionID, { type: "execution.failed", error: `\u5DF2\u4E2D\u65AD\uFF08${data.reason ?? "unknown"}\uFF09` });
|
|
150867
150995
|
break;
|
|
@@ -150869,6 +150997,7 @@ async function routeEvent(event, deps) {
|
|
|
150869
150997
|
case "session.status": {
|
|
150870
150998
|
const data = event.data;
|
|
150871
150999
|
const statusType = data.status?.type;
|
|
151000
|
+
deps.log.debug("session.status", { sessionID: data.sessionID, status: statusType });
|
|
150872
151001
|
if (statusType === "idle") {
|
|
150873
151002
|
deps.markEnded(data.sessionID);
|
|
150874
151003
|
deps.applyRun(data.sessionID, { type: "execution.succeeded" });
|
|
@@ -150879,6 +151008,7 @@ async function routeEvent(event, deps) {
|
|
|
150879
151008
|
}
|
|
150880
151009
|
case "session.idle": {
|
|
150881
151010
|
const data = event.data;
|
|
151011
|
+
deps.log.debug("session.idle", { sessionID: data.sessionID });
|
|
150882
151012
|
deps.markEnded(data.sessionID);
|
|
150883
151013
|
deps.applyRun(data.sessionID, { type: "execution.succeeded" });
|
|
150884
151014
|
break;
|
|
@@ -151556,6 +151686,7 @@ async function start(ctx, config, log2, logSink) {
|
|
|
151556
151686
|
await owner.load().catch((err2) => log2.warn("owner \u8BFB\u53D6\u5931\u8D25", { error: errorMessage(err2) }));
|
|
151557
151687
|
const dedup = new MessageDedup(storage, log2);
|
|
151558
151688
|
const executions = new ExecutionTracker();
|
|
151689
|
+
const sessionLinks = new SessionParentLinks();
|
|
151559
151690
|
const runs = createRunController({
|
|
151560
151691
|
sender,
|
|
151561
151692
|
log: log2,
|
|
@@ -152161,9 +152292,13 @@ ${downloadedAttachmentPrompt(message.attachment, outcome)}`;
|
|
|
152161
152292
|
})();
|
|
152162
152293
|
const eventDeps = {
|
|
152163
152294
|
log: log2,
|
|
152164
|
-
touch: (sessionID) => executions.touch(
|
|
152295
|
+
touch: (sessionID) => sessionLinks.walk(sessionID, (id2) => executions.touch(id2)),
|
|
152165
152296
|
markStarted: (sessionID) => executions.markStarted(sessionID),
|
|
152166
152297
|
markEnded: (sessionID) => executions.markEnded(sessionID),
|
|
152298
|
+
onSessionCreated: (sessionID, parentID) => {
|
|
152299
|
+
sessionLinks.remember(sessionID, parentID);
|
|
152300
|
+
log2.debug("session.created \u7236\u5B50\u767B\u8BB0", { sessionID, parentID: parentID ?? null });
|
|
152301
|
+
},
|
|
152167
152302
|
applyRun: (sessionID, event) => runs.apply(sessionID, event),
|
|
152168
152303
|
onPermissionAsked: (data) => approvals.onAsked(data),
|
|
152169
152304
|
onPermissionReplied: (data) => approvals.onReplied(data),
|
|
@@ -152186,13 +152321,16 @@ ${downloadedAttachmentPrompt(message.attachment, outcome)}`;
|
|
|
152186
152321
|
}
|
|
152187
152322
|
await sender.sendText(link2.chatId, text);
|
|
152188
152323
|
}
|
|
152189
|
-
const
|
|
152324
|
+
const hasPendingInteraction = (sessionID) => approvals.hasPendingFor(sessionID) || formRelay.hasPendingFor(sessionID);
|
|
152325
|
+
const stopWatchdog = config.staleExecutionMs > 0 ? startWatchdog({
|
|
152190
152326
|
log: log2,
|
|
152191
152327
|
staleExecutionMs: config.staleExecutionMs,
|
|
152192
|
-
staleExecutions: () => executions.stale(config.staleExecutionMs),
|
|
152193
|
-
staleQueued: () => runs.staleQueued(config.staleExecutionMs),
|
|
152328
|
+
staleExecutions: () => executions.stale(config.staleExecutionMs, Date.now(), hasPendingInteraction),
|
|
152329
|
+
staleQueued: () => runs.staleQueued(config.staleExecutionMs, Date.now(), hasPendingInteraction),
|
|
152194
152330
|
recover: (sessionID, reason) => recovery.recover(sessionID, reason)
|
|
152195
|
-
})
|
|
152331
|
+
}) : () => {
|
|
152332
|
+
log2.info("\u770B\u95E8\u72D7\u5DF2\u5173\u95ED\uFF08staleExecutionMs=0\uFF09");
|
|
152333
|
+
};
|
|
152196
152334
|
async function notifyStuck(sessionID, reason, ok) {
|
|
152197
152335
|
const link2 = await sessionMap.resolveBySession(sessionID);
|
|
152198
152336
|
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.13",
|
|
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",
|