larkin 0.5.14 → 0.5.16
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/dist/agent/freshness-gate.cjs +27 -0
- package/dist/app/agent-readiness.cjs +10 -5
- package/dist/app/agent-readiness.mjs +9 -5
- package/dist/app/lark-cli.mjs +46 -1
- package/dist/dashboard/dashboard-view-model.mjs +2 -4
- package/dist/feishu/host-processing-eye.cjs +50 -3
- package/dist/feishu/host-processing-eye.mjs +50 -3
- package/dist/feishu/host-shell.cjs +44 -8
- package/dist/feishu/host-shell.mjs +44 -9
- package/dist/feishu/im-freshness-adapter.cjs +96 -0
- package/package.json +1 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.evaluateFreshness = evaluateFreshness;
|
|
4
|
+
/** Provider-neutral optimistic gate. Provider fields and cursor ordering stay adapter-owned. */
|
|
5
|
+
function evaluateFreshness(input) {
|
|
6
|
+
let snapshot;
|
|
7
|
+
try {
|
|
8
|
+
snapshot = input.probe();
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
return { status: "unavailable", reason: error instanceof Error ? error.message : String(error) };
|
|
12
|
+
}
|
|
13
|
+
try {
|
|
14
|
+
const current = input.adapter.cursor(snapshot);
|
|
15
|
+
const comparison = input.adapter.compare(input.seen, current);
|
|
16
|
+
if (comparison === "gap")
|
|
17
|
+
return { status: "unavailable", reason: "authoritative history cannot establish a comparable head" };
|
|
18
|
+
if (comparison === "fresh")
|
|
19
|
+
return { status: "fresh", current, snapshot };
|
|
20
|
+
if (!current)
|
|
21
|
+
return { status: "unavailable", reason: "authoritative history returned an invalid empty cursor" };
|
|
22
|
+
return { status: "conflict", current, snapshot, context: input.adapter.unseen(input.seen, snapshot) };
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
return { status: "unavailable", reason: error instanceof Error ? error.message : String(error) };
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.isRuntimeReadinessCurrent = isRuntimeReadinessCurrent;
|
|
4
|
+
exports.isSessionCurrent = isSessionCurrent;
|
|
4
5
|
exports.isCurrentOwnedDaemon = isCurrentOwnedDaemon;
|
|
5
6
|
exports.isChannelReconnecting = isChannelReconnecting;
|
|
6
7
|
exports.projectAgentReadiness = projectAgentReadiness;
|
|
@@ -16,6 +17,14 @@ function isRuntimeReadinessCurrent(readiness, daemonStartedAt) {
|
|
|
16
17
|
const epoch = timestamp(daemonStartedAt);
|
|
17
18
|
return observedAt !== null && epoch !== null && observedAt >= epoch;
|
|
18
19
|
}
|
|
20
|
+
function isSessionCurrent(session, daemonStartedAt) {
|
|
21
|
+
// A resumed session deliberately retains its original creation time. Its
|
|
22
|
+
// current daemon-epoch proof is the fresh session observation, not that
|
|
23
|
+
// historic creation timestamp.
|
|
24
|
+
const epoch = timestamp(daemonStartedAt);
|
|
25
|
+
const sessionObservedAt = timestamp(session?.lastSeenAt) ?? timestamp(session?.startedAt);
|
|
26
|
+
return epoch !== null && sessionObservedAt !== null && sessionObservedAt >= epoch;
|
|
27
|
+
}
|
|
19
28
|
function isCurrentOwnedDaemon(daemon) {
|
|
20
29
|
return daemon?.state === "owned"
|
|
21
30
|
&& Number(daemon.pid) > 0
|
|
@@ -34,11 +43,7 @@ function projectAgentReadiness(input) {
|
|
|
34
43
|
const daemonOwned = isCurrentOwnedDaemon(input.daemon) && (0, process_state_js_1.daemonHasAgent)(input.daemon, input.agentId);
|
|
35
44
|
const daemonStartedAt = timestamp(input.daemon.startedAt);
|
|
36
45
|
const connectedAt = timestamp(status?.connectedAt);
|
|
37
|
-
|
|
38
|
-
// current daemon-epoch proof is the fresh session observation, not that
|
|
39
|
-
// historic creation timestamp.
|
|
40
|
-
const sessionObservedAt = timestamp(status?.session?.lastSeenAt) ?? timestamp(status?.session?.startedAt);
|
|
41
|
-
const sessionCurrent = daemonStartedAt !== null && sessionObservedAt !== null && sessionObservedAt >= daemonStartedAt;
|
|
46
|
+
const sessionCurrent = isSessionCurrent(status?.session, input.daemon.startedAt);
|
|
42
47
|
const channelConnected = daemonOwned
|
|
43
48
|
&& status?.connectedVia === "channel"
|
|
44
49
|
&& daemonStartedAt !== null
|
|
@@ -10,6 +10,14 @@ export function isRuntimeReadinessCurrent(readiness, daemonStartedAt) {
|
|
|
10
10
|
const epoch = timestamp(daemonStartedAt);
|
|
11
11
|
return observedAt !== null && epoch !== null && observedAt >= epoch;
|
|
12
12
|
}
|
|
13
|
+
export function isSessionCurrent(session, daemonStartedAt) {
|
|
14
|
+
// A resumed session deliberately retains its original creation time. Its
|
|
15
|
+
// current daemon-epoch proof is the fresh session observation, not that
|
|
16
|
+
// historic creation timestamp.
|
|
17
|
+
const epoch = timestamp(daemonStartedAt);
|
|
18
|
+
const sessionObservedAt = timestamp(session?.lastSeenAt) ?? timestamp(session?.startedAt);
|
|
19
|
+
return epoch !== null && sessionObservedAt !== null && sessionObservedAt >= epoch;
|
|
20
|
+
}
|
|
13
21
|
export function isCurrentOwnedDaemon(daemon) {
|
|
14
22
|
return daemon?.state === "owned"
|
|
15
23
|
&& Number(daemon.pid) > 0
|
|
@@ -28,11 +36,7 @@ export function projectAgentReadiness(input) {
|
|
|
28
36
|
const daemonOwned = isCurrentOwnedDaemon(input.daemon) && daemonHasAgent(input.daemon, input.agentId);
|
|
29
37
|
const daemonStartedAt = timestamp(input.daemon.startedAt);
|
|
30
38
|
const connectedAt = timestamp(status?.connectedAt);
|
|
31
|
-
|
|
32
|
-
// current daemon-epoch proof is the fresh session observation, not that
|
|
33
|
-
// historic creation timestamp.
|
|
34
|
-
const sessionObservedAt = timestamp(status?.session?.lastSeenAt) ?? timestamp(status?.session?.startedAt);
|
|
35
|
-
const sessionCurrent = daemonStartedAt !== null && sessionObservedAt !== null && sessionObservedAt >= daemonStartedAt;
|
|
39
|
+
const sessionCurrent = isSessionCurrent(status?.session, input.daemon.startedAt);
|
|
36
40
|
const channelConnected = daemonOwned
|
|
37
41
|
&& status?.connectedVia === "channel"
|
|
38
42
|
&& daemonStartedAt !== null
|
package/dist/app/lark-cli.mjs
CHANGED
|
@@ -284,6 +284,45 @@ function parseCommentReply(argv) {
|
|
|
284
284
|
return { messageId, text };
|
|
285
285
|
}
|
|
286
286
|
const IM_WRITE_MEMO_LIMIT = 512;
|
|
287
|
+
function normalizeImBodyEscapes(argv) {
|
|
288
|
+
const next = [...argv];
|
|
289
|
+
const boundary = next.indexOf("--");
|
|
290
|
+
const limit = boundary < 0 ? next.length : boundary;
|
|
291
|
+
for (let index = 0; index < limit; index += 1) {
|
|
292
|
+
const argument = next[index];
|
|
293
|
+
const flag = ["--text", "--markdown"].find((candidate) => argument === candidate || argument.startsWith(`${candidate}=`));
|
|
294
|
+
if (!flag)
|
|
295
|
+
continue;
|
|
296
|
+
const valueIndex = argument === flag ? index + 1 : index;
|
|
297
|
+
if (valueIndex >= limit)
|
|
298
|
+
continue;
|
|
299
|
+
const value = argument === flag ? next[valueIndex] : argument.slice(flag.length + 1);
|
|
300
|
+
const hasEscapedWhitespace = /\\[nt]/.test(value);
|
|
301
|
+
// 已含真实换行、Windows 路径和 fenced code 的反斜杠均可能是用户的原文;
|
|
302
|
+
// 仅修复无歧义的单行普通文本,避免破坏代码、正则或路径。
|
|
303
|
+
const preserveVerbatim = /[\r\n]/.test(value)
|
|
304
|
+
|| value.includes("```")
|
|
305
|
+
|| /(?:^|[\s("'`])[A-Za-z]:\\/.test(value);
|
|
306
|
+
const normalized = hasEscapedWhitespace && !preserveVerbatim
|
|
307
|
+
? value.replace(/\\([nt])/g, (_match, escape) => escape === "n" ? "\n" : "\t")
|
|
308
|
+
: value;
|
|
309
|
+
if (argument === flag)
|
|
310
|
+
next[valueIndex] = normalized;
|
|
311
|
+
else
|
|
312
|
+
next[index] = `${flag}=${normalized}`;
|
|
313
|
+
if (argument === flag)
|
|
314
|
+
index += 1;
|
|
315
|
+
}
|
|
316
|
+
return next;
|
|
317
|
+
}
|
|
318
|
+
function recordOutboundAt(store, target) {
|
|
319
|
+
store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
320
|
+
const at = new Date().toISOString();
|
|
321
|
+
state.last_outbound_at = at;
|
|
322
|
+
state.last_outbound_by_target ??= {};
|
|
323
|
+
state.last_outbound_by_target[target] = at;
|
|
324
|
+
});
|
|
325
|
+
}
|
|
287
326
|
// 只标注、不拦截:每次成功写把「实际生效的幂等 key → 服务端返回的 message_id」记进备忘。
|
|
288
327
|
// 同 key 再次成功且服务端返回同一个 message_id,说明服务端走了幂等去重(没有产生新消息),
|
|
289
328
|
// 返回 true 供输出标注 duplicate。拦截权始终在服务端,备忘不会吞掉任何发送。
|
|
@@ -1117,7 +1156,7 @@ function emitCommittedUnverified(result, io, input) {
|
|
|
1117
1156
|
}
|
|
1118
1157
|
export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
1119
1158
|
const io = dependencies.io ?? defaultIo();
|
|
1120
|
-
|
|
1159
|
+
let effectiveArgv = argv;
|
|
1121
1160
|
const runtimeAgentId = larkinConfig.resolveRuntimeAuthority(env);
|
|
1122
1161
|
if (!runtimeAgentId) {
|
|
1123
1162
|
try {
|
|
@@ -1175,6 +1214,9 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1175
1214
|
}
|
|
1176
1215
|
if (decision.kind === "passthrough")
|
|
1177
1216
|
return passthroughWithObservation(effectiveArgv, privateEnv, io, nativeDependencies, store);
|
|
1217
|
+
if (decision.kind === "guarded" && (decision.operation === "send" || decision.operation === "reply")) {
|
|
1218
|
+
effectiveArgv = normalizeImBodyEscapes(effectiveArgv);
|
|
1219
|
+
}
|
|
1178
1220
|
try {
|
|
1179
1221
|
const recallMessageId = decision.operation === "recall" ? assertRecallSyntax(effectiveArgv) : null;
|
|
1180
1222
|
const priorRecall = recallMessageId ? recallLedgerEntry(store, recallMessageId) : null;
|
|
@@ -1233,6 +1275,9 @@ export function runLarkCli(argv, env = process.env, dependencies = {}) {
|
|
|
1233
1275
|
}
|
|
1234
1276
|
const intentKey = policyFlagValue(effectiveArgv, "--idempotency-key") ?? intentId(targetKey, effectiveArgv);
|
|
1235
1277
|
const write = callNative(botArgv(effectiveArgv, intentKey, decision), privateEnv, io, nativeDependencies);
|
|
1278
|
+
if (!write.error && write.status === 0 && (decision.operation === "send" || decision.operation === "reply")) {
|
|
1279
|
+
recordOutboundAt(store, targetKey);
|
|
1280
|
+
}
|
|
1236
1281
|
if (decision.operation === "urgent-app") {
|
|
1237
1282
|
try {
|
|
1238
1283
|
assertUrgentAppNativeAccepted(write);
|
|
@@ -7,7 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { StringDecoder } from "node:string_decoder";
|
|
8
8
|
import { fileURLToPath } from "node:url";
|
|
9
9
|
import { readJson, readProcessState } from "../platform/process-state.mjs";
|
|
10
|
-
import { isRuntimeReadinessCurrent } from "../app/agent-readiness.mjs";
|
|
10
|
+
import { isRuntimeReadinessCurrent, isSessionCurrent } from "../app/agent-readiness.mjs";
|
|
11
11
|
import { isAllowedDashboardAvatarUrl } from "./dashboard-avatar.mjs";
|
|
12
12
|
import { collectWorkspaceEntry as collectTypedWorkspaceEntry } from "./dashboard-workspace.mjs";
|
|
13
13
|
import { buildFingerprint, packageVersion } from "../platform/build-info.mjs";
|
|
@@ -480,9 +480,7 @@ async function collectAgentStatus(a, configDir, daemonStartedAt, piModelResolver
|
|
|
480
480
|
// 本 agent 专属时间线:三路历史合并按时间倒序,不是全局一条汇总——每个 agent 只看自己的故事。
|
|
481
481
|
const { recentErrors, lastActivity, lastDeliver, feed } = projectStatusTimeline(status);
|
|
482
482
|
const statusReadiness = status.runtimeReadiness;
|
|
483
|
-
const
|
|
484
|
-
const daemonEpoch = finiteTime(daemonStartedAt);
|
|
485
|
-
const sessionCurrent = sessionStartedAt !== null && daemonEpoch !== null && sessionStartedAt >= daemonEpoch;
|
|
483
|
+
const sessionCurrent = isSessionCurrent(status.session, daemonStartedAt);
|
|
486
484
|
const runtimeReadiness = sessionCurrent && isRuntimeReadinessCurrent(statusReadiness, daemonStartedAt)
|
|
487
485
|
|| statusReadiness?.state !== "ready"
|
|
488
486
|
? statusReadiness || null
|
|
@@ -15,6 +15,7 @@ class ProcessingEyeOrchestrator {
|
|
|
15
15
|
recordError;
|
|
16
16
|
setTimer;
|
|
17
17
|
clearTimer;
|
|
18
|
+
now;
|
|
18
19
|
constructor(options = {}) {
|
|
19
20
|
this.options = options;
|
|
20
21
|
this.execFile = options.execFile ?? node_child_process_1.execFile;
|
|
@@ -22,6 +23,7 @@ class ProcessingEyeOrchestrator {
|
|
|
22
23
|
this.recordError = options.recordStatusError ?? (() => { });
|
|
23
24
|
this.setTimer = options.setTimer ?? setTimeout;
|
|
24
25
|
this.clearTimer = options.clearTimer ?? clearTimeout;
|
|
26
|
+
this.now = options.now ?? Date.now;
|
|
25
27
|
}
|
|
26
28
|
larkApi(agent, method, apiPath, data, callback) {
|
|
27
29
|
const args = ["api", method, apiPath];
|
|
@@ -64,7 +66,7 @@ class ProcessingEyeOrchestrator {
|
|
|
64
66
|
const existing = this.state.get(agent.agentId);
|
|
65
67
|
if (existing)
|
|
66
68
|
return existing;
|
|
67
|
-
const created = { sawActive: false, fallbackTimer: null, completionTimer: null, gen: 0 };
|
|
69
|
+
const created = { sawActive: false, fallbackTimer: null, completionTimer: null, progressTimer: null, turnStartedAt: 0, gen: 0 };
|
|
68
70
|
this.state.set(agent.agentId, created);
|
|
69
71
|
return created;
|
|
70
72
|
}
|
|
@@ -74,6 +76,12 @@ class ProcessingEyeOrchestrator {
|
|
|
74
76
|
this.clearTimer(state.completionTimer);
|
|
75
77
|
state.completionTimer = null;
|
|
76
78
|
}
|
|
79
|
+
cancelProgress(state) {
|
|
80
|
+
if (!state.progressTimer)
|
|
81
|
+
return;
|
|
82
|
+
this.clearTimer(state.progressTimer);
|
|
83
|
+
state.progressTimer = null;
|
|
84
|
+
}
|
|
77
85
|
restoreAndClear(agent) {
|
|
78
86
|
let leftovers = [];
|
|
79
87
|
try {
|
|
@@ -87,7 +95,7 @@ class ProcessingEyeOrchestrator {
|
|
|
87
95
|
this.pending.set(agent.agentId, leftovers);
|
|
88
96
|
this.clear(agent, "启动清扫遗留");
|
|
89
97
|
}
|
|
90
|
-
add(agent, msgId) {
|
|
98
|
+
add(agent, msgId, options = {}) {
|
|
91
99
|
if (!agent.feishuProfile || !msgId || !msgId.startsWith("om_")) {
|
|
92
100
|
this.log(`👀 跳过 agent=${agent.name} msg=${msgId || "?"} profile=${agent.feishuProfile || "缺"}(不满足点表情条件)`);
|
|
93
101
|
return;
|
|
@@ -100,7 +108,9 @@ class ProcessingEyeOrchestrator {
|
|
|
100
108
|
// add itself therefore establishes an active generation instead of
|
|
101
109
|
// erasing activity already observed for that turn.
|
|
102
110
|
state.sawActive = true;
|
|
111
|
+
state.turnStartedAt = this.now();
|
|
103
112
|
this.cancelCompletion(state);
|
|
113
|
+
this.cancelProgress(state);
|
|
104
114
|
if (state.fallbackTimer)
|
|
105
115
|
this.clearTimer(state.fallbackTimer);
|
|
106
116
|
const generation = state.gen;
|
|
@@ -112,6 +122,22 @@ class ProcessingEyeOrchestrator {
|
|
|
112
122
|
this.clear(agent, "15分钟兜底");
|
|
113
123
|
}, 15 * 60 * 1000);
|
|
114
124
|
state.fallbackTimer = fallbackTimer;
|
|
125
|
+
let progressTimer;
|
|
126
|
+
progressTimer = this.setTimer(() => {
|
|
127
|
+
const current = this.state.get(agent.agentId);
|
|
128
|
+
if (current?.gen !== generation || current.progressTimer !== progressTimer)
|
|
129
|
+
return;
|
|
130
|
+
current.progressTimer = null;
|
|
131
|
+
const pending = this.pending.get(agent.agentId) || [];
|
|
132
|
+
const anchor = pending.at(-1);
|
|
133
|
+
if (!anchor)
|
|
134
|
+
return;
|
|
135
|
+
const lastOutboundAt = this.options.lastOutboundAt?.(agent, anchor.target) ?? null;
|
|
136
|
+
if (lastOutboundAt !== null && lastOutboundAt >= current.turnStartedAt)
|
|
137
|
+
return;
|
|
138
|
+
this.sendProgress(agent, anchor);
|
|
139
|
+
}, 2 * 60 * 1000);
|
|
140
|
+
state.progressTimer = progressTimer;
|
|
115
141
|
this.larkApi(agent, "POST", `/open-apis/im/v1/messages/${msgId}/reactions`, { reaction_type: { emoji_type: "OnIt" } }, (error, result) => {
|
|
116
142
|
const reactionId = result?.data?.reaction_id;
|
|
117
143
|
if (!reactionId) {
|
|
@@ -124,7 +150,8 @@ class ProcessingEyeOrchestrator {
|
|
|
124
150
|
return;
|
|
125
151
|
}
|
|
126
152
|
const list = this.pending.get(agent.agentId) || [];
|
|
127
|
-
list.push({ msgId, reactionId })
|
|
153
|
+
list.push({ msgId, reactionId, ...(options.replyInThread ? { replyInThread: true } : {}),
|
|
154
|
+
...(options.target ? { target: options.target } : {}) });
|
|
128
155
|
this.pending.set(agent.agentId, list);
|
|
129
156
|
this.save(agent);
|
|
130
157
|
});
|
|
@@ -139,6 +166,7 @@ class ProcessingEyeOrchestrator {
|
|
|
139
166
|
state.fallbackTimer = null;
|
|
140
167
|
}
|
|
141
168
|
this.cancelCompletion(state);
|
|
169
|
+
this.cancelProgress(state);
|
|
142
170
|
}
|
|
143
171
|
const list = this.pending.get(agent.agentId) || [];
|
|
144
172
|
if (!list.length) {
|
|
@@ -157,6 +185,25 @@ class ProcessingEyeOrchestrator {
|
|
|
157
185
|
// 执行结束(含正常完成):只摘除 👀,不再追加任何完成 reaction(用户 2026-08-12 要求)。
|
|
158
186
|
this.log(`👀 已摘 agent=${agent.name} n=${list.length} 原因=${reason || "?"}`);
|
|
159
187
|
}
|
|
188
|
+
sendProgress(agent, anchor) {
|
|
189
|
+
const selected = this.options.cliForAgent?.(agent) ?? (() => {
|
|
190
|
+
const managed = (0, agent_lark_cli_workspace_js_1.managedOfficialLarkCli)(agent, this.options.envForAgent?.(agent) ?? process.env);
|
|
191
|
+
return { command: managed.command.command, argsPrefix: managed.command.argsPrefix, env: managed.env };
|
|
192
|
+
})();
|
|
193
|
+
const args = ["im", "+messages-reply", "--message-id", anchor.msgId,
|
|
194
|
+
...(anchor.replyInThread ? ["--reply-in-thread"] : []),
|
|
195
|
+
"--text", "我还在处理中,完成后会汇总更新。", "--json", "--as", "bot"];
|
|
196
|
+
this.execFile(selected.command, [...selected.argsPrefix, ...args], { encoding: "utf8", timeout: 10_000, env: selected.env }, (error, stdout, stderr) => {
|
|
197
|
+
if (error) {
|
|
198
|
+
const code = error.code;
|
|
199
|
+
const detail = String(stderr || "").trim().slice(0, 160) || String(stdout || "").trim().slice(0, 120);
|
|
200
|
+
this.log(`长任务进度发送失败 agent=${agent.name}: exit=${code ?? "?"}${detail ? ` | ${detail}` : ""}`);
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
this.options.recordOutboundAt?.(agent, anchor.target);
|
|
204
|
+
this.log(`长任务进度已发送 agent=${agent.name} msg=${anchor.msgId}`);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
160
207
|
observeActivity(agent, activity) {
|
|
161
208
|
const state = this.getState(agent);
|
|
162
209
|
if (activity === "error" || activity === "offline") {
|
|
@@ -12,6 +12,7 @@ export class ProcessingEyeOrchestrator {
|
|
|
12
12
|
recordError;
|
|
13
13
|
setTimer;
|
|
14
14
|
clearTimer;
|
|
15
|
+
now;
|
|
15
16
|
constructor(options = {}) {
|
|
16
17
|
this.options = options;
|
|
17
18
|
this.execFile = options.execFile ?? nodeExecFile;
|
|
@@ -19,6 +20,7 @@ export class ProcessingEyeOrchestrator {
|
|
|
19
20
|
this.recordError = options.recordStatusError ?? (() => { });
|
|
20
21
|
this.setTimer = options.setTimer ?? setTimeout;
|
|
21
22
|
this.clearTimer = options.clearTimer ?? clearTimeout;
|
|
23
|
+
this.now = options.now ?? Date.now;
|
|
22
24
|
}
|
|
23
25
|
larkApi(agent, method, apiPath, data, callback) {
|
|
24
26
|
const args = ["api", method, apiPath];
|
|
@@ -61,7 +63,7 @@ export class ProcessingEyeOrchestrator {
|
|
|
61
63
|
const existing = this.state.get(agent.agentId);
|
|
62
64
|
if (existing)
|
|
63
65
|
return existing;
|
|
64
|
-
const created = { sawActive: false, fallbackTimer: null, completionTimer: null, gen: 0 };
|
|
66
|
+
const created = { sawActive: false, fallbackTimer: null, completionTimer: null, progressTimer: null, turnStartedAt: 0, gen: 0 };
|
|
65
67
|
this.state.set(agent.agentId, created);
|
|
66
68
|
return created;
|
|
67
69
|
}
|
|
@@ -71,6 +73,12 @@ export class ProcessingEyeOrchestrator {
|
|
|
71
73
|
this.clearTimer(state.completionTimer);
|
|
72
74
|
state.completionTimer = null;
|
|
73
75
|
}
|
|
76
|
+
cancelProgress(state) {
|
|
77
|
+
if (!state.progressTimer)
|
|
78
|
+
return;
|
|
79
|
+
this.clearTimer(state.progressTimer);
|
|
80
|
+
state.progressTimer = null;
|
|
81
|
+
}
|
|
74
82
|
restoreAndClear(agent) {
|
|
75
83
|
let leftovers = [];
|
|
76
84
|
try {
|
|
@@ -84,7 +92,7 @@ export class ProcessingEyeOrchestrator {
|
|
|
84
92
|
this.pending.set(agent.agentId, leftovers);
|
|
85
93
|
this.clear(agent, "启动清扫遗留");
|
|
86
94
|
}
|
|
87
|
-
add(agent, msgId) {
|
|
95
|
+
add(agent, msgId, options = {}) {
|
|
88
96
|
if (!agent.feishuProfile || !msgId || !msgId.startsWith("om_")) {
|
|
89
97
|
this.log(`👀 跳过 agent=${agent.name} msg=${msgId || "?"} profile=${agent.feishuProfile || "缺"}(不满足点表情条件)`);
|
|
90
98
|
return;
|
|
@@ -97,7 +105,9 @@ export class ProcessingEyeOrchestrator {
|
|
|
97
105
|
// add itself therefore establishes an active generation instead of
|
|
98
106
|
// erasing activity already observed for that turn.
|
|
99
107
|
state.sawActive = true;
|
|
108
|
+
state.turnStartedAt = this.now();
|
|
100
109
|
this.cancelCompletion(state);
|
|
110
|
+
this.cancelProgress(state);
|
|
101
111
|
if (state.fallbackTimer)
|
|
102
112
|
this.clearTimer(state.fallbackTimer);
|
|
103
113
|
const generation = state.gen;
|
|
@@ -109,6 +119,22 @@ export class ProcessingEyeOrchestrator {
|
|
|
109
119
|
this.clear(agent, "15分钟兜底");
|
|
110
120
|
}, 15 * 60 * 1000);
|
|
111
121
|
state.fallbackTimer = fallbackTimer;
|
|
122
|
+
let progressTimer;
|
|
123
|
+
progressTimer = this.setTimer(() => {
|
|
124
|
+
const current = this.state.get(agent.agentId);
|
|
125
|
+
if (current?.gen !== generation || current.progressTimer !== progressTimer)
|
|
126
|
+
return;
|
|
127
|
+
current.progressTimer = null;
|
|
128
|
+
const pending = this.pending.get(agent.agentId) || [];
|
|
129
|
+
const anchor = pending.at(-1);
|
|
130
|
+
if (!anchor)
|
|
131
|
+
return;
|
|
132
|
+
const lastOutboundAt = this.options.lastOutboundAt?.(agent, anchor.target) ?? null;
|
|
133
|
+
if (lastOutboundAt !== null && lastOutboundAt >= current.turnStartedAt)
|
|
134
|
+
return;
|
|
135
|
+
this.sendProgress(agent, anchor);
|
|
136
|
+
}, 2 * 60 * 1000);
|
|
137
|
+
state.progressTimer = progressTimer;
|
|
112
138
|
this.larkApi(agent, "POST", `/open-apis/im/v1/messages/${msgId}/reactions`, { reaction_type: { emoji_type: "OnIt" } }, (error, result) => {
|
|
113
139
|
const reactionId = result?.data?.reaction_id;
|
|
114
140
|
if (!reactionId) {
|
|
@@ -121,7 +147,8 @@ export class ProcessingEyeOrchestrator {
|
|
|
121
147
|
return;
|
|
122
148
|
}
|
|
123
149
|
const list = this.pending.get(agent.agentId) || [];
|
|
124
|
-
list.push({ msgId, reactionId })
|
|
150
|
+
list.push({ msgId, reactionId, ...(options.replyInThread ? { replyInThread: true } : {}),
|
|
151
|
+
...(options.target ? { target: options.target } : {}) });
|
|
125
152
|
this.pending.set(agent.agentId, list);
|
|
126
153
|
this.save(agent);
|
|
127
154
|
});
|
|
@@ -136,6 +163,7 @@ export class ProcessingEyeOrchestrator {
|
|
|
136
163
|
state.fallbackTimer = null;
|
|
137
164
|
}
|
|
138
165
|
this.cancelCompletion(state);
|
|
166
|
+
this.cancelProgress(state);
|
|
139
167
|
}
|
|
140
168
|
const list = this.pending.get(agent.agentId) || [];
|
|
141
169
|
if (!list.length) {
|
|
@@ -154,6 +182,25 @@ export class ProcessingEyeOrchestrator {
|
|
|
154
182
|
// 执行结束(含正常完成):只摘除 👀,不再追加任何完成 reaction(用户 2026-08-12 要求)。
|
|
155
183
|
this.log(`👀 已摘 agent=${agent.name} n=${list.length} 原因=${reason || "?"}`);
|
|
156
184
|
}
|
|
185
|
+
sendProgress(agent, anchor) {
|
|
186
|
+
const selected = this.options.cliForAgent?.(agent) ?? (() => {
|
|
187
|
+
const managed = managedOfficialLarkCli(agent, this.options.envForAgent?.(agent) ?? process.env);
|
|
188
|
+
return { command: managed.command.command, argsPrefix: managed.command.argsPrefix, env: managed.env };
|
|
189
|
+
})();
|
|
190
|
+
const args = ["im", "+messages-reply", "--message-id", anchor.msgId,
|
|
191
|
+
...(anchor.replyInThread ? ["--reply-in-thread"] : []),
|
|
192
|
+
"--text", "我还在处理中,完成后会汇总更新。", "--json", "--as", "bot"];
|
|
193
|
+
this.execFile(selected.command, [...selected.argsPrefix, ...args], { encoding: "utf8", timeout: 10_000, env: selected.env }, (error, stdout, stderr) => {
|
|
194
|
+
if (error) {
|
|
195
|
+
const code = error.code;
|
|
196
|
+
const detail = String(stderr || "").trim().slice(0, 160) || String(stdout || "").trim().slice(0, 120);
|
|
197
|
+
this.log(`长任务进度发送失败 agent=${agent.name}: exit=${code ?? "?"}${detail ? ` | ${detail}` : ""}`);
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
this.options.recordOutboundAt?.(agent, anchor.target);
|
|
201
|
+
this.log(`长任务进度已发送 agent=${agent.name} msg=${anchor.msgId}`);
|
|
202
|
+
});
|
|
203
|
+
}
|
|
157
204
|
observeActivity(agent, activity) {
|
|
158
205
|
const state = this.getState(agent);
|
|
159
206
|
if (activity === "error" || activity === "offline") {
|
|
@@ -37,6 +37,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
37
37
|
};
|
|
38
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
39
|
exports.memberNamesFromPayloads = memberNamesFromPayloads;
|
|
40
|
+
exports.lastOutboundAtForTarget = lastOutboundAtForTarget;
|
|
40
41
|
exports.createHostShell = createHostShell;
|
|
41
42
|
const node_child_process_1 = require("node:child_process");
|
|
42
43
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
@@ -49,6 +50,7 @@ const workspace_service_js_1 = require("../platform/workspace-service.cjs");
|
|
|
49
50
|
const agent_state_store_js_1 = require("../agent/agent-state-store.cjs");
|
|
50
51
|
const host_business_state_js_1 = require("./host-business-state.cjs");
|
|
51
52
|
const host_processing_eye_js_1 = require("./host-processing-eye.cjs");
|
|
53
|
+
const im_freshness_adapter_js_1 = require("./im-freshness-adapter.cjs");
|
|
52
54
|
const inbox_projection_js_1 = require("../agent/inbox-projection.cjs");
|
|
53
55
|
const host_reminder_orchestrator_js_1 = require("../agent/host-reminder-orchestrator.cjs");
|
|
54
56
|
const inbox_audit_heartbeat_js_1 = require("../agent/inbox-audit-heartbeat.cjs");
|
|
@@ -120,6 +122,34 @@ function memberNamesFromPayloads(payloads) {
|
|
|
120
122
|
return names;
|
|
121
123
|
}
|
|
122
124
|
function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
|
|
125
|
+
function canonicalImTarget(target) {
|
|
126
|
+
if (!target)
|
|
127
|
+
return null;
|
|
128
|
+
try {
|
|
129
|
+
return (0, im_freshness_adapter_js_1.serializeFeishuImTarget)((0, im_freshness_adapter_js_1.feishuImTarget)(target));
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
return null;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
function lastOutboundAtForTarget(store, target) {
|
|
136
|
+
const state = store.readJson("freshnessState", {});
|
|
137
|
+
const canonicalTarget = canonicalImTarget(target);
|
|
138
|
+
const value = canonicalTarget ? state.last_outbound_by_target?.[canonicalTarget] : state.last_outbound_at;
|
|
139
|
+
const timestamp = typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
140
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
141
|
+
}
|
|
142
|
+
function recordOutboundAtForTarget(store, target) {
|
|
143
|
+
const canonicalTarget = canonicalImTarget(target);
|
|
144
|
+
store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
145
|
+
const at = new Date().toISOString();
|
|
146
|
+
state.last_outbound_at = at;
|
|
147
|
+
if (!canonicalTarget)
|
|
148
|
+
return;
|
|
149
|
+
state.last_outbound_by_target ??= {};
|
|
150
|
+
state.last_outbound_by_target[canonicalTarget] = at;
|
|
151
|
+
});
|
|
152
|
+
}
|
|
123
153
|
function safeRuntimeStatusDiagnostic(error) {
|
|
124
154
|
return (0, provider_error_classifier_js_1.safeProviderDiagnostic)(error, "Runtime entered an error state", 500);
|
|
125
155
|
}
|
|
@@ -338,6 +368,8 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
338
368
|
recordStatusError: (agent, text) => hostState.recordStatusError(agent, text),
|
|
339
369
|
readPending: (agent) => stateStore(agent).readJson("pendingReact", {}).items || [],
|
|
340
370
|
writePending: (agent, items) => stateStore(agent).writeJson("pendingReact", { items }),
|
|
371
|
+
lastOutboundAt: (agent, target) => lastOutboundAtForTarget(stateStore(agent), target),
|
|
372
|
+
recordOutboundAt: (agent, target) => recordOutboundAtForTarget(stateStore(agent), target),
|
|
341
373
|
cliForAgent: (agent) => {
|
|
342
374
|
const managed = managedCliForAgent(agent);
|
|
343
375
|
return { command: managed.command.command, argsPrefix: managed.command.argsPrefix, env: managed.env };
|
|
@@ -567,8 +599,12 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
567
599
|
excerpt: (0, host_business_state_js_1.safeConversationExcerpt)(event.content, 180),
|
|
568
600
|
at: new Date().toISOString(),
|
|
569
601
|
}, 30);
|
|
570
|
-
if (inboxEnvelope.sender_type === "human" || inboxEnvelope.sender_type === "agent")
|
|
571
|
-
processingEyes.add(agent, String(inboxEnvelope.message_id || "")
|
|
602
|
+
if (inboxEnvelope.sender_type === "human" || inboxEnvelope.sender_type === "agent") {
|
|
603
|
+
processingEyes.add(agent, String(inboxEnvelope.message_id || ""), {
|
|
604
|
+
replyInThread: String(inboxEnvelope.target || "").startsWith("thread:"),
|
|
605
|
+
target: String(inboxEnvelope.target || ""),
|
|
606
|
+
});
|
|
607
|
+
}
|
|
572
608
|
if (receipt.status === "deferred")
|
|
573
609
|
log(`Runtime 暂缓投递,消息保留在 inbox seq=${inboxEnvelope.seq}: ${receipt.reason}`);
|
|
574
610
|
}
|
|
@@ -1563,9 +1599,9 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1563
1599
|
const status = hostState.readStatus(agent);
|
|
1564
1600
|
const turns = status.session && typeof status.session === "object"
|
|
1565
1601
|
? Math.max(0, Number(status.session.turns) || 0) : 0;
|
|
1566
|
-
const
|
|
1567
|
-
?
|
|
1568
|
-
|
|
1602
|
+
const sessionCurrent = (0, agent_readiness_js_1.isSessionCurrent)(status.session && typeof status.session === "object"
|
|
1603
|
+
? status.session
|
|
1604
|
+
: null, daemonStartedAt);
|
|
1569
1605
|
const runtimeReady = sessionCurrent && (0, agent_readiness_js_1.isRuntimeReadinessCurrent)(status.runtimeReadiness, daemonStartedAt);
|
|
1570
1606
|
const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
|
|
1571
1607
|
return { turns, runtimeReady, ...connectionState(status), pendingCount };
|
|
@@ -1650,9 +1686,9 @@ function createHostShell({ env = process.env, runtimeHost, channelPackage, event
|
|
|
1650
1686
|
const status = hostState.readStatus(agent);
|
|
1651
1687
|
const turns = status.session && typeof status.session === "object"
|
|
1652
1688
|
? Math.max(0, Number(status.session.turns) || 0) : 0;
|
|
1653
|
-
const
|
|
1654
|
-
?
|
|
1655
|
-
|
|
1689
|
+
const sessionCurrent = (0, agent_readiness_js_1.isSessionCurrent)(status.session && typeof status.session === "object"
|
|
1690
|
+
? status.session
|
|
1691
|
+
: null, daemonStartedAt);
|
|
1656
1692
|
const runtimeReady = sessionCurrent && (0, agent_readiness_js_1.isRuntimeReadinessCurrent)(status.runtimeReadiness, daemonStartedAt);
|
|
1657
1693
|
const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
|
|
1658
1694
|
return { turns, runtimeReady, ...connectionState(status), pendingCount };
|
|
@@ -9,6 +9,7 @@ import { reconcileAgentWorkspace } from "../platform/workspace-service.mjs";
|
|
|
9
9
|
import { createAgentStateStore } from "../agent/agent-state-store.mjs";
|
|
10
10
|
import { HostEnvelopeProjector, HostStateProjection, SenderIdentityCache, projectActivityStatus, projectSessionStatus, safeConversationExcerpt, shouldPreventiveReconnect, } from "./host-business-state.mjs";
|
|
11
11
|
import { ProcessingEyeOrchestrator } from "./host-processing-eye.mjs";
|
|
12
|
+
import { feishuImTarget, serializeFeishuImTarget } from "./im-freshness-adapter.mjs";
|
|
12
13
|
import { projectInboxEnvelope, targetKeyOfInboxEnvelope } from "../agent/inbox-projection.mjs";
|
|
13
14
|
import { HostReminderOrchestrator } from "../agent/host-reminder-orchestrator.mjs";
|
|
14
15
|
import { boundedInboxAuditDiagnostic, InboxAuditHeartbeat, INBOX_AUDIT_CADENCE_MS } from "../agent/inbox-audit-heartbeat.mjs";
|
|
@@ -22,7 +23,7 @@ import { readDocumentCommentSubscription, verifyCallbackProbe } from "../platfor
|
|
|
22
23
|
import { loadConfig, resolveInboxAuditSchedule, resolveMentionPolicy } from "../platform/config.mjs";
|
|
23
24
|
import { processCommandToken } from "../app/internal-command.mjs";
|
|
24
25
|
import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
|
|
25
|
-
import { isChannelReconnecting, isRuntimeReadinessCurrent } from "../app/agent-readiness.mjs";
|
|
26
|
+
import { isChannelReconnecting, isRuntimeReadinessCurrent, isSessionCurrent } from "../app/agent-readiness.mjs";
|
|
26
27
|
import { documentCommentMessageId, documentCommentNoticeType, projectDocumentCommentEnvelope, resolveDocumentCommentContext, } from "./document-comment.mjs";
|
|
27
28
|
import { isOpenPlatformHost, requireOpenDomain } from "./platform-hosts.mjs";
|
|
28
29
|
const PENDING_DOCUMENT_COMMENT_LIMIT = 256;
|
|
@@ -80,6 +81,34 @@ export function memberNamesFromPayloads(payloads) {
|
|
|
80
81
|
return names;
|
|
81
82
|
}
|
|
82
83
|
function errorMessage(error) { return error instanceof Error ? error.message : String(error); }
|
|
84
|
+
function canonicalImTarget(target) {
|
|
85
|
+
if (!target)
|
|
86
|
+
return null;
|
|
87
|
+
try {
|
|
88
|
+
return serializeFeishuImTarget(feishuImTarget(target));
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
export function lastOutboundAtForTarget(store, target) {
|
|
95
|
+
const state = store.readJson("freshnessState", {});
|
|
96
|
+
const canonicalTarget = canonicalImTarget(target);
|
|
97
|
+
const value = canonicalTarget ? state.last_outbound_by_target?.[canonicalTarget] : state.last_outbound_at;
|
|
98
|
+
const timestamp = typeof value === "string" ? Date.parse(value) : Number.NaN;
|
|
99
|
+
return Number.isFinite(timestamp) ? timestamp : null;
|
|
100
|
+
}
|
|
101
|
+
function recordOutboundAtForTarget(store, target) {
|
|
102
|
+
const canonicalTarget = canonicalImTarget(target);
|
|
103
|
+
store.mutateJson("freshnessState", { version: 1, cursors: {} }, (state) => {
|
|
104
|
+
const at = new Date().toISOString();
|
|
105
|
+
state.last_outbound_at = at;
|
|
106
|
+
if (!canonicalTarget)
|
|
107
|
+
return;
|
|
108
|
+
state.last_outbound_by_target ??= {};
|
|
109
|
+
state.last_outbound_by_target[canonicalTarget] = at;
|
|
110
|
+
});
|
|
111
|
+
}
|
|
83
112
|
function safeRuntimeStatusDiagnostic(error) {
|
|
84
113
|
return safeProviderDiagnostic(error, "Runtime entered an error state", 500);
|
|
85
114
|
}
|
|
@@ -298,6 +327,8 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
298
327
|
recordStatusError: (agent, text) => hostState.recordStatusError(agent, text),
|
|
299
328
|
readPending: (agent) => stateStore(agent).readJson("pendingReact", {}).items || [],
|
|
300
329
|
writePending: (agent, items) => stateStore(agent).writeJson("pendingReact", { items }),
|
|
330
|
+
lastOutboundAt: (agent, target) => lastOutboundAtForTarget(stateStore(agent), target),
|
|
331
|
+
recordOutboundAt: (agent, target) => recordOutboundAtForTarget(stateStore(agent), target),
|
|
301
332
|
cliForAgent: (agent) => {
|
|
302
333
|
const managed = managedCliForAgent(agent);
|
|
303
334
|
return { command: managed.command.command, argsPrefix: managed.command.argsPrefix, env: managed.env };
|
|
@@ -527,8 +558,12 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
527
558
|
excerpt: safeConversationExcerpt(event.content, 180),
|
|
528
559
|
at: new Date().toISOString(),
|
|
529
560
|
}, 30);
|
|
530
|
-
if (inboxEnvelope.sender_type === "human" || inboxEnvelope.sender_type === "agent")
|
|
531
|
-
processingEyes.add(agent, String(inboxEnvelope.message_id || "")
|
|
561
|
+
if (inboxEnvelope.sender_type === "human" || inboxEnvelope.sender_type === "agent") {
|
|
562
|
+
processingEyes.add(agent, String(inboxEnvelope.message_id || ""), {
|
|
563
|
+
replyInThread: String(inboxEnvelope.target || "").startsWith("thread:"),
|
|
564
|
+
target: String(inboxEnvelope.target || ""),
|
|
565
|
+
});
|
|
566
|
+
}
|
|
532
567
|
if (receipt.status === "deferred")
|
|
533
568
|
log(`Runtime 暂缓投递,消息保留在 inbox seq=${inboxEnvelope.seq}: ${receipt.reason}`);
|
|
534
569
|
}
|
|
@@ -1523,9 +1558,9 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
1523
1558
|
const status = hostState.readStatus(agent);
|
|
1524
1559
|
const turns = status.session && typeof status.session === "object"
|
|
1525
1560
|
? Math.max(0, Number(status.session.turns) || 0) : 0;
|
|
1526
|
-
const
|
|
1527
|
-
?
|
|
1528
|
-
|
|
1561
|
+
const sessionCurrent = isSessionCurrent(status.session && typeof status.session === "object"
|
|
1562
|
+
? status.session
|
|
1563
|
+
: null, daemonStartedAt);
|
|
1529
1564
|
const runtimeReady = sessionCurrent && isRuntimeReadinessCurrent(status.runtimeReadiness, daemonStartedAt);
|
|
1530
1565
|
const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
|
|
1531
1566
|
return { turns, runtimeReady, ...connectionState(status), pendingCount };
|
|
@@ -1610,9 +1645,9 @@ export function createHostShell({ env = process.env, runtimeHost, channelPackage
|
|
|
1610
1645
|
const status = hostState.readStatus(agent);
|
|
1611
1646
|
const turns = status.session && typeof status.session === "object"
|
|
1612
1647
|
? Math.max(0, Number(status.session.turns) || 0) : 0;
|
|
1613
|
-
const
|
|
1614
|
-
?
|
|
1615
|
-
|
|
1648
|
+
const sessionCurrent = isSessionCurrent(status.session && typeof status.session === "object"
|
|
1649
|
+
? status.session
|
|
1650
|
+
: null, daemonStartedAt);
|
|
1616
1651
|
const runtimeReady = sessionCurrent && isRuntimeReadinessCurrent(status.runtimeReadiness, daemonStartedAt);
|
|
1617
1652
|
const pendingCount = stateStore(agent).withInboxTransaction(() => stateStore(agent).readNdjson("inbox").length);
|
|
1618
1653
|
return { turns, runtimeReady, ...connectionState(status), pendingCount };
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.feishuImFreshnessAdapter = void 0;
|
|
4
|
+
exports.messageRevisionTime = messageRevisionTime;
|
|
5
|
+
exports.feishuImTarget = feishuImTarget;
|
|
6
|
+
exports.serializeFeishuImTarget = serializeFeishuImTarget;
|
|
7
|
+
exports.mergeFeishuImCursor = mergeFeishuImCursor;
|
|
8
|
+
function integerMillis(value) {
|
|
9
|
+
if (typeof value !== "string" || !/^[0-9]+$/.test(value))
|
|
10
|
+
return null;
|
|
11
|
+
try {
|
|
12
|
+
return BigInt(value);
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
function messageRevisionTime(message) {
|
|
19
|
+
const created = integerMillis(message.create_time);
|
|
20
|
+
const updated = message.update_time === undefined || message.update_time === "" ? created : integerMillis(message.update_time);
|
|
21
|
+
if (created === null || updated === null)
|
|
22
|
+
throw new Error(`malformed Feishu IM revision for ${message.message_id || "unknown message"}`);
|
|
23
|
+
return created > updated ? created : updated;
|
|
24
|
+
}
|
|
25
|
+
function feishuImTarget(value) {
|
|
26
|
+
const thread = /^thread:([^:]+):(.+)$/.exec(value);
|
|
27
|
+
if (thread)
|
|
28
|
+
return { provider: "feishu.im", resourceKind: "thread", resourceId: `${thread[1]}/${thread[2]}` };
|
|
29
|
+
const chat = /^chat:(.+)$/.exec(value);
|
|
30
|
+
if (chat)
|
|
31
|
+
return { provider: "feishu.im", resourceKind: "chat", resourceId: chat[1] };
|
|
32
|
+
throw new Error(`unsupported Feishu IM freshness target: ${value}`);
|
|
33
|
+
}
|
|
34
|
+
function serializeFeishuImTarget(target) {
|
|
35
|
+
return `${target.provider}/${target.resourceKind}/${target.resourceId}`;
|
|
36
|
+
}
|
|
37
|
+
function normalizedMessages(snapshot) {
|
|
38
|
+
if (!snapshot || !Array.isArray(snapshot.messages))
|
|
39
|
+
throw new Error("malformed Feishu IM history payload: messages must be an array");
|
|
40
|
+
return snapshot.messages.map((message) => {
|
|
41
|
+
if (!message || typeof message !== "object" || typeof message.message_id !== "string" || !message.message_id) {
|
|
42
|
+
throw new Error("malformed Feishu IM history payload: message_id is required");
|
|
43
|
+
}
|
|
44
|
+
return { ...message, revision: messageRevisionTime(message) };
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
exports.feishuImFreshnessAdapter = {
|
|
48
|
+
cursor(snapshot) {
|
|
49
|
+
const messages = normalizedMessages(snapshot);
|
|
50
|
+
if (!messages.length)
|
|
51
|
+
return null;
|
|
52
|
+
const revision = messages.reduce((maximum, message) => message.revision > maximum ? message.revision : maximum, 0n);
|
|
53
|
+
return {
|
|
54
|
+
schema: 1,
|
|
55
|
+
revisionTime: revision.toString(),
|
|
56
|
+
messageIds: [...new Set(messages.filter((message) => message.revision === revision).map((message) => message.message_id))].sort(),
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
compare(seen, current) {
|
|
60
|
+
if (!current)
|
|
61
|
+
return seen ? "gap" : "fresh";
|
|
62
|
+
if (!seen)
|
|
63
|
+
return "conflict";
|
|
64
|
+
const seenTime = integerMillis(seen.revisionTime);
|
|
65
|
+
const currentTime = integerMillis(current.revisionTime);
|
|
66
|
+
if (seen.schema !== 1 || seenTime === null || currentTime === null)
|
|
67
|
+
return "gap";
|
|
68
|
+
if (currentTime < seenTime)
|
|
69
|
+
return "gap";
|
|
70
|
+
if (currentTime > seenTime)
|
|
71
|
+
return "conflict";
|
|
72
|
+
return current.messageIds.every((id) => seen.messageIds.includes(id)) ? "fresh" : "conflict";
|
|
73
|
+
},
|
|
74
|
+
unseen(seen, snapshot) {
|
|
75
|
+
const messages = normalizedMessages(snapshot);
|
|
76
|
+
if (!seen)
|
|
77
|
+
return messages.map(({ revision: _revision, ...message }) => message);
|
|
78
|
+
const seenTime = integerMillis(seen.revisionTime);
|
|
79
|
+
if (seenTime === null)
|
|
80
|
+
return messages.map(({ revision: _revision, ...message }) => message);
|
|
81
|
+
return messages.filter((message) => message.revision > seenTime
|
|
82
|
+
|| (message.revision === seenTime && !seen.messageIds.includes(message.message_id)))
|
|
83
|
+
.map(({ revision: _revision, ...message }) => message);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
function mergeFeishuImCursor(left, right) {
|
|
87
|
+
if (!left)
|
|
88
|
+
return { ...right, messageIds: [...right.messageIds] };
|
|
89
|
+
const leftTime = BigInt(left.revisionTime);
|
|
90
|
+
const rightTime = BigInt(right.revisionTime);
|
|
91
|
+
if (rightTime < leftTime)
|
|
92
|
+
return left;
|
|
93
|
+
if (rightTime > leftTime)
|
|
94
|
+
return { ...right, messageIds: [...right.messageIds] };
|
|
95
|
+
return { schema: 1, revisionTime: left.revisionTime, messageIds: [...new Set([...left.messageIds, ...right.messageIds])].sort() };
|
|
96
|
+
}
|