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,462 @@
1
+ import crypto from "node:crypto";
2
+ import { createMessageEnvelope, targetAliases, targetFor, } from "./message-policy.mjs";
3
+ function asRecord(value) {
4
+ return value && typeof value === "object" && !Array.isArray(value) ? value : {};
5
+ }
6
+ export function safeConversationExcerpt(value, max = 360) {
7
+ let text = String(value || "").replace(/\s+/g, " ").trim();
8
+ text = text
9
+ .replace(/\b(sk-[A-Za-z0-9_-]{12,})\b/g, "[已隐藏凭证]")
10
+ .replace(/\b(token|secret|password|passwd)\s*[:=]\s*[^\s,;]+/gi, "$1=[已隐藏]");
11
+ return text.length > max ? `${text.slice(0, Math.max(0, max - 1))}…` : text;
12
+ }
13
+ /** 距最后一次「连接建立或真实入站事件」的静默毫秒数;未连接返回 null。 */
14
+ export function inboundSilenceMs(status, now) {
15
+ const connected = Date.parse(String(status.connectedAt || ""));
16
+ if (!Number.isFinite(connected))
17
+ return null;
18
+ const inbound = Date.parse(String(status.inboundVerifiedAt || ""));
19
+ const base = Number.isFinite(inbound) ? Math.max(connected, inbound) : connected;
20
+ return Math.max(0, now.getTime() - base);
21
+ }
22
+ /**
23
+ * 静默超阈值 → 该做预防性重连。两个不抢的例外:
24
+ * - connectedAt 早于 connectedAfterMs(status.json 里上一轮 daemon 的残留断言,本轮还没真正连上);
25
+ * - SDK 自身正在重连(reconnectingAt 未过期)。
26
+ */
27
+ export function shouldPreventiveReconnect(status, now, thresholdMs, connectedAfterMs = null) {
28
+ if (!(thresholdMs > 0))
29
+ return false;
30
+ if (connectedAfterMs !== null) {
31
+ const connected = Date.parse(String(status.connectedAt || ""));
32
+ if (!Number.isFinite(connected) || connected < connectedAfterMs)
33
+ return false;
34
+ }
35
+ const silence = inboundSilenceMs(status, now);
36
+ if (silence === null || silence < thresholdMs)
37
+ return false;
38
+ const reconnecting = Date.parse(String(status.reconnectingAt || ""));
39
+ if (Number.isFinite(reconnecting) && now.getTime() - reconnecting < thresholdMs)
40
+ return false;
41
+ // Maintenance reconnecting creates a fresh connectedAt, but that transport fact is not new
42
+ // inbound evidence. Keep the drought cycle closed until a real inbound event arrives later.
43
+ const persistedDroughtReconnect = Math.max(Date.parse(String(status.droughtReconnectAt || "")) || Number.NEGATIVE_INFINITY, Date.parse(String(status.droughtReconnectAbandonedAt || "")) || Number.NEGATIVE_INFINITY);
44
+ // A daemon restart opens a new maintenance cycle. Only a marker written during
45
+ // the current event-source epoch can suppress another attempt in this run.
46
+ const droughtReconnect = connectedAfterMs === null || persistedDroughtReconnect >= connectedAfterMs
47
+ ? persistedDroughtReconnect
48
+ : Number.NEGATIVE_INFINITY;
49
+ const inbound = Date.parse(String(status.inboundVerifiedAt || ""));
50
+ if (Number.isFinite(droughtReconnect) && (!Number.isFinite(inbound) || droughtReconnect >= inbound))
51
+ return false;
52
+ return true;
53
+ }
54
+ export class HostStateProjection {
55
+ storeFor;
56
+ log;
57
+ now;
58
+ constructor(storeFor, log = () => { }, now = () => new Date()) {
59
+ this.storeFor = storeFor;
60
+ this.log = log;
61
+ this.now = now;
62
+ }
63
+ stateFiles(agent) { return this.storeFor(agent).paths; }
64
+ readStatus(agent) {
65
+ try {
66
+ return asRecord(this.storeFor(agent).readJson("status", {}));
67
+ }
68
+ catch {
69
+ return {};
70
+ }
71
+ }
72
+ updateStatus(agent, patch) {
73
+ const next = { ...this.readStatus(agent), ...patch };
74
+ try {
75
+ this.storeFor(agent).writeJson("status", next);
76
+ }
77
+ catch (error) {
78
+ this.log(`status 写失败 agent=${agent.name}: ${error.message}`);
79
+ }
80
+ }
81
+ appendStatusLog(agent, key, entry, cap) {
82
+ const current = this.readStatus(agent);
83
+ const existing = Array.isArray(current[key]) ? current[key] : [];
84
+ const list = [...existing, entry].slice(-cap);
85
+ const latestKey = key === "activityLog" ? "lastActivity" : key === "deliverLog" ? "lastDeliver" : null;
86
+ this.updateStatus(agent, { [key]: list, ...(latestKey ? { [latestKey]: entry } : {}) });
87
+ }
88
+ recordStatusError(agent, text) {
89
+ this.appendStatusLog(agent, "recentErrors", { at: this.now().toISOString(), text: String(text).slice(0, 200) }, 20);
90
+ }
91
+ appendConversation(agent, item) {
92
+ try {
93
+ this.storeFor(agent).appendNdjson("conversation", { ...item, text: safeConversationExcerpt(item.text) });
94
+ }
95
+ catch (error) {
96
+ this.log(`conversation 写失败 agent=${agent.name}: ${error.message}`);
97
+ }
98
+ }
99
+ loadMap(agent) {
100
+ try {
101
+ return asRecord(this.storeFor(agent).readJson("map", {}));
102
+ }
103
+ catch {
104
+ return {};
105
+ }
106
+ }
107
+ saveMap(agent, map) {
108
+ try {
109
+ this.storeFor(agent).writeJson("map", map);
110
+ }
111
+ catch (error) {
112
+ this.log("saveMap 失败", error.message);
113
+ }
114
+ }
115
+ mapTargets(agent, targets, chatId) {
116
+ const map = this.loadMap(agent);
117
+ for (const target of targets)
118
+ if (target)
119
+ map[target] = chatId;
120
+ this.saveMap(agent, map);
121
+ }
122
+ saveReplyContext(agent, keys, chatId, replyTo, threadId) {
123
+ const aliases = Array.isArray(keys) ? keys : [keys];
124
+ let map = {};
125
+ try {
126
+ map = asRecord(this.storeFor(agent).readJson("replyctx", {}));
127
+ }
128
+ catch { /* first run or corrupt state retains historical fail-open behavior */ }
129
+ const entry = {
130
+ chat_id: chatId || null,
131
+ reply_to: replyTo || null,
132
+ thread_id: threadId || null,
133
+ in_topic: Boolean(threadId),
134
+ };
135
+ for (const alias of aliases)
136
+ if (alias)
137
+ map[alias] = entry;
138
+ if (chatId)
139
+ map[chatId] = entry;
140
+ try {
141
+ this.storeFor(agent).writeJson("replyctx", map);
142
+ }
143
+ catch (error) {
144
+ this.log("replyctx 写失败", error.message);
145
+ }
146
+ }
147
+ loadSenderProfiles(agent) {
148
+ try {
149
+ return asRecord(this.storeFor(agent).readJson("senderProfiles", {}));
150
+ }
151
+ catch {
152
+ return {};
153
+ }
154
+ }
155
+ saveSenderProfile(agent, senderId, profile) {
156
+ const all = this.loadSenderProfiles(agent);
157
+ all[senderId] = profile;
158
+ try {
159
+ this.storeFor(agent).writeJson("senderProfiles", all);
160
+ }
161
+ catch { /* signature cache persistence is best-effort by design */ }
162
+ }
163
+ recordReadReceipts(agent, reader, readAt, ids) {
164
+ let data = { receipts: [] };
165
+ try {
166
+ data = asRecord(this.storeFor(agent).readJson("readReceipts", data));
167
+ }
168
+ catch { /* first run */ }
169
+ data.receipts = [...(Array.isArray(data.receipts) ? data.receipts : []), { ids, reader, readAt }].slice(-200);
170
+ this.storeFor(agent).writeJson("readReceipts", data);
171
+ }
172
+ }
173
+ export class SenderIdentityCache {
174
+ options;
175
+ chatNames = new Map();
176
+ signatures = new Map();
177
+ now;
178
+ wait;
179
+ failureTtlMs;
180
+ constructor(options) {
181
+ this.options = options;
182
+ this.now = options.now ?? Date.now;
183
+ this.wait = options.wait ?? ((milliseconds) => new Promise((resolve) => setTimeout(resolve, milliseconds)));
184
+ this.failureTtlMs = options.signatureFailureTtlMs ?? 10 * 60 * 1000;
185
+ }
186
+ chatKey(agent, chatId) { return `${agent.agentId}:${chatId}`; }
187
+ signatureKey(agent, senderId) { return `${agent.agentId}:${senderId}`; }
188
+ ensureChatNames(agent, chatId, waitMs) {
189
+ if (!chatId || !agent.feishuProfile)
190
+ return Promise.resolve({});
191
+ const key = this.chatKey(agent, chatId);
192
+ const hit = this.chatNames.get(key);
193
+ if (hit && !(hit instanceof Promise))
194
+ return Promise.resolve(hit.names);
195
+ const pending = hit ?? this.options.fetchChatNames(agent, chatId).then((names) => {
196
+ if (names)
197
+ this.chatNames.set(key, { names, at: this.now() });
198
+ else
199
+ this.chatNames.delete(key);
200
+ return names ?? {};
201
+ });
202
+ if (!hit)
203
+ this.chatNames.set(key, pending);
204
+ if (!waitMs)
205
+ return Promise.resolve({});
206
+ return Promise.race([pending, this.wait(waitMs).then(() => ({}))]);
207
+ }
208
+ noteUnknownSender(agent, chatId, senderId) {
209
+ if (!senderId || !/^(ou_|cli_|app_)/.test(senderId))
210
+ return;
211
+ const key = this.chatKey(agent, chatId);
212
+ const hit = this.chatNames.get(key);
213
+ if (!hit || hit instanceof Promise || this.now() - hit.at < 60_000)
214
+ return;
215
+ this.chatNames.delete(key);
216
+ void this.ensureChatNames(agent, chatId, 0);
217
+ }
218
+ ensureSenderSignature(agent, senderId, waitMs) {
219
+ if (!senderId || !senderId.startsWith("ou_") || !agent.feishuProfile)
220
+ return Promise.resolve(null);
221
+ const key = this.signatureKey(agent, senderId);
222
+ const hit = this.signatures.get(key);
223
+ if (hit && !(hit instanceof Promise)) {
224
+ const fresh = this.now() - hit.at < (hit.ttl || this.failureTtlMs);
225
+ if (!fresh) {
226
+ this.signatures.delete(key);
227
+ void this.ensureSenderSignature(agent, senderId, 0);
228
+ }
229
+ return Promise.resolve(hit.desc);
230
+ }
231
+ const pending = hit ?? this.options.fetchSenderSignature(agent, senderId).then((result) => {
232
+ const entry = { ...result, at: this.now() };
233
+ this.signatures.set(key, entry);
234
+ if (result.ok)
235
+ this.options.state.saveSenderProfile(agent, senderId, {
236
+ description: result.desc,
237
+ name: result.name,
238
+ at: entry.at,
239
+ });
240
+ return result.desc;
241
+ });
242
+ if (!hit)
243
+ this.signatures.set(key, pending);
244
+ return waitMs ? Promise.race([pending, this.wait(waitMs).then(() => null)]) : Promise.resolve(null);
245
+ }
246
+ warmSenderProfiles(agent) {
247
+ for (const [senderId, profile] of Object.entries(this.options.state.loadSenderProfiles(agent))) {
248
+ this.signatures.set(this.signatureKey(agent, senderId), {
249
+ desc: profile.description ?? null,
250
+ name: profile.name || null,
251
+ at: profile.at || 0,
252
+ ok: true,
253
+ ttl: 6 * 60 * 60 * 1000,
254
+ });
255
+ }
256
+ }
257
+ }
258
+ export class HostEnvelopeProjector {
259
+ state;
260
+ noteUnknownSender;
261
+ randomHex;
262
+ now;
263
+ agentCliExecutable;
264
+ sequenceByAgent = new Map();
265
+ constructor(state, noteUnknownSender, randomHex = (bytes) => crypto.randomBytes(bytes).toString("hex"), now = () => new Date(), agentCliExecutable = "larkin") {
266
+ this.state = state;
267
+ this.noteUnknownSender = noteUnknownSender;
268
+ this.randomHex = randomHex;
269
+ this.now = now;
270
+ this.agentCliExecutable = agentCliExecutable;
271
+ }
272
+ agentCommand(suffix) {
273
+ return `${this.agentCliExecutable} ${suffix}`;
274
+ }
275
+ larkCommand(suffix) {
276
+ return `${this.agentCliExecutable} ${suffix}`;
277
+ }
278
+ nextSequence(agentId) {
279
+ const next = (this.sequenceByAgent.get(agentId) || 0) + 1;
280
+ this.sequenceByAgent.set(agentId, next);
281
+ return next;
282
+ }
283
+ projectInbound(agent, event, options = {}) {
284
+ const target = targetFor(event);
285
+ const aliases = targetAliases(event, target);
286
+ this.state.mapTargets(agent, aliases, event.chat_id);
287
+ const messageId = event.message_id || `m_${this.randomHex(6)}`;
288
+ if (options.anchorReply !== false) {
289
+ this.state.saveReplyContext(agent, aliases, event.chat_id, messageId, event.thread_id || null);
290
+ }
291
+ const names = options.names || {};
292
+ if (!names[event.sender_id])
293
+ this.noteUnknownSender(agent, event.chat_id, event.sender_id);
294
+ return createMessageEnvelope({
295
+ agent,
296
+ event: { ...event, message_id: messageId },
297
+ seq: this.nextSequence(agent.agentId),
298
+ names,
299
+ signature: options.signature,
300
+ });
301
+ }
302
+ createReminderEnvelope(agentId, reminder, overdueMs, repeatDescription) {
303
+ const seq = this.nextSequence(agentId);
304
+ const anchorMessageId = typeof reminder.msgRef === "string" && /^om_[A-Za-z0-9_-]+$/.test(reminder.msgRef)
305
+ ? reminder.msgRef
306
+ : null;
307
+ const lines = [
308
+ `[定时提醒触发] ${reminder.title}`,
309
+ `提醒ID: #${reminder.reminderId.slice(0, 8)}` + (reminder.repeat && repeatDescription
310
+ ? ` 重复: ${repeatDescription}(下次已自动排在 ${reminder.fireAt})`
311
+ : " 类型: 一次性"),
312
+ overdueMs > 120_000 ? `注意: 原定时间已过 ${Math.round(overdueMs / 60_000)} 分钟(Runtime Host 离线期间错过,现补触发)` : null,
313
+ anchorMessageId ? `锚定消息: ${anchorMessageId}` : reminder.msgRef ? `历史锚点 ${String(reminder.msgRef)} 不是飞书 om_ message_id,不能用于回复` : null,
314
+ anchorMessageId
315
+ ? `回复原会话: ${this.larkCommand(`im +messages-reply --message-id ${anchorMessageId} ...`)}`
316
+ : null,
317
+ reminder.channel && /^oc_[A-Za-z0-9_-]+$/.test(reminder.channel)
318
+ ? `发送到原群: ${this.larkCommand(`im +messages-send --chat-id ${reminder.channel} ...`)}`
319
+ : reminder.channel
320
+ ? `历史目标 ${reminder.channel} 不是 chat_id;${anchorMessageId ? "若不回复锚定消息," : ""}先用 ${this.larkCommand("im +chat-search")} 查询并确认 oc_ chat_id,禁止按名称猜测发送目标`
321
+ : anchorMessageId
322
+ ? null
323
+ : `本条存量提醒缺少可用的飞书 message_id/chat_id,无法安全推断原会话;请先用 ${this.larkCommand("im +chat-search")} 确认目标,禁止猜测发送`,
324
+ `这是你之前用 ${this.agentCommand("reminder schedule")} 设置的提醒,请按标题执行相应动作。管理: ${this.agentCommand("reminder list")} / ${this.agentCommand("reminder snooze")} / ${this.agentCommand("reminder cancel")}`,
325
+ ].filter((line) => Boolean(line));
326
+ return {
327
+ message_id: `rem_${reminder.reminderId.slice(0, 16)}_${seq}`,
328
+ seq,
329
+ sender_name: "定时提醒",
330
+ sender_type: "system",
331
+ channel_type: "dm",
332
+ channel_name: "system",
333
+ content: lines.join("\n"),
334
+ timestamp: this.now().toISOString(),
335
+ thread_id: null,
336
+ wake: true,
337
+ };
338
+ }
339
+ createRedeliveryEnvelope(agentId, wakeCount) {
340
+ const seq = this.nextSequence(agentId);
341
+ return {
342
+ message_id: `redeliver_${this.randomHex(6)}`,
343
+ seq,
344
+ sender_name: "系统",
345
+ sender_type: "system",
346
+ channel_type: "dm",
347
+ channel_name: "system",
348
+ content: `[启动补投] 服务重启期间有 ${wakeCount} 条本应唤醒你的消息未被读取(可能包含用户消息、@提及或定时提醒)。请先用 ${this.agentCommand("inbox check")} 看目标摘要,再用 ${this.agentCommand("inbox poll")} 领取完整消息;仅当 message_id 以 om_ 开头时才用 ${this.larkCommand("im +messages-reply")},系统 rem_/redeliver_ ID 不可回复;有 chat_id 时可用 ${this.larkCommand("im +messages-send")},否则先查询确认目标,禁止猜测。`,
349
+ timestamp: this.now().toISOString(),
350
+ thread_id: null,
351
+ };
352
+ }
353
+ }
354
+ export function countWakeEnvelopes(lines) {
355
+ return lines.filter((line) => {
356
+ try {
357
+ return JSON.parse(line).wake === true;
358
+ }
359
+ catch {
360
+ return false;
361
+ }
362
+ }).length;
363
+ }
364
+ /** Project daemon activity into dashboard state without changing or interpreting Runtime transport frames. */
365
+ export function projectActivityStatus(current, message, runtime, resumeSession, now = new Date()) {
366
+ if (message.isHeartbeat)
367
+ return null;
368
+ const activityKind = message.activityKind || message.activity || "unknown";
369
+ const at = Number.isFinite(message.observedAtMs)
370
+ ? new Date(message.observedAtMs).toISOString()
371
+ : now.toISOString();
372
+ const entries = Array.isArray(message.entries) ? message.entries : [];
373
+ const tool = [...entries].reverse().find((entry) => entry?.kind === "tool_start" && entry.toolName)?.toolName || null;
374
+ const activityEntry = {
375
+ state: activityKind === "text" ? "working" : (message.activity || activityKind),
376
+ activityKind,
377
+ detail: activityKind === "thinking" ? null : (safeConversationExcerpt(message.detail, 180) || null),
378
+ detailKind: message.detailKind || "other",
379
+ tool,
380
+ at,
381
+ launchId: message.launchId || null,
382
+ clientSeq: Number.isFinite(message.clientSeq) ? message.clientSeq : null,
383
+ };
384
+ const activityLog = Array.isArray(current.activityLog) ? current.activityLog : [];
385
+ const previousActivity = current.lastActivity && typeof current.lastActivity === "object" && !Array.isArray(current.lastActivity)
386
+ ? current.lastActivity
387
+ : null;
388
+ const repeatedThinking = activityKind === "thinking"
389
+ && (previousActivity?.activityKind === "thinking" || previousActivity?.state === "thinking");
390
+ // Runtime adapters emit text/thinking once per streamed delta. Text is only a freshness signal;
391
+ // thinking is durable on transition, then coalesced until the next state. Persisting every frame
392
+ // otherwise evicts meaningful turn/tool/idle history from the Dashboard within milliseconds.
393
+ const appendToHistory = activityKind !== "text" && !repeatedThinking;
394
+ const patch = {
395
+ activityLog: appendToHistory ? [...activityLog, activityEntry].slice(-80) : activityLog,
396
+ lastActivity: activityEntry,
397
+ };
398
+ const entryKinds = new Set(entries.map((entry) => entry?.kind).filter(Boolean));
399
+ const compactStarted = activityEntry.detailKind === "compacting_context" || entryKinds.has("compaction_started");
400
+ const compactFinished = activityEntry.detailKind === "compaction_finished" || entryKinds.has("compaction_finished");
401
+ if (compactStarted || compactFinished) {
402
+ const previous = current.compaction || {};
403
+ const atMs = Date.parse(at);
404
+ const nearPreviousCompact = [previous.startedAt, previous.lastFinishedAt]
405
+ .map((value) => typeof value === "string" ? Date.parse(value) : Number.NaN)
406
+ .some((value) => Number.isFinite(value) && Number.isFinite(atMs) && Math.abs(atMs - value) <= 5000);
407
+ const sameEvent = Boolean(message.producerFactId) && previous.lastEventId === message.producerFactId;
408
+ const duplicateStart = Boolean(previous.active) || sameEvent || nearPreviousCompact;
409
+ const shouldCountStart = compactStarted && !duplicateStart;
410
+ patch.compaction = compactFinished ? {
411
+ ...previous,
412
+ sessionId: current.session?.id || previous.sessionId || null,
413
+ active: false,
414
+ count: Number(previous.count || 0) + (shouldCountStart ? 1 : 0),
415
+ startedAt: shouldCountStart ? at : (previous.startedAt || at),
416
+ lastFinishedAt: at,
417
+ lastEventId: message.producerFactId || null,
418
+ } : {
419
+ sessionId: current.session?.id || null,
420
+ active: true,
421
+ count: Number(previous.count || 0) + (duplicateStart ? 0 : 1),
422
+ startedAt: duplicateStart ? (previous.startedAt || at) : at,
423
+ lastFinishedAt: previous.lastFinishedAt || null,
424
+ lastEventId: message.producerFactId || null,
425
+ };
426
+ }
427
+ if (activityEntry.detailKind === "message_received" || activityEntry.detailKind === "turn_started") {
428
+ patch.session = {
429
+ ...(current.session || {}),
430
+ runtime,
431
+ id: current.session?.id || resumeSession || null,
432
+ startedAt: current.session?.startedAt || at,
433
+ lastTurnAt: at,
434
+ turns: Number(current.session?.turns || 0) + 1,
435
+ };
436
+ }
437
+ return patch;
438
+ }
439
+ export function projectSessionStatus(current, runtime, sessionId, launchId, now = new Date(), effective = {}) {
440
+ const sameSession = current.session?.id === sessionId;
441
+ const at = now.toISOString();
442
+ return {
443
+ session: {
444
+ runtime,
445
+ id: sessionId,
446
+ launchId: launchId || current.session?.launchId || null,
447
+ startedAt: sameSession ? (current.session?.startedAt || at) : at,
448
+ lastSeenAt: at,
449
+ lastTurnAt: sameSession ? (current.session?.lastTurnAt || null) : null,
450
+ turns: sameSession ? Number(current.session?.turns || 0) : 0,
451
+ ...((effective.model || (sameSession ? current.session?.model : null))
452
+ ? { model: effective.model || current.session?.model }
453
+ : {}),
454
+ ...((effective.reasoningEffort || (sameSession ? current.session?.reasoningEffort : null))
455
+ ? { reasoningEffort: effective.reasoningEffort || current.session?.reasoningEffort }
456
+ : {}),
457
+ },
458
+ ...(sameSession ? {} : {
459
+ compaction: { sessionId, active: false, count: 0, startedAt: null, lastFinishedAt: null, lastEventId: null },
460
+ }),
461
+ };
462
+ }
@@ -0,0 +1,161 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.HostChannelBusiness = void 0;
4
+ const message_policy_js_1 = require("./message-policy.cjs");
5
+ const channel_1 = require("@larksuite/channel");
6
+ function messageOf(error) { return error instanceof Error ? error.message : String(error); }
7
+ class HostChannelBusiness {
8
+ options;
9
+ log;
10
+ now;
11
+ constructor(options) {
12
+ this.options = options;
13
+ this.log = options.log ?? (() => { });
14
+ this.now = options.now ?? (() => new Date());
15
+ }
16
+ handlers(agent, channel) {
17
+ return {
18
+ message: (message) => {
19
+ try {
20
+ const event = (0, message_policy_js_1.normalizeChannelMessage)(message);
21
+ this.options.state.updateStatus(agent, { inboundVerifiedAt: this.now().toISOString() });
22
+ const senderIsBot = message.senderIsBot === true;
23
+ const decision = (0, message_policy_js_1.decideWake)({
24
+ senderIsBot,
25
+ mentionedBot: message.mentionedBot === true,
26
+ mentionAll: message.mentionAll === true,
27
+ isGroup: (message.chatType || "group") !== "p2p",
28
+ mentionPolicy: senderIsBot ? "require" : this.options.mentionPolicy?.(agent.agentId, String(message.chatId || ""))
29
+ || (Array.isArray(agent.noMentionChats) && agent.noMentionChats.includes(String(message.chatId || "")) ? "free" : "require"),
30
+ });
31
+ if (!decision.wake) {
32
+ this.log(`${senderIsBot ? "bot 消息未点名@我" : "群消息未@bot"} → 只入箱不唤醒 agent=${agent.name} chat=${message.chatId}`);
33
+ }
34
+ void this.options.onMessage(agent, event, { wake: decision.wake });
35
+ }
36
+ catch (error) {
37
+ this.log(`channel message 处理失败 agent=${agent.name}: ${messageOf(error)}`);
38
+ }
39
+ },
40
+ comment: async (event) => {
41
+ try {
42
+ this.options.state.updateStatus(agent, {
43
+ inboundVerifiedAt: this.now().toISOString(),
44
+ documentCommentEventAt: this.now().toISOString(),
45
+ });
46
+ if (!channel || !this.options.onComment)
47
+ return;
48
+ await this.options.onComment(agent, event, channel);
49
+ }
50
+ catch (error) {
51
+ this.log(`document comment 处理失败 agent=${agent.name}: ${messageOf(error)}`);
52
+ this.options.state.recordStatusError(agent, `document comment: ${messageOf(error)}`);
53
+ }
54
+ },
55
+ cardAction: async (event) => {
56
+ if (!this.options.onCardAction)
57
+ return { toast: { type: "error", content: "交互能力未启用,请重新运行 larkin setup 并重启 Larkin。" } };
58
+ return this.options.onCardAction(agent, event);
59
+ },
60
+ reconnecting: () => {
61
+ this.log(`ws 重连中… agent=${agent.name}`);
62
+ this.options.state.updateStatus(agent, { reconnectingAt: this.now().toISOString() });
63
+ },
64
+ reconnected: () => {
65
+ this.log(`ws 已恢复 agent=${agent.name}`);
66
+ const connectedAt = this.now().toISOString();
67
+ this.options.state.updateStatus(agent, { reconnectingAt: null, reconnectedAt: connectedAt, connectedAt });
68
+ if (channel && this.options.onReconnected) {
69
+ void Promise.resolve(this.options.onReconnected(agent, channel)).catch((error) => {
70
+ this.log(`document comment 恢复重放失败 agent=${agent.name}: ${messageOf(error)}`);
71
+ });
72
+ }
73
+ },
74
+ error: () => {
75
+ this.log(`ws 错误 agent=${agent.name}`);
76
+ this.options.state.recordStatusError(agent, "channel ws 连接错误");
77
+ },
78
+ };
79
+ }
80
+ registerReadReceipts(agent, dispatcher) {
81
+ try {
82
+ dispatcher.register({
83
+ "im.message.message_read_v1": async (raw) => {
84
+ try {
85
+ const event = raw;
86
+ const reader = event?.reader?.reader_id?.open_id || null;
87
+ const readAt = event?.reader?.read_time || null;
88
+ const ids = event?.message_id_list || [];
89
+ this.options.state.recordReadReceipts(agent, reader, readAt, ids);
90
+ this.log(`已读回执 agent=${agent.name} reader=${reader || "?"} msgs=${ids.length}`);
91
+ }
92
+ catch (error) {
93
+ this.log(`已读回执处理失败 agent=${agent.name}: ${messageOf(error)}`);
94
+ }
95
+ },
96
+ });
97
+ }
98
+ catch (error) {
99
+ this.log(`已读回执注册失败(不影响消息收发) agent=${agent.name}: ${messageOf(error)}`);
100
+ }
101
+ }
102
+ /**
103
+ * Replace the SDK's action-value TTL dedup handler after channel.connect().
104
+ * Durable callback event_id ownership lives in InteractionStateMachine, so
105
+ * retries receive the same response while later state-version clicks remain legal.
106
+ */
107
+ registerCardActions(agent, dispatcher) {
108
+ dispatcher.register({
109
+ "card.action.trigger": async (raw) => {
110
+ const event = (0, channel_1.normalizeCardAction)(raw, { includeRaw: true });
111
+ if (!event)
112
+ return { toast: { type: "error", content: "无法解析卡片操作,请刷新后重试。" } };
113
+ return this.handlers(agent).cardAction(event);
114
+ },
115
+ });
116
+ }
117
+ persistBotIdentity(agent, openId, name, via, avatarUrl = null) {
118
+ agent.botOpenId = openId;
119
+ agent.botName = name || agent.botName || null;
120
+ try {
121
+ let previous = {};
122
+ try {
123
+ previous = this.options.stateStore(agent).readJson("botIdentity", {}) || {};
124
+ }
125
+ catch { /* first run */ }
126
+ this.options.stateStore(agent).writeJson("botIdentity", {
127
+ open_id: openId,
128
+ name: name || previous.name || null,
129
+ avatar_url: avatarUrl || previous.avatar_url || null,
130
+ updated_at: this.now().toISOString(),
131
+ });
132
+ }
133
+ catch (error) {
134
+ this.log(`bot-identity 写入失败 agent=${agent.name}: ${messageOf(error)}`);
135
+ }
136
+ this.log(`bot 身份就绪(${via}) agent=${agent.name} bot=${name || "?"}(${openId})`);
137
+ this.options.state.updateStatus(agent, { connectedAt: this.now().toISOString(), connectedVia: via, reconnectingAt: null });
138
+ }
139
+ async connected(agent, channel, onFatal) {
140
+ const identity = channel.botIdentity || null;
141
+ if (!identity?.openId) {
142
+ this.log(`createLarkChannel 已连接但未取到 botIdentity agent=${agent.name}`);
143
+ onFatal?.(new Error("bot identity unavailable"));
144
+ return;
145
+ }
146
+ let avatarUrl = null;
147
+ let botName = identity.name || null;
148
+ try {
149
+ const info = await channel.rawClient?.request({ url: "/open-apis/bot/v3/info", method: "GET" });
150
+ if (info?.bot?.open_id === identity.openId) {
151
+ avatarUrl = info.bot.avatar_url || null;
152
+ botName = info.bot.app_name || botName;
153
+ }
154
+ }
155
+ catch (error) {
156
+ this.log(`bot 头像刷新失败(不影响连接) agent=${agent.name}: ${messageOf(error)}`);
157
+ }
158
+ this.persistBotIdentity(agent, identity.openId, botName, "channel", avatarUrl);
159
+ }
160
+ }
161
+ exports.HostChannelBusiness = HostChannelBusiness;