larkin 0.2.70

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.
Files changed (144) hide show
  1. package/CONTRIBUTING.md +48 -0
  2. package/LICENSE +201 -0
  3. package/README.md +122 -0
  4. package/SECURITY.md +14 -0
  5. package/artifacts/release/THIRD_PARTY_NOTICES.txt +5488 -0
  6. package/dist/agent/agent-cli-capabilities.cjs +43 -0
  7. package/dist/agent/agent-cli-capabilities.mjs +35 -0
  8. package/dist/agent/agent-state-store.cjs +961 -0
  9. package/dist/agent/agent-state-store.mjs +923 -0
  10. package/dist/agent/agent-transport.cjs +201 -0
  11. package/dist/agent/agent-transport.mjs +194 -0
  12. package/dist/agent/config-cli-contract.cjs +19 -0
  13. package/dist/agent/config-cli-contract.mjs +16 -0
  14. package/dist/agent/context-prompt.cjs +159 -0
  15. package/dist/agent/context-prompt.mjs +155 -0
  16. package/dist/agent/freshness-gate.mjs +24 -0
  17. package/dist/agent/history-projection.cjs +72 -0
  18. package/dist/agent/history-projection.mjs +65 -0
  19. package/dist/agent/host-reminder-orchestrator.cjs +289 -0
  20. package/dist/agent/host-reminder-orchestrator.mjs +252 -0
  21. package/dist/agent/inbox-projection.cjs +92 -0
  22. package/dist/agent/inbox-projection.mjs +85 -0
  23. package/dist/agent/interaction-state-machine.cjs +710 -0
  24. package/dist/agent/interaction-state-machine.mjs +705 -0
  25. package/dist/agent/reminder-routes.cjs +192 -0
  26. package/dist/agent/reminder-routes.mjs +156 -0
  27. package/dist/agent/reminder-store.cjs +220 -0
  28. package/dist/agent/reminder-store.mjs +205 -0
  29. package/dist/agent/transport-business-context.cjs +486 -0
  30. package/dist/agent/transport-business-context.mjs +446 -0
  31. package/dist/agent/transport-shell.cjs +45 -0
  32. package/dist/agent/transport-shell.mjs +39 -0
  33. package/dist/app/agent-cli.mjs +596 -0
  34. package/dist/app/agent-config.mjs +633 -0
  35. package/dist/app/agent-lark-cli-workspace.cjs +95 -0
  36. package/dist/app/agent-lark-cli-workspace.mjs +85 -0
  37. package/dist/app/agent-readiness.cjs +42 -0
  38. package/dist/app/agent-readiness.mjs +38 -0
  39. package/dist/app/binary-entry.mjs +86 -0
  40. package/dist/app/cli.mjs +176 -0
  41. package/dist/app/dashboard.mjs +205 -0
  42. package/dist/app/internal-command.cjs +57 -0
  43. package/dist/app/internal-command.mjs +48 -0
  44. package/dist/app/lark-channel-secret.mjs +22 -0
  45. package/dist/app/lark-cli.mjs +887 -0
  46. package/dist/app/lark.mjs +45 -0
  47. package/dist/app/local-control.mjs +728 -0
  48. package/dist/app/official-lark-cli.cjs +138 -0
  49. package/dist/app/official-lark-cli.mjs +128 -0
  50. package/dist/app/pi-auth-cli.mjs +67 -0
  51. package/dist/app/run.mjs +364 -0
  52. package/dist/app/runtime-agent-config.mjs +416 -0
  53. package/dist/app/runtime-model-directory.mjs +78 -0
  54. package/dist/app/runtime-process.mjs +113 -0
  55. package/dist/app/session-cli.mjs +83 -0
  56. package/dist/app/setup-dashboard.mjs +26 -0
  57. package/dist/app/setup.mjs +166 -0
  58. package/dist/app/telemetry.mjs +83 -0
  59. package/dist/dashboard/dashboard-assets.mjs +21 -0
  60. package/dist/dashboard/dashboard-avatar.mjs +128 -0
  61. package/dist/dashboard/dashboard-config-controller.mjs +525 -0
  62. package/dist/dashboard/dashboard-lifecycle.cjs +97 -0
  63. package/dist/dashboard/dashboard-lifecycle.mjs +88 -0
  64. package/dist/dashboard/dashboard-template.mjs +29 -0
  65. package/dist/dashboard/dashboard-view-model.mjs +556 -0
  66. package/dist/dashboard/dashboard-workspace.mjs +104 -0
  67. package/dist/dashboard/web/assets/dashboard.css +3 -0
  68. package/dist/dashboard/web/assets/dashboard.js +49 -0
  69. package/dist/feishu/channel-transport.cjs +151 -0
  70. package/dist/feishu/channel-transport.mjs +148 -0
  71. package/dist/feishu/document-comment.cjs +120 -0
  72. package/dist/feishu/document-comment.mjs +109 -0
  73. package/dist/feishu/host-business-state.cjs +477 -0
  74. package/dist/feishu/host-business-state.mjs +462 -0
  75. package/dist/feishu/host-channel-business.cjs +161 -0
  76. package/dist/feishu/host-channel-business.mjs +157 -0
  77. package/dist/feishu/host-processing-eye.cjs +185 -0
  78. package/dist/feishu/host-processing-eye.mjs +181 -0
  79. package/dist/feishu/host-shell.cjs +1425 -0
  80. package/dist/feishu/host-shell.mjs +1385 -0
  81. package/dist/feishu/im-freshness-adapter.mjs +89 -0
  82. package/dist/feishu/interaction-orchestrator.cjs +279 -0
  83. package/dist/feishu/interaction-orchestrator.mjs +242 -0
  84. package/dist/feishu/lark-passthrough.mjs +70 -0
  85. package/dist/feishu/message-policy.cjs +111 -0
  86. package/dist/feishu/message-policy.mjs +98 -0
  87. package/dist/feishu/outbound-transport.cjs +221 -0
  88. package/dist/feishu/outbound-transport.mjs +215 -0
  89. package/dist/platform/build-info.cjs +25 -0
  90. package/dist/platform/build-info.mjs +18 -0
  91. package/dist/platform/callback-capability.cjs +301 -0
  92. package/dist/platform/callback-capability.mjs +252 -0
  93. package/dist/platform/check-bun-version.cjs +12 -0
  94. package/dist/platform/config.cjs +751 -0
  95. package/dist/platform/config.mjs +694 -0
  96. package/dist/platform/process-inspect.cjs +89 -0
  97. package/dist/platform/process-state.cjs +237 -0
  98. package/dist/platform/process-state.mjs +223 -0
  99. package/dist/platform/release-artifacts.mjs +61 -0
  100. package/dist/platform/root-layout.cjs +111 -0
  101. package/dist/platform/root-layout.mjs +72 -0
  102. package/dist/platform/telemetry-config.cjs +64 -0
  103. package/dist/platform/telemetry-config.mjs +56 -0
  104. package/dist/platform/telemetry-spool.cjs +661 -0
  105. package/dist/platform/telemetry-spool.mjs +653 -0
  106. package/dist/platform/telemetry-tracing.cjs +602 -0
  107. package/dist/platform/telemetry-tracing.mjs +596 -0
  108. package/dist/platform/telemetry-uploader.cjs +150 -0
  109. package/dist/platform/telemetry-uploader.mjs +146 -0
  110. package/dist/platform/workspace-service.cjs +469 -0
  111. package/dist/platform/workspace-service.mjs +430 -0
  112. package/dist/runtime/builtin-pi-assets.mjs +33 -0
  113. package/dist/runtime/claude-model-catalog.cjs +102 -0
  114. package/dist/runtime/claude-model-catalog.mjs +99 -0
  115. package/dist/runtime/codex-model-catalog.cjs +108 -0
  116. package/dist/runtime/codex-model-catalog.mjs +105 -0
  117. package/dist/runtime/pi-bash-timeout-extension.mjs +60 -0
  118. package/dist/runtime/pi-bash-timeout-injection.mjs +60 -0
  119. package/dist/runtime/pi-bash-timeout.bundle.js +43 -0
  120. package/dist/runtime/pi-model-catalog.cjs +116 -0
  121. package/dist/runtime/pi-model-catalog.mjs +75 -0
  122. package/dist/runtime/pi-official-auth.mjs +480 -0
  123. package/dist/runtime/pi-provider-config.cjs +252 -0
  124. package/dist/runtime/pi-provider-config.mjs +239 -0
  125. package/dist/runtime/pi-rpc-client.cjs +255 -0
  126. package/dist/runtime/pi-rpc-client.mjs +251 -0
  127. package/dist/runtime/pi-subagent-injection.mjs +129 -0
  128. package/dist/runtime/pi-subagents.bundle.js +8990 -0
  129. package/dist/runtime/runtime-adapters.mjs +1019 -0
  130. package/dist/runtime/runtime-contracts.cjs +2 -0
  131. package/dist/runtime/runtime-contracts.mjs +1 -0
  132. package/dist/runtime/runtime-host.cjs +930 -0
  133. package/dist/runtime/runtime-host.mjs +922 -0
  134. package/dist/runtime/runtime-model-catalog.cjs +43 -0
  135. package/dist/runtime/runtime-model-catalog.mjs +40 -0
  136. package/dist/runtime/runtime-readiness.cjs +177 -0
  137. package/dist/runtime/runtime-readiness.mjs +133 -0
  138. package/dist/setup/bot-register.mjs +655 -0
  139. package/dist/setup/grant-scopes.mjs +133 -0
  140. package/dist/setup/run-credential-preflight.mjs +89 -0
  141. package/dist/setup/setup-agent-choice.mjs +172 -0
  142. package/dist/setup/setup-bind.mjs +337 -0
  143. package/dist/setup/setup-binding.mjs +52 -0
  144. package/package.json +112 -0
@@ -0,0 +1,157 @@
1
+ import { decideWake, normalizeChannelMessage } from "./message-policy.mjs";
2
+ import { normalizeCardAction } from "@larksuite/channel";
3
+ function messageOf(error) { return error instanceof Error ? error.message : String(error); }
4
+ export class HostChannelBusiness {
5
+ options;
6
+ log;
7
+ now;
8
+ constructor(options) {
9
+ this.options = options;
10
+ this.log = options.log ?? (() => { });
11
+ this.now = options.now ?? (() => new Date());
12
+ }
13
+ handlers(agent, channel) {
14
+ return {
15
+ message: (message) => {
16
+ try {
17
+ const event = normalizeChannelMessage(message);
18
+ this.options.state.updateStatus(agent, { inboundVerifiedAt: this.now().toISOString() });
19
+ const senderIsBot = message.senderIsBot === true;
20
+ const decision = decideWake({
21
+ senderIsBot,
22
+ mentionedBot: message.mentionedBot === true,
23
+ mentionAll: message.mentionAll === true,
24
+ isGroup: (message.chatType || "group") !== "p2p",
25
+ mentionPolicy: senderIsBot ? "require" : this.options.mentionPolicy?.(agent.agentId, String(message.chatId || ""))
26
+ || (Array.isArray(agent.noMentionChats) && agent.noMentionChats.includes(String(message.chatId || "")) ? "free" : "require"),
27
+ });
28
+ if (!decision.wake) {
29
+ this.log(`${senderIsBot ? "bot 消息未点名@我" : "群消息未@bot"} → 只入箱不唤醒 agent=${agent.name} chat=${message.chatId}`);
30
+ }
31
+ void this.options.onMessage(agent, event, { wake: decision.wake });
32
+ }
33
+ catch (error) {
34
+ this.log(`channel message 处理失败 agent=${agent.name}: ${messageOf(error)}`);
35
+ }
36
+ },
37
+ comment: async (event) => {
38
+ try {
39
+ this.options.state.updateStatus(agent, {
40
+ inboundVerifiedAt: this.now().toISOString(),
41
+ documentCommentEventAt: this.now().toISOString(),
42
+ });
43
+ if (!channel || !this.options.onComment)
44
+ return;
45
+ await this.options.onComment(agent, event, channel);
46
+ }
47
+ catch (error) {
48
+ this.log(`document comment 处理失败 agent=${agent.name}: ${messageOf(error)}`);
49
+ this.options.state.recordStatusError(agent, `document comment: ${messageOf(error)}`);
50
+ }
51
+ },
52
+ cardAction: async (event) => {
53
+ if (!this.options.onCardAction)
54
+ return { toast: { type: "error", content: "交互能力未启用,请重新运行 larkin setup 并重启 Larkin。" } };
55
+ return this.options.onCardAction(agent, event);
56
+ },
57
+ reconnecting: () => {
58
+ this.log(`ws 重连中… agent=${agent.name}`);
59
+ this.options.state.updateStatus(agent, { reconnectingAt: this.now().toISOString() });
60
+ },
61
+ reconnected: () => {
62
+ this.log(`ws 已恢复 agent=${agent.name}`);
63
+ const connectedAt = this.now().toISOString();
64
+ this.options.state.updateStatus(agent, { reconnectingAt: null, reconnectedAt: connectedAt, connectedAt });
65
+ if (channel && this.options.onReconnected) {
66
+ void Promise.resolve(this.options.onReconnected(agent, channel)).catch((error) => {
67
+ this.log(`document comment 恢复重放失败 agent=${agent.name}: ${messageOf(error)}`);
68
+ });
69
+ }
70
+ },
71
+ error: () => {
72
+ this.log(`ws 错误 agent=${agent.name}`);
73
+ this.options.state.recordStatusError(agent, "channel ws 连接错误");
74
+ },
75
+ };
76
+ }
77
+ registerReadReceipts(agent, dispatcher) {
78
+ try {
79
+ dispatcher.register({
80
+ "im.message.message_read_v1": async (raw) => {
81
+ try {
82
+ const event = raw;
83
+ const reader = event?.reader?.reader_id?.open_id || null;
84
+ const readAt = event?.reader?.read_time || null;
85
+ const ids = event?.message_id_list || [];
86
+ this.options.state.recordReadReceipts(agent, reader, readAt, ids);
87
+ this.log(`已读回执 agent=${agent.name} reader=${reader || "?"} msgs=${ids.length}`);
88
+ }
89
+ catch (error) {
90
+ this.log(`已读回执处理失败 agent=${agent.name}: ${messageOf(error)}`);
91
+ }
92
+ },
93
+ });
94
+ }
95
+ catch (error) {
96
+ this.log(`已读回执注册失败(不影响消息收发) agent=${agent.name}: ${messageOf(error)}`);
97
+ }
98
+ }
99
+ /**
100
+ * Replace the SDK's action-value TTL dedup handler after channel.connect().
101
+ * Durable callback event_id ownership lives in InteractionStateMachine, so
102
+ * retries receive the same response while later state-version clicks remain legal.
103
+ */
104
+ registerCardActions(agent, dispatcher) {
105
+ dispatcher.register({
106
+ "card.action.trigger": async (raw) => {
107
+ const event = normalizeCardAction(raw, { includeRaw: true });
108
+ if (!event)
109
+ return { toast: { type: "error", content: "无法解析卡片操作,请刷新后重试。" } };
110
+ return this.handlers(agent).cardAction(event);
111
+ },
112
+ });
113
+ }
114
+ persistBotIdentity(agent, openId, name, via, avatarUrl = null) {
115
+ agent.botOpenId = openId;
116
+ agent.botName = name || agent.botName || null;
117
+ try {
118
+ let previous = {};
119
+ try {
120
+ previous = this.options.stateStore(agent).readJson("botIdentity", {}) || {};
121
+ }
122
+ catch { /* first run */ }
123
+ this.options.stateStore(agent).writeJson("botIdentity", {
124
+ open_id: openId,
125
+ name: name || previous.name || null,
126
+ avatar_url: avatarUrl || previous.avatar_url || null,
127
+ updated_at: this.now().toISOString(),
128
+ });
129
+ }
130
+ catch (error) {
131
+ this.log(`bot-identity 写入失败 agent=${agent.name}: ${messageOf(error)}`);
132
+ }
133
+ this.log(`bot 身份就绪(${via}) agent=${agent.name} bot=${name || "?"}(${openId})`);
134
+ this.options.state.updateStatus(agent, { connectedAt: this.now().toISOString(), connectedVia: via, reconnectingAt: null });
135
+ }
136
+ async connected(agent, channel, onFatal) {
137
+ const identity = channel.botIdentity || null;
138
+ if (!identity?.openId) {
139
+ this.log(`createLarkChannel 已连接但未取到 botIdentity agent=${agent.name}`);
140
+ onFatal?.(new Error("bot identity unavailable"));
141
+ return;
142
+ }
143
+ let avatarUrl = null;
144
+ let botName = identity.name || null;
145
+ try {
146
+ const info = await channel.rawClient?.request({ url: "/open-apis/bot/v3/info", method: "GET" });
147
+ if (info?.bot?.open_id === identity.openId) {
148
+ avatarUrl = info.bot.avatar_url || null;
149
+ botName = info.bot.app_name || botName;
150
+ }
151
+ }
152
+ catch (error) {
153
+ this.log(`bot 头像刷新失败(不影响连接) agent=${agent.name}: ${messageOf(error)}`);
154
+ }
155
+ this.persistBotIdentity(agent, identity.openId, botName, "channel", avatarUrl);
156
+ }
157
+ }
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ProcessingEyeOrchestrator = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const agent_lark_cli_workspace_js_1 = require("../app/agent-lark-cli-workspace.cjs");
6
+ function errorMessage(error) {
7
+ return error instanceof Error ? error.message : String(error || "?");
8
+ }
9
+ class ProcessingEyeOrchestrator {
10
+ options;
11
+ pending = new Map();
12
+ state = new Map();
13
+ execFile;
14
+ log;
15
+ recordError;
16
+ setTimer;
17
+ clearTimer;
18
+ constructor(options = {}) {
19
+ this.options = options;
20
+ this.execFile = options.execFile ?? node_child_process_1.execFile;
21
+ this.log = options.log ?? (() => { });
22
+ this.recordError = options.recordStatusError ?? (() => { });
23
+ this.setTimer = options.setTimer ?? setTimeout;
24
+ this.clearTimer = options.clearTimer ?? clearTimeout;
25
+ }
26
+ larkApi(agent, method, apiPath, data, callback) {
27
+ const args = ["api", method, apiPath];
28
+ if (data)
29
+ args.push("--data", JSON.stringify(data));
30
+ const selected = this.options.cliForAgent?.(agent) ?? (() => {
31
+ const managed = (0, agent_lark_cli_workspace_js_1.managedOfficialLarkCli)(agent, this.options.envForAgent?.(agent) ?? process.env);
32
+ return { command: managed.command.command, argsPrefix: managed.command.argsPrefix, env: managed.env };
33
+ })();
34
+ this.execFile(selected.command, [...selected.argsPrefix, ...args], { encoding: "utf8", timeout: 10_000,
35
+ env: selected.env }, (error, stdout, stderr) => {
36
+ let result = null;
37
+ try {
38
+ result = JSON.parse(String(stdout));
39
+ }
40
+ catch { /* non-JSON */ }
41
+ if (error && !result) {
42
+ const killed = "killed" in error && Boolean(error.killed);
43
+ // 不回显命令行:execFile 的 message 以整条命令开头,会占满 recentErrors 的 200 字符截断额度,
44
+ // 把真正可诊断的 exit 码和 stderr 挤出去(2026-07-17 三次偶发失败均因此无法定位)。
45
+ const code = error.code;
46
+ const head = String(stderr || "").trim().slice(0, 160) || String(stdout || "").trim().slice(0, 120);
47
+ const detail = (killed ? "超时(10s)被杀" : `exit=${code ?? "?"}`) + (head ? ` | ${head}` : "");
48
+ const endpoint = apiPath.split("/").slice(-2).join("/");
49
+ this.log(`larkApi ${method} ${endpoint} 失败 agent=${agent.name}: ${detail}`);
50
+ const processingReaction = /\/reactions(?:\/[^/]+)?$/.test(apiPath);
51
+ if (!processingReaction)
52
+ this.recordError(agent, `larkApi ${method} ${endpoint}: ${detail}`);
53
+ }
54
+ callback?.(error, result);
55
+ });
56
+ }
57
+ save(agent) {
58
+ try {
59
+ this.options.writePending?.(agent, this.pending.get(agent.agentId) || []);
60
+ }
61
+ catch { /* best effort */ }
62
+ }
63
+ getState(agent) {
64
+ const existing = this.state.get(agent.agentId);
65
+ if (existing)
66
+ return existing;
67
+ const created = { sawActive: false, fallbackTimer: null, completionTimer: null, gen: 0 };
68
+ this.state.set(agent.agentId, created);
69
+ return created;
70
+ }
71
+ cancelCompletion(state) {
72
+ if (!state.completionTimer)
73
+ return;
74
+ this.clearTimer(state.completionTimer);
75
+ state.completionTimer = null;
76
+ }
77
+ restoreAndClear(agent) {
78
+ let leftovers = [];
79
+ try {
80
+ leftovers = this.options.readPending?.(agent) || [];
81
+ }
82
+ catch {
83
+ return;
84
+ }
85
+ if (!leftovers.length)
86
+ return;
87
+ this.pending.set(agent.agentId, leftovers);
88
+ this.clear(agent, "启动清扫遗留");
89
+ }
90
+ add(agent, msgId) {
91
+ if (!agent.feishuProfile || !msgId || !msgId.startsWith("om_")) {
92
+ this.log(`👀 跳过 agent=${agent.name} msg=${msgId || "?"} profile=${agent.feishuProfile || "缺"}(不满足点表情条件)`);
93
+ return;
94
+ }
95
+ this.log(`👀 点上尝试 agent=${agent.name} msg=${msgId}`);
96
+ const state = this.getState(agent);
97
+ state.gen += 1;
98
+ // A successful delivery may emit turn-start before the Feishu POST is
99
+ // attempted, and a new inbound message may arrive during idle grace. The
100
+ // add itself therefore establishes an active generation instead of
101
+ // erasing activity already observed for that turn.
102
+ state.sawActive = true;
103
+ this.cancelCompletion(state);
104
+ if (state.fallbackTimer)
105
+ this.clearTimer(state.fallbackTimer);
106
+ const generation = state.gen;
107
+ let fallbackTimer;
108
+ fallbackTimer = this.setTimer(() => {
109
+ const current = this.state.get(agent.agentId);
110
+ if (current?.gen !== generation || current.fallbackTimer !== fallbackTimer)
111
+ return;
112
+ this.clear(agent, "15分钟兜底");
113
+ }, 15 * 60 * 1000);
114
+ state.fallbackTimer = fallbackTimer;
115
+ this.larkApi(agent, "POST", `/open-apis/im/v1/messages/${msgId}/reactions`, { reaction_type: { emoji_type: "OnIt" } }, (error, result) => {
116
+ const reactionId = result?.data?.reaction_id;
117
+ if (!reactionId) {
118
+ this.log(`👀 点上失败 msg=${msgId}: ${result ? JSON.stringify(result.error || result).slice(0, 120) : errorMessage(error)}`);
119
+ return;
120
+ }
121
+ if ((this.state.get(agent.agentId)?.gen ?? 0) !== generation) {
122
+ this.log(`👀 迟到回执自删 agent=${agent.name} msg=${msgId}(点上生效前执行已结束)`);
123
+ this.larkApi(agent, "DELETE", `/open-apis/im/v1/messages/${msgId}/reactions/${reactionId}`, null, () => { });
124
+ return;
125
+ }
126
+ const list = this.pending.get(agent.agentId) || [];
127
+ list.push({ msgId, reactionId });
128
+ this.pending.set(agent.agentId, list);
129
+ this.save(agent);
130
+ });
131
+ }
132
+ clear(agent, reason) {
133
+ const state = this.state.get(agent.agentId);
134
+ if (state) {
135
+ state.gen += 1;
136
+ state.sawActive = false;
137
+ if (state.fallbackTimer) {
138
+ this.clearTimer(state.fallbackTimer);
139
+ state.fallbackTimer = null;
140
+ }
141
+ this.cancelCompletion(state);
142
+ }
143
+ const list = this.pending.get(agent.agentId) || [];
144
+ if (!list.length) {
145
+ this.log(`👀 清除(无待摘) agent=${agent.name} 原因=${reason || "?"}`);
146
+ return;
147
+ }
148
+ this.pending.set(agent.agentId, []);
149
+ this.save(agent);
150
+ for (const { msgId, reactionId } of list) {
151
+ this.larkApi(agent, "DELETE", `/open-apis/im/v1/messages/${msgId}/reactions/${reactionId}`, null, (error, result) => {
152
+ if (error || result?.ok === false) {
153
+ this.log(`👀 摘除失败 msg=${msgId}: ${result ? JSON.stringify(result.error || {}).slice(0, 120) : errorMessage(error)}`);
154
+ }
155
+ });
156
+ }
157
+ this.log(`👀 已摘 agent=${agent.name} n=${list.length} 原因=${reason || "?"}`);
158
+ }
159
+ observeActivity(agent, activity) {
160
+ const state = this.getState(agent);
161
+ if (activity === "error" || activity === "offline") {
162
+ this.clear(agent, `activity:${activity}`);
163
+ return;
164
+ }
165
+ if (activity === "idle") {
166
+ if (!state.sawActive || state.completionTimer)
167
+ return;
168
+ const generation = state.gen;
169
+ let completionTimer;
170
+ completionTimer = this.setTimer(() => {
171
+ const current = this.state.get(agent.agentId);
172
+ if (current?.gen !== generation || current.completionTimer !== completionTimer)
173
+ return;
174
+ this.clear(agent, "activity:idle(执行结束缓冲完成)");
175
+ }, 1_000);
176
+ state.completionTimer = completionTimer;
177
+ return;
178
+ }
179
+ // Runtime adapters may add new intermediate activity types. Anything other
180
+ // than an explicit terminal state means the turn is still active.
181
+ state.sawActive = true;
182
+ this.cancelCompletion(state);
183
+ }
184
+ }
185
+ exports.ProcessingEyeOrchestrator = ProcessingEyeOrchestrator;
@@ -0,0 +1,181 @@
1
+ import { execFile as nodeExecFile } from "node:child_process";
2
+ import { managedOfficialLarkCli } from "../app/agent-lark-cli-workspace.mjs";
3
+ function errorMessage(error) {
4
+ return error instanceof Error ? error.message : String(error || "?");
5
+ }
6
+ export class ProcessingEyeOrchestrator {
7
+ options;
8
+ pending = new Map();
9
+ state = new Map();
10
+ execFile;
11
+ log;
12
+ recordError;
13
+ setTimer;
14
+ clearTimer;
15
+ constructor(options = {}) {
16
+ this.options = options;
17
+ this.execFile = options.execFile ?? nodeExecFile;
18
+ this.log = options.log ?? (() => { });
19
+ this.recordError = options.recordStatusError ?? (() => { });
20
+ this.setTimer = options.setTimer ?? setTimeout;
21
+ this.clearTimer = options.clearTimer ?? clearTimeout;
22
+ }
23
+ larkApi(agent, method, apiPath, data, callback) {
24
+ const args = ["api", method, apiPath];
25
+ if (data)
26
+ args.push("--data", JSON.stringify(data));
27
+ const selected = this.options.cliForAgent?.(agent) ?? (() => {
28
+ const managed = managedOfficialLarkCli(agent, this.options.envForAgent?.(agent) ?? process.env);
29
+ return { command: managed.command.command, argsPrefix: managed.command.argsPrefix, env: managed.env };
30
+ })();
31
+ this.execFile(selected.command, [...selected.argsPrefix, ...args], { encoding: "utf8", timeout: 10_000,
32
+ env: selected.env }, (error, stdout, stderr) => {
33
+ let result = null;
34
+ try {
35
+ result = JSON.parse(String(stdout));
36
+ }
37
+ catch { /* non-JSON */ }
38
+ if (error && !result) {
39
+ const killed = "killed" in error && Boolean(error.killed);
40
+ // 不回显命令行:execFile 的 message 以整条命令开头,会占满 recentErrors 的 200 字符截断额度,
41
+ // 把真正可诊断的 exit 码和 stderr 挤出去(2026-07-17 三次偶发失败均因此无法定位)。
42
+ const code = error.code;
43
+ const head = String(stderr || "").trim().slice(0, 160) || String(stdout || "").trim().slice(0, 120);
44
+ const detail = (killed ? "超时(10s)被杀" : `exit=${code ?? "?"}`) + (head ? ` | ${head}` : "");
45
+ const endpoint = apiPath.split("/").slice(-2).join("/");
46
+ this.log(`larkApi ${method} ${endpoint} 失败 agent=${agent.name}: ${detail}`);
47
+ const processingReaction = /\/reactions(?:\/[^/]+)?$/.test(apiPath);
48
+ if (!processingReaction)
49
+ this.recordError(agent, `larkApi ${method} ${endpoint}: ${detail}`);
50
+ }
51
+ callback?.(error, result);
52
+ });
53
+ }
54
+ save(agent) {
55
+ try {
56
+ this.options.writePending?.(agent, this.pending.get(agent.agentId) || []);
57
+ }
58
+ catch { /* best effort */ }
59
+ }
60
+ getState(agent) {
61
+ const existing = this.state.get(agent.agentId);
62
+ if (existing)
63
+ return existing;
64
+ const created = { sawActive: false, fallbackTimer: null, completionTimer: null, gen: 0 };
65
+ this.state.set(agent.agentId, created);
66
+ return created;
67
+ }
68
+ cancelCompletion(state) {
69
+ if (!state.completionTimer)
70
+ return;
71
+ this.clearTimer(state.completionTimer);
72
+ state.completionTimer = null;
73
+ }
74
+ restoreAndClear(agent) {
75
+ let leftovers = [];
76
+ try {
77
+ leftovers = this.options.readPending?.(agent) || [];
78
+ }
79
+ catch {
80
+ return;
81
+ }
82
+ if (!leftovers.length)
83
+ return;
84
+ this.pending.set(agent.agentId, leftovers);
85
+ this.clear(agent, "启动清扫遗留");
86
+ }
87
+ add(agent, msgId) {
88
+ if (!agent.feishuProfile || !msgId || !msgId.startsWith("om_")) {
89
+ this.log(`👀 跳过 agent=${agent.name} msg=${msgId || "?"} profile=${agent.feishuProfile || "缺"}(不满足点表情条件)`);
90
+ return;
91
+ }
92
+ this.log(`👀 点上尝试 agent=${agent.name} msg=${msgId}`);
93
+ const state = this.getState(agent);
94
+ state.gen += 1;
95
+ // A successful delivery may emit turn-start before the Feishu POST is
96
+ // attempted, and a new inbound message may arrive during idle grace. The
97
+ // add itself therefore establishes an active generation instead of
98
+ // erasing activity already observed for that turn.
99
+ state.sawActive = true;
100
+ this.cancelCompletion(state);
101
+ if (state.fallbackTimer)
102
+ this.clearTimer(state.fallbackTimer);
103
+ const generation = state.gen;
104
+ let fallbackTimer;
105
+ fallbackTimer = this.setTimer(() => {
106
+ const current = this.state.get(agent.agentId);
107
+ if (current?.gen !== generation || current.fallbackTimer !== fallbackTimer)
108
+ return;
109
+ this.clear(agent, "15分钟兜底");
110
+ }, 15 * 60 * 1000);
111
+ state.fallbackTimer = fallbackTimer;
112
+ this.larkApi(agent, "POST", `/open-apis/im/v1/messages/${msgId}/reactions`, { reaction_type: { emoji_type: "OnIt" } }, (error, result) => {
113
+ const reactionId = result?.data?.reaction_id;
114
+ if (!reactionId) {
115
+ this.log(`👀 点上失败 msg=${msgId}: ${result ? JSON.stringify(result.error || result).slice(0, 120) : errorMessage(error)}`);
116
+ return;
117
+ }
118
+ if ((this.state.get(agent.agentId)?.gen ?? 0) !== generation) {
119
+ this.log(`👀 迟到回执自删 agent=${agent.name} msg=${msgId}(点上生效前执行已结束)`);
120
+ this.larkApi(agent, "DELETE", `/open-apis/im/v1/messages/${msgId}/reactions/${reactionId}`, null, () => { });
121
+ return;
122
+ }
123
+ const list = this.pending.get(agent.agentId) || [];
124
+ list.push({ msgId, reactionId });
125
+ this.pending.set(agent.agentId, list);
126
+ this.save(agent);
127
+ });
128
+ }
129
+ clear(agent, reason) {
130
+ const state = this.state.get(agent.agentId);
131
+ if (state) {
132
+ state.gen += 1;
133
+ state.sawActive = false;
134
+ if (state.fallbackTimer) {
135
+ this.clearTimer(state.fallbackTimer);
136
+ state.fallbackTimer = null;
137
+ }
138
+ this.cancelCompletion(state);
139
+ }
140
+ const list = this.pending.get(agent.agentId) || [];
141
+ if (!list.length) {
142
+ this.log(`👀 清除(无待摘) agent=${agent.name} 原因=${reason || "?"}`);
143
+ return;
144
+ }
145
+ this.pending.set(agent.agentId, []);
146
+ this.save(agent);
147
+ for (const { msgId, reactionId } of list) {
148
+ this.larkApi(agent, "DELETE", `/open-apis/im/v1/messages/${msgId}/reactions/${reactionId}`, null, (error, result) => {
149
+ if (error || result?.ok === false) {
150
+ this.log(`👀 摘除失败 msg=${msgId}: ${result ? JSON.stringify(result.error || {}).slice(0, 120) : errorMessage(error)}`);
151
+ }
152
+ });
153
+ }
154
+ this.log(`👀 已摘 agent=${agent.name} n=${list.length} 原因=${reason || "?"}`);
155
+ }
156
+ observeActivity(agent, activity) {
157
+ const state = this.getState(agent);
158
+ if (activity === "error" || activity === "offline") {
159
+ this.clear(agent, `activity:${activity}`);
160
+ return;
161
+ }
162
+ if (activity === "idle") {
163
+ if (!state.sawActive || state.completionTimer)
164
+ return;
165
+ const generation = state.gen;
166
+ let completionTimer;
167
+ completionTimer = this.setTimer(() => {
168
+ const current = this.state.get(agent.agentId);
169
+ if (current?.gen !== generation || current.completionTimer !== completionTimer)
170
+ return;
171
+ this.clear(agent, "activity:idle(执行结束缓冲完成)");
172
+ }, 1_000);
173
+ state.completionTimer = completionTimer;
174
+ return;
175
+ }
176
+ // Runtime adapters may add new intermediate activity types. Anything other
177
+ // than an explicit terminal state means the turn is still active.
178
+ state.sawActive = true;
179
+ this.cancelCompletion(state);
180
+ }
181
+ }