@controlflow-ai/daemon 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +54 -6
- package/package.json +3 -1
- package/src/agent-avatar.ts +30 -0
- package/src/agent-key.ts +28 -0
- package/src/agent-permissions.ts +359 -0
- package/src/agent-runtime.ts +795 -28
- package/src/agent-workspace.ts +183 -0
- package/src/app.ts +1970 -79
- package/src/args.ts +54 -7
- package/src/cli.ts +873 -14
- package/src/client.ts +472 -10
- package/src/coco.ts +9 -40
- package/src/codex.ts +33 -5
- package/src/config.ts +28 -4
- package/src/console.ts +230 -20
- package/src/daemon-client.ts +116 -3
- package/src/daemon.ts +936 -98
- package/src/db.ts +3128 -122
- package/src/delivery-ws.ts +269 -0
- package/src/format.ts +4 -1
- package/src/lark/cli.ts +3 -3
- package/src/lark/event-router.ts +60 -4
- package/src/lark/inbound-events.ts +156 -3
- package/src/lark/server-integration.ts +659 -111
- package/src/lark/ws-daemon.ts +136 -10
- package/src/local-api.ts +545 -15
- package/src/local-auth.ts +33 -1
- package/src/message-attachments.ts +71 -0
- package/src/messaging-cli.ts +741 -0
- package/src/messaging-status.ts +669 -0
- package/src/migrations/024_agents_model.ts +10 -0
- package/src/migrations/025_room_archive.ts +44 -0
- package/src/migrations/026_project_archive.ts +44 -0
- package/src/migrations/027_agent_permission_profiles.ts +16 -0
- package/src/migrations/028_lark_websocket_restart_state.ts +16 -0
- package/src/migrations/029_held_message_drafts.ts +32 -0
- package/src/migrations/030_agent_room_read_state.ts +25 -0
- package/src/migrations/031_room_tasks.ts +29 -0
- package/src/migrations/032_room_reminders.ts +29 -0
- package/src/migrations/033_room_saved_messages.ts +25 -0
- package/src/migrations/034_agent_activity_events.ts +27 -0
- package/src/migrations/035_agent_avatars.ts +17 -0
- package/src/migrations/036_project_agent_defaults.ts +21 -0
- package/src/migrations/037_message_attachments.ts +36 -0
- package/src/migrations/038_agent_activity_room_scope.ts +64 -0
- package/src/migrations/039_message_attachments_path.ts +34 -0
- package/src/migrations/040_message_attachments_file_schema.ts +80 -0
- package/src/migrations/041_room_system_events.ts +30 -0
- package/src/migrations/042_message_attachment_file_kind.ts +52 -0
- package/src/migrations/043_room_mode_skill_registry.ts +92 -0
- package/src/migrations/044_workflow_runtime.ts +69 -0
- package/src/migrations/045_skill_repository_ownership.ts +64 -0
- package/src/migrations.ts +69 -1
- package/src/neeko.ts +40 -4
- package/src/runtime-env.ts +179 -0
- package/src/runtime-registry.ts +83 -13
- package/src/server.ts +244 -4
- package/src/token-file.ts +13 -6
- package/src/types.ts +362 -0
- package/src/workflow-runtime.ts +275 -0
- package/src/web.ts +0 -904
|
@@ -1,8 +1,48 @@
|
|
|
1
1
|
import { MessageStore } from '../db.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { homeDir } from '../config.js';
|
|
3
|
+
import { boundAgents, defaultLarkConfigPath, loadLarkCredentials, type LarkCredential } from './credentials.js';
|
|
4
|
+
import { extractLarkImageResources, extractMentionOpenIds, ingestLarkMessage, type LarkImageResource, type LarkMessageEnvelope } from './event-router.js';
|
|
5
|
+
import { countInboundParseFailures, countProbeInboundEvents, countProviderInboundEvents, latestInboundEventSummary, latestProbeInboundEventSummary, storeInboundEvent, type InboundRawEventSummary } from './inbound-events.js';
|
|
4
6
|
import type { ReceivePolicy } from './dispatcher.js';
|
|
5
|
-
import { addMessageReaction, createLarkApiClient, getChatInfo, getChatMembers, startLarkDaemon, type LarkDaemonHandle } from './ws-daemon.js';
|
|
7
|
+
import { addMessageReaction, buildLarkEventRawBody, createLarkApiClient, getChatInfo, getChatMembers, startLarkDaemon, type LarkDaemonHandle, type LarkEventCallback, type LarkWebSocketLifecycleCallback } from './ws-daemon.js';
|
|
8
|
+
import type { MessageDelivery } from '../types.js';
|
|
9
|
+
import { larkBotNeedsRestart } from '../messaging-status.js';
|
|
10
|
+
|
|
11
|
+
async function readableToUint8Array(stream: NodeJS.ReadableStream): Promise<Uint8Array> {
|
|
12
|
+
const chunks: Buffer[] = [];
|
|
13
|
+
return new Promise((resolve, reject) => {
|
|
14
|
+
stream.on('data', (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
15
|
+
stream.on('end', () => resolve(Buffer.concat(chunks)));
|
|
16
|
+
stream.on('error', reject);
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function larkDownloadToUint8Array(raw: unknown): Promise<Uint8Array> {
|
|
21
|
+
if (raw instanceof Uint8Array) return raw;
|
|
22
|
+
if (Buffer.isBuffer(raw)) return raw;
|
|
23
|
+
if (raw && typeof raw === 'object') {
|
|
24
|
+
const record = raw as { getReadableStream?: () => NodeJS.ReadableStream; data?: unknown };
|
|
25
|
+
if (typeof record.getReadableStream === 'function') return readableToUint8Array(record.getReadableStream());
|
|
26
|
+
if (record.data instanceof Uint8Array) return record.data;
|
|
27
|
+
if (Buffer.isBuffer(record.data)) return record.data;
|
|
28
|
+
}
|
|
29
|
+
throw new Error('unexpected Lark image download response');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function downloadLarkImage(input: { appId: string; appSecret: string; resource: LarkImageResource }): Promise<Uint8Array> {
|
|
33
|
+
const client = createLarkApiClient(input.appId, input.appSecret) as unknown as {
|
|
34
|
+
im: { v1?: { image?: { get(input: { path: { image_key: string } }): Promise<unknown> } } };
|
|
35
|
+
};
|
|
36
|
+
if (!client.im.v1?.image?.get) throw new Error('Lark SDK image download API is unavailable');
|
|
37
|
+
return larkDownloadToUint8Array(await client.im.v1.image.get({ path: { image_key: input.resource.fileKey } }));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface LarkDeliveryNotifyResult {
|
|
41
|
+
deliveries: number;
|
|
42
|
+
target_connections: number;
|
|
43
|
+
open_sockets: number;
|
|
44
|
+
websocket_frames: number;
|
|
45
|
+
}
|
|
6
46
|
|
|
7
47
|
export interface LarkServerIntegrationOptions {
|
|
8
48
|
/** Shared MessageStore for DB operations (caller owns lifecycle). */
|
|
@@ -11,16 +51,94 @@ export interface LarkServerIntegrationOptions {
|
|
|
11
51
|
botOpenIdByApp?: Map<string, string>;
|
|
12
52
|
logger?: Partial<Pick<Console, 'log' | 'warn' | 'error'>>;
|
|
13
53
|
configPath?: string;
|
|
54
|
+
dbPath?: string;
|
|
14
55
|
startDaemon?: typeof startLarkDaemon;
|
|
56
|
+
deliveryNotifier?: {
|
|
57
|
+
notifyDeliveries(deliveries: MessageDelivery[]): LarkDeliveryNotifyResult;
|
|
58
|
+
};
|
|
59
|
+
syncRoomMetadata?: boolean;
|
|
60
|
+
sendDeliveryReaction?: boolean;
|
|
15
61
|
}
|
|
16
62
|
|
|
17
63
|
export type LarkReloadResult =
|
|
18
64
|
| { ok: true; bots: string[]; started: string[]; stopped: string[]; restarted: string[] }
|
|
19
65
|
| { ok: false; bots: string[]; error: string };
|
|
20
66
|
|
|
67
|
+
export type LarkRestartResult =
|
|
68
|
+
| { ok: true; bots: string[]; restarted: string[]; missing: string[]; skipped_recent?: string[]; skipped_ineffective?: string[] }
|
|
69
|
+
| { ok: false; bots: string[]; restarted: string[]; missing: string[]; skipped_recent?: string[]; skipped_ineffective?: string[]; error: string };
|
|
70
|
+
|
|
71
|
+
export interface LarkRestartStaleOptions {
|
|
72
|
+
staleAfterMs: number;
|
|
73
|
+
now?: number;
|
|
74
|
+
minRestartIntervalMs?: number;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface LarkProbeEventInput {
|
|
78
|
+
appId: string;
|
|
79
|
+
envelope?: 'im.message.receive_v1' | 'card.action.trigger' | string;
|
|
80
|
+
data: unknown;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface LarkProbeEventResult {
|
|
84
|
+
app_id: string;
|
|
85
|
+
envelope: string;
|
|
86
|
+
raw_event_id: string;
|
|
87
|
+
inserted: boolean;
|
|
88
|
+
duplicate: boolean;
|
|
89
|
+
parse_ok: 0 | 1;
|
|
90
|
+
handled: boolean;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface LarkBotRuntimeStatus {
|
|
94
|
+
app_id: string;
|
|
95
|
+
agent: string | null;
|
|
96
|
+
label: string | null;
|
|
97
|
+
bot_open_id_known: boolean;
|
|
98
|
+
authorized_user_count: number;
|
|
99
|
+
runtime_home: string;
|
|
100
|
+
config_path: string;
|
|
101
|
+
db_path: string | null;
|
|
102
|
+
ws_state: string | null;
|
|
103
|
+
ws_last_connect_at: string | null;
|
|
104
|
+
ws_next_connect_at: string | null;
|
|
105
|
+
ws_reconnect_attempts: number | null;
|
|
106
|
+
ws_last_event_at: string | null;
|
|
107
|
+
ws_last_event_type: string | null;
|
|
108
|
+
ws_last_error: string | null;
|
|
109
|
+
restart_count: number;
|
|
110
|
+
last_restart_at: string | null;
|
|
111
|
+
last_restart_reason: string | null;
|
|
112
|
+
started_at: string;
|
|
113
|
+
event_count: number;
|
|
114
|
+
message_event_count: number;
|
|
115
|
+
provider_event_count: number;
|
|
116
|
+
provider_message_event_count: number;
|
|
117
|
+
last_event_at: string | null;
|
|
118
|
+
last_event_type: string | null;
|
|
119
|
+
last_provider_event_at: string | null;
|
|
120
|
+
last_provider_message_event_at: string | null;
|
|
121
|
+
last_message_event_at: string | null;
|
|
122
|
+
last_raw_event_id: string | null;
|
|
123
|
+
last_ingest_status: string | null;
|
|
124
|
+
last_delivery_count: number | null;
|
|
125
|
+
last_delivery_notify: LarkDeliveryNotifyResult | null;
|
|
126
|
+
db_event_count: number;
|
|
127
|
+
db_provider_event_count: number;
|
|
128
|
+
db_probe_event_count: number;
|
|
129
|
+
db_parse_error_count: number;
|
|
130
|
+
db_last_event: InboundRawEventSummary | null;
|
|
131
|
+
db_last_probe_event: InboundRawEventSummary | null;
|
|
132
|
+
last_error: string | null;
|
|
133
|
+
}
|
|
134
|
+
|
|
21
135
|
export interface LarkServerIntegrationResult {
|
|
22
136
|
handles: LarkDaemonHandle[];
|
|
137
|
+
status(): LarkBotRuntimeStatus[];
|
|
23
138
|
reload(): LarkReloadResult;
|
|
139
|
+
restart(appIds?: string[]): LarkRestartResult;
|
|
140
|
+
restartStale(options: LarkRestartStaleOptions): LarkRestartResult;
|
|
141
|
+
injectProbeEvent(input: LarkProbeEventInput): Promise<LarkProbeEventResult>;
|
|
24
142
|
stop(): void;
|
|
25
143
|
}
|
|
26
144
|
|
|
@@ -51,6 +169,13 @@ function mentionsBotLabel(envelope: LarkMessageEnvelope, bot: LarkCredential): b
|
|
|
51
169
|
return (envelope.message?.mentions ?? []).some((mention) => mention.name?.trim().toLowerCase() === expected);
|
|
52
170
|
}
|
|
53
171
|
|
|
172
|
+
function mentionOpenIdForBotLabel(envelope: LarkMessageEnvelope, bot: LarkCredential): string | null {
|
|
173
|
+
if (!bot.label) return null;
|
|
174
|
+
const expected = bot.label.trim().toLowerCase();
|
|
175
|
+
if (!expected) return null;
|
|
176
|
+
const mention = (envelope.message?.mentions ?? []).find((entry) => entry.name?.trim().toLowerCase() === expected);
|
|
177
|
+
return mention?.id?.open_id?.trim() || null;
|
|
178
|
+
}
|
|
54
179
|
|
|
55
180
|
function senderUnionId(envelope: LarkMessageEnvelope): string | null {
|
|
56
181
|
return envelope.sender?.sender_id?.union_id?.trim() || null;
|
|
@@ -64,6 +189,29 @@ function deliveryReactionEmojiType(): string {
|
|
|
64
189
|
return process.env.PAL_LARK_ACTION_REACTION_EMOJI?.trim() || 'Typing';
|
|
65
190
|
}
|
|
66
191
|
|
|
192
|
+
function addSingleAgentBotMentionMapping(input: {
|
|
193
|
+
mappings: Map<string, string>;
|
|
194
|
+
bot: LarkCredential;
|
|
195
|
+
botOpenIdByApp: Map<string, string>;
|
|
196
|
+
fallbackBotOpenId?: string | null;
|
|
197
|
+
envelope?: LarkMessageEnvelope;
|
|
198
|
+
}): void {
|
|
199
|
+
const agents = boundAgents(input.bot);
|
|
200
|
+
if (agents.length !== 1) return;
|
|
201
|
+
const openId = input.botOpenIdByApp.get(input.bot.appId)
|
|
202
|
+
?? input.bot.botOpenId
|
|
203
|
+
?? (input.envelope ? mentionOpenIdForBotLabel(input.envelope, input.bot) : null)
|
|
204
|
+
?? input.fallbackBotOpenId
|
|
205
|
+
?? null;
|
|
206
|
+
if (openId) input.mappings.set(openId, agents[0]!);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function hasDeliveryForAgents(deliveries: Array<{ agent: string }>, agents: string[]): boolean {
|
|
210
|
+
if (agents.length === 0) return false;
|
|
211
|
+
const agentSet = new Set(agents);
|
|
212
|
+
return deliveries.some((delivery) => agentSet.has(delivery.agent));
|
|
213
|
+
}
|
|
214
|
+
|
|
67
215
|
function apiErrorMessage(err: unknown): string {
|
|
68
216
|
if (err instanceof Error) {
|
|
69
217
|
const responseData = (err as Error & { response?: { data?: unknown } }).response?.data;
|
|
@@ -78,6 +226,78 @@ function apiErrorMessage(err: unknown): string {
|
|
|
78
226
|
return String(err);
|
|
79
227
|
}
|
|
80
228
|
|
|
229
|
+
function timestampFromMs(value: number | undefined): string | null {
|
|
230
|
+
return value ? new Date(value).toISOString() : null;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function parseInboundReceivedAt(value: string | null | undefined): number | null {
|
|
234
|
+
if (!value) return null;
|
|
235
|
+
const normalized = /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/.test(value)
|
|
236
|
+
? `${value.replace(' ', 'T')}Z`
|
|
237
|
+
: value;
|
|
238
|
+
const time = Date.parse(normalized);
|
|
239
|
+
return Number.isFinite(time) ? time : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function hasProviderEventAfterRestart(status: { db_last_event?: InboundRawEventSummary | null; last_restart_at?: string | null }): boolean {
|
|
243
|
+
const lastRestartAt = parseInboundReceivedAt(status.last_restart_at);
|
|
244
|
+
const lastProviderEventAt = parseInboundReceivedAt(status.db_last_event?.received_at);
|
|
245
|
+
return lastRestartAt !== null && lastProviderEventAt !== null && lastProviderEventAt > lastRestartAt;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
function isLocalProbeEvent(storeResult: Pick<LarkProbeEventResult, 'raw_event_id'> | { event_id?: string; event_type?: string }): boolean {
|
|
249
|
+
const eventId = 'raw_event_id' in storeResult ? storeResult.raw_event_id : storeResult.event_id;
|
|
250
|
+
const eventType = 'event_type' in storeResult ? storeResult.event_type : undefined;
|
|
251
|
+
return Boolean(eventId?.startsWith('pal.probe:') || eventType?.startsWith('pal.probe.'));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function isLocalProbeChatId(chatId: string | null | undefined): boolean {
|
|
255
|
+
return Boolean(chatId?.includes('pal_probe') || chatId?.startsWith('oc_probe'));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function larkWsConnectionStatus(handle: LarkDaemonHandle): Pick<LarkBotRuntimeStatus, 'ws_state' | 'ws_last_connect_at' | 'ws_next_connect_at' | 'ws_reconnect_attempts'> {
|
|
259
|
+
try {
|
|
260
|
+
const status = (handle.wsClient as unknown as {
|
|
261
|
+
getConnectionStatus?: () => {
|
|
262
|
+
state?: string;
|
|
263
|
+
lastConnectTime?: number;
|
|
264
|
+
nextConnectTime?: number;
|
|
265
|
+
reconnectAttempts?: number;
|
|
266
|
+
};
|
|
267
|
+
}).getConnectionStatus?.();
|
|
268
|
+
return {
|
|
269
|
+
ws_state: status?.state ?? null,
|
|
270
|
+
ws_last_connect_at: timestampFromMs(status?.lastConnectTime),
|
|
271
|
+
ws_next_connect_at: timestampFromMs(status?.nextConnectTime),
|
|
272
|
+
ws_reconnect_attempts: typeof status?.reconnectAttempts === 'number' ? status.reconnectAttempts : null,
|
|
273
|
+
};
|
|
274
|
+
} catch {
|
|
275
|
+
return {
|
|
276
|
+
ws_state: null,
|
|
277
|
+
ws_last_connect_at: null,
|
|
278
|
+
ws_next_connect_at: null,
|
|
279
|
+
ws_reconnect_attempts: null,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
function notifyLarkDeliveries(input: {
|
|
285
|
+
notifier: LarkServerIntegrationOptions['deliveryNotifier'];
|
|
286
|
+
deliveries: MessageDelivery[];
|
|
287
|
+
appId: string;
|
|
288
|
+
log: Pick<Console, 'log' | 'warn'>;
|
|
289
|
+
}): LarkDeliveryNotifyResult | null {
|
|
290
|
+
if (!input.notifier) return null;
|
|
291
|
+
try {
|
|
292
|
+
const notify = input.notifier.notifyDeliveries(input.deliveries);
|
|
293
|
+
input.log.log(`[lark/${input.appId}] notify deliveries=${notify.deliveries} target_connections=${notify.target_connections} open_sockets=${notify.open_sockets} websocket_frames=${notify.websocket_frames}`);
|
|
294
|
+
return notify;
|
|
295
|
+
} catch (err) {
|
|
296
|
+
input.log.warn(`[lark/${input.appId}] notify failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
297
|
+
return null;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
81
301
|
async function syncLarkRoomMetadata(input: {
|
|
82
302
|
bot: LarkCredential;
|
|
83
303
|
chatId: string;
|
|
@@ -155,6 +375,7 @@ async function syncExistingLarkGroupRooms(input: {
|
|
|
155
375
|
seen.add(key);
|
|
156
376
|
const bot = botsByApp.get(mapping.app_id);
|
|
157
377
|
if (!bot) continue;
|
|
378
|
+
if (isLocalProbeChatId(mapping.external_chat_id)) continue;
|
|
158
379
|
await syncLarkRoomMetadata({
|
|
159
380
|
bot,
|
|
160
381
|
chatId: mapping.external_chat_id,
|
|
@@ -214,7 +435,12 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
|
|
|
214
435
|
const msgStore = options.store;
|
|
215
436
|
const botOpenIdByApp = options.botOpenIdByApp ?? new Map<string, string>();
|
|
216
437
|
const startDaemon = options.startDaemon ?? startLarkDaemon;
|
|
217
|
-
const
|
|
438
|
+
const runtimeHome = homeDir();
|
|
439
|
+
const configPath = options.configPath ?? defaultLarkConfigPath();
|
|
440
|
+
const dbPath = options.dbPath ?? null;
|
|
441
|
+
const syncRoomMetadataEnabled = options.syncRoomMetadata ?? true;
|
|
442
|
+
const sendDeliveryReactionEnabled = options.sendDeliveryReaction ?? true;
|
|
443
|
+
const active = new Map<string, { bot: LarkCredential; handle: LarkDaemonHandle; fingerprint: string; status: LarkBotRuntimeStatus; onEvent: LarkEventCallback; onLifecycle: LarkWebSocketLifecycleCallback }>();
|
|
218
444
|
|
|
219
445
|
const activeBots = (): LarkCredential[] => Array.from(active.values()).map((entry) => entry.bot);
|
|
220
446
|
const activeHandles = (): LarkDaemonHandle[] => Array.from(active.values()).map((entry) => entry.handle);
|
|
@@ -247,135 +473,280 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
|
|
|
247
473
|
});
|
|
248
474
|
};
|
|
249
475
|
|
|
250
|
-
const startBot = (bot: LarkCredential): LarkDaemonHandle =>
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
476
|
+
const startBot = (bot: LarkCredential): { handle: LarkDaemonHandle; status: LarkBotRuntimeStatus; onEvent: LarkEventCallback; onLifecycle: LarkWebSocketLifecycleCallback } => {
|
|
477
|
+
const restartState = msgStore.getLarkWebsocketRestartState(bot.appId);
|
|
478
|
+
const status: LarkBotRuntimeStatus = {
|
|
479
|
+
app_id: bot.appId,
|
|
480
|
+
agent: bot.agent?.trim() || null,
|
|
481
|
+
label: bot.label?.trim() || null,
|
|
482
|
+
bot_open_id_known: Boolean(bot.botOpenId?.trim() || botOpenIdByApp.get(bot.appId)),
|
|
483
|
+
authorized_user_count: 0,
|
|
484
|
+
runtime_home: runtimeHome,
|
|
485
|
+
config_path: configPath,
|
|
486
|
+
db_path: dbPath,
|
|
487
|
+
ws_state: null,
|
|
488
|
+
ws_last_connect_at: null,
|
|
489
|
+
ws_next_connect_at: null,
|
|
490
|
+
ws_reconnect_attempts: null,
|
|
491
|
+
ws_last_event_at: null,
|
|
492
|
+
ws_last_event_type: null,
|
|
493
|
+
ws_last_error: null,
|
|
494
|
+
restart_count: restartState?.restart_count ?? 0,
|
|
495
|
+
last_restart_at: restartState?.last_restart_at ?? null,
|
|
496
|
+
last_restart_reason: restartState?.last_restart_reason ?? null,
|
|
497
|
+
started_at: new Date().toISOString(),
|
|
498
|
+
event_count: 0,
|
|
499
|
+
message_event_count: 0,
|
|
500
|
+
provider_event_count: 0,
|
|
501
|
+
provider_message_event_count: 0,
|
|
502
|
+
last_event_at: null,
|
|
503
|
+
last_event_type: null,
|
|
504
|
+
last_provider_event_at: null,
|
|
505
|
+
last_provider_message_event_at: null,
|
|
506
|
+
last_message_event_at: null,
|
|
507
|
+
last_raw_event_id: null,
|
|
508
|
+
last_ingest_status: null,
|
|
509
|
+
last_delivery_count: null,
|
|
510
|
+
last_delivery_notify: null,
|
|
511
|
+
db_event_count: 0,
|
|
512
|
+
db_provider_event_count: 0,
|
|
513
|
+
db_probe_event_count: 0,
|
|
514
|
+
db_parse_error_count: 0,
|
|
515
|
+
db_last_event: null,
|
|
516
|
+
db_last_probe_event: null,
|
|
517
|
+
last_error: null,
|
|
518
|
+
};
|
|
519
|
+
|
|
520
|
+
const onLifecycle: LarkWebSocketLifecycleCallback = (event) => {
|
|
521
|
+
status.ws_last_event_at = new Date().toISOString();
|
|
522
|
+
status.ws_last_event_type = event.type;
|
|
523
|
+
status.ws_last_error = event.error ?? null;
|
|
524
|
+
};
|
|
525
|
+
|
|
526
|
+
const onEvent: LarkEventCallback = async ({ envelope, data, storeResult }) => {
|
|
527
|
+
const isProbe = isLocalProbeEvent(storeResult);
|
|
528
|
+
status.event_count += 1;
|
|
529
|
+
status.last_event_at = new Date().toISOString();
|
|
530
|
+
status.last_event_type = envelope;
|
|
531
|
+
status.last_raw_event_id = storeResult.event_id;
|
|
532
|
+
status.last_error = null;
|
|
533
|
+
if (!isProbe) {
|
|
534
|
+
status.provider_event_count += 1;
|
|
535
|
+
status.last_provider_event_at = status.last_event_at;
|
|
536
|
+
}
|
|
537
|
+
log.log(`[lark/${bot.appId}] ${envelope} stored id=${storeResult.id} event_id=${storeResult.event_id} parse_ok=${storeResult.parse_ok}`);
|
|
538
|
+
if (envelope === 'im.message.receive_v1') {
|
|
539
|
+
status.message_event_count += 1;
|
|
540
|
+
status.last_message_event_at = status.last_event_at;
|
|
541
|
+
if (!isProbe) {
|
|
542
|
+
status.provider_message_event_count += 1;
|
|
543
|
+
status.last_provider_message_event_at = status.last_event_at;
|
|
281
544
|
}
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
log.log(
|
|
296
|
-
`[lark/${bot.appId}] ingest ${tag} lock_msg=${result.message.id} chat=${result.message.chat_name} parent=${result.message.parent_id ?? '-'}`,
|
|
297
|
-
);
|
|
298
|
-
const larkMessage = (data as LarkMessageEnvelope).message;
|
|
299
|
-
if (larkMessage?.chat_id) {
|
|
300
|
-
try {
|
|
301
|
-
const account = msgStore.registerChannelAccount({
|
|
302
|
-
name: bot.label ?? bot.appId,
|
|
303
|
-
appId: bot.appId,
|
|
304
|
-
botOpenId,
|
|
305
|
-
agent: agents.length === 1 ? agents[0]! : null,
|
|
306
|
-
});
|
|
307
|
-
msgStore.resolveChannelConversation({
|
|
308
|
-
accountId: account.id,
|
|
309
|
-
chatName: result.message.chat_name,
|
|
310
|
-
conversationKey: `${result.message.chat_name}:app:${bot.appId}`,
|
|
311
|
-
externalChatId: larkMessage.chat_id,
|
|
312
|
-
externalRootId: larkMessage.root_id ?? null,
|
|
313
|
-
externalThreadId: larkMessage.thread_id ?? null,
|
|
314
|
-
scope: larkMessage.root_id || larkMessage.thread_id ? 'thread' : larkMessage.chat_type === 'p2p' ? 'p2p' : 'chat',
|
|
315
|
-
chatType: larkMessage.chat_type === 'p2p' ? 'p2p' : 'group',
|
|
316
|
-
auditOnly: false,
|
|
317
|
-
});
|
|
318
|
-
await syncLarkRoomMetadata({
|
|
319
|
-
bot,
|
|
320
|
-
chatId: larkMessage.chat_id,
|
|
321
|
-
roomId: result.message.chat_id,
|
|
322
|
-
store: msgStore,
|
|
323
|
-
botOpenId,
|
|
324
|
-
agent: agents.length === 1 ? agents[0]! : null,
|
|
325
|
-
log,
|
|
326
|
-
});
|
|
327
|
-
} catch (err) {
|
|
328
|
-
log.warn(`[lark/${bot.appId}] channel mapping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
329
|
-
}
|
|
545
|
+
status.last_ingest_status = null;
|
|
546
|
+
status.last_delivery_count = null;
|
|
547
|
+
status.last_delivery_notify = null;
|
|
548
|
+
try {
|
|
549
|
+
const larkEnvelope = data as LarkMessageEnvelope;
|
|
550
|
+
const identity = await resolveSenderUnionId({ bot, envelope: larkEnvelope, store: msgStore });
|
|
551
|
+
if (identity.status === 'pending') {
|
|
552
|
+
msgStore.recordPendingInboundEvent({ rawEventId: storeResult.event_id, provider: 'lark', reason: 'missing_lark_sender_user_id', error: identity.reason });
|
|
553
|
+
status.last_ingest_status = 'pending_sender_identity';
|
|
554
|
+
status.last_delivery_count = 0;
|
|
555
|
+
status.last_delivery_notify = null;
|
|
556
|
+
log.warn(`[lark/${bot.appId}] sender union_id lookup pending: ${identity.reason}`);
|
|
557
|
+
return;
|
|
330
558
|
}
|
|
331
|
-
if (
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
559
|
+
if (identity.status !== 'resolved' || !msgStore.isLarkAuthorizedUser(identity.unionId)) {
|
|
560
|
+
status.last_ingest_status = 'skipped_unauthorized_sender';
|
|
561
|
+
status.last_delivery_count = 0;
|
|
562
|
+
status.last_delivery_notify = null;
|
|
563
|
+
log.log(`[lark/${bot.appId}] business ingest skipped sender_union=${identity.status === 'resolved' ? identity.unionId : '-'} authorized=false`);
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
const agents = boundAgents(bot);
|
|
567
|
+
const mentionOpenIds = extractMentionOpenIds(larkEnvelope);
|
|
568
|
+
const inferredBotOpenId = agents.length === 1 && mentionOpenIds.length === 1 ? mentionOpenIds[0] : null;
|
|
569
|
+
const botOpenId = botOpenIdByApp.get(bot.appId) ?? bot.botOpenId ?? inferredBotOpenId;
|
|
570
|
+
const recipientByMentionOpenId = new Map<string, string>();
|
|
571
|
+
for (const candidate of activeBots()) {
|
|
572
|
+
addSingleAgentBotMentionMapping({
|
|
573
|
+
mappings: recipientByMentionOpenId,
|
|
574
|
+
bot: candidate,
|
|
575
|
+
botOpenIdByApp,
|
|
576
|
+
fallbackBotOpenId: candidate.appId === bot.appId ? inferredBotOpenId : null,
|
|
577
|
+
envelope: larkEnvelope,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
const labelMentionedThisBot = mentionsBotLabel(larkEnvelope, bot);
|
|
581
|
+
const labelMentionedAnyKnownBot = activeBots().some((candidate) => mentionsBotLabel(larkEnvelope, candidate));
|
|
582
|
+
if (labelMentionedAnyKnownBot && !labelMentionedThisBot) {
|
|
583
|
+
status.last_ingest_status = 'skipped_other_bot_label';
|
|
584
|
+
status.last_delivery_count = 0;
|
|
585
|
+
status.last_delivery_notify = null;
|
|
586
|
+
log.log(`[lark/${bot.appId}] label mention targets another bot, skipping ingest`);
|
|
587
|
+
return;
|
|
588
|
+
}
|
|
589
|
+
const hasReplyContext = Boolean(larkEnvelope.message?.root_id || larkEnvelope.message?.parent_id);
|
|
590
|
+
const recipientOverride = agents.length === 1 && (
|
|
591
|
+
larkEnvelope.message?.chat_type === 'p2p' ||
|
|
592
|
+
labelMentionedThisBot ||
|
|
593
|
+
hasReplyContext
|
|
594
|
+
)
|
|
595
|
+
? agents[0]!
|
|
596
|
+
: undefined;
|
|
597
|
+
|
|
598
|
+
const result = ingestLarkMessage({
|
|
599
|
+
appId: bot.appId,
|
|
600
|
+
envelope: larkEnvelope,
|
|
601
|
+
store: msgStore,
|
|
602
|
+
recipientByMentionOpenId,
|
|
603
|
+
recipientOverride,
|
|
604
|
+
});
|
|
605
|
+
if (result.status === 'ok' && result.message) {
|
|
606
|
+
const tag = result.deduped ? 'dup' : result.threadOrphan ? 'orphan' : 'new';
|
|
607
|
+
status.last_ingest_status = `ok_${tag}`;
|
|
608
|
+
log.log(
|
|
609
|
+
`[lark/${bot.appId}] ingest ${tag} lock_msg=${result.message.id} chat=${result.message.chat_name} parent=${result.message.parent_id ?? '-'}`,
|
|
610
|
+
);
|
|
611
|
+
const larkMessage = (data as LarkMessageEnvelope).message;
|
|
612
|
+
if (!result.deduped) {
|
|
613
|
+
for (const resource of extractLarkImageResources(larkEnvelope)) {
|
|
341
614
|
try {
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
615
|
+
const content = await downloadLarkImage({ appId: bot.appId, appSecret: bot.appSecret, resource });
|
|
616
|
+
const attachment = msgStore.addMessageAttachment(result.message.id, {
|
|
617
|
+
kind: 'image',
|
|
618
|
+
mimeType: resource.mimeType,
|
|
619
|
+
filename: resource.filename,
|
|
620
|
+
content,
|
|
621
|
+
sourceProvider: 'lark',
|
|
622
|
+
sourceRef: resource.fileKey,
|
|
347
623
|
});
|
|
348
|
-
log.log(`[lark/${bot.appId}]
|
|
624
|
+
log.log(`[lark/${bot.appId}] stored image attachment=${attachment.id} message=${result.message.id} bytes=${attachment.size_bytes}`);
|
|
349
625
|
} catch (err) {
|
|
350
|
-
log.warn(`[lark/${bot.appId}]
|
|
626
|
+
log.warn(`[lark/${bot.appId}] image download failed message=${result.message.id}: ${apiErrorMessage(err)}`);
|
|
351
627
|
}
|
|
352
628
|
}
|
|
353
629
|
}
|
|
630
|
+
if (larkMessage?.chat_id && !isProbe) {
|
|
631
|
+
try {
|
|
632
|
+
const account = msgStore.registerChannelAccount({
|
|
633
|
+
name: bot.label ?? bot.appId,
|
|
634
|
+
appId: bot.appId,
|
|
635
|
+
botOpenId,
|
|
636
|
+
agent: agents.length === 1 ? agents[0]! : null,
|
|
637
|
+
});
|
|
638
|
+
msgStore.resolveChannelConversation({
|
|
639
|
+
accountId: account.id,
|
|
640
|
+
chatName: result.message.chat_name,
|
|
641
|
+
conversationKey: `${result.message.chat_name}:app:${bot.appId}`,
|
|
642
|
+
externalChatId: larkMessage.chat_id,
|
|
643
|
+
externalRootId: larkMessage.root_id ?? null,
|
|
644
|
+
externalThreadId: larkMessage.thread_id ?? null,
|
|
645
|
+
scope: larkMessage.root_id || larkMessage.thread_id ? 'thread' : larkMessage.chat_type === 'p2p' ? 'p2p' : 'chat',
|
|
646
|
+
chatType: larkMessage.chat_type === 'p2p' ? 'p2p' : 'group',
|
|
647
|
+
auditOnly: false,
|
|
648
|
+
});
|
|
649
|
+
if (syncRoomMetadataEnabled) {
|
|
650
|
+
await syncLarkRoomMetadata({
|
|
651
|
+
bot,
|
|
652
|
+
chatId: larkMessage.chat_id,
|
|
653
|
+
roomId: result.message.chat_id,
|
|
654
|
+
store: msgStore,
|
|
655
|
+
botOpenId,
|
|
656
|
+
agent: agents.length === 1 ? agents[0]! : null,
|
|
657
|
+
log,
|
|
658
|
+
});
|
|
659
|
+
}
|
|
660
|
+
} catch (err) {
|
|
661
|
+
log.warn(`[lark/${bot.appId}] channel mapping failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (tag === 'new' || result.deduped) {
|
|
665
|
+
const deliveries = msgStore.resolveDeliveriesForMessage(result.message.id);
|
|
666
|
+
status.last_delivery_count = deliveries.length;
|
|
667
|
+
if (deliveries.length === 0) {
|
|
668
|
+
status.last_delivery_notify = null;
|
|
669
|
+
log.log(`[lark/${bot.appId}] resolver created no deliveries message=${result.message.id}`);
|
|
670
|
+
} else {
|
|
671
|
+
for (const delivery of deliveries) {
|
|
672
|
+
log.log(`[lark/${bot.appId}] resolver delivery id=${delivery.id} agent=${delivery.agent} message=${result.message.id}`);
|
|
673
|
+
}
|
|
674
|
+
if (result.deduped) {
|
|
675
|
+
status.last_delivery_notify = null;
|
|
676
|
+
log.log(`[lark/${bot.appId}] duplicate event did not re-notify deliveries message=${result.message.id}`);
|
|
677
|
+
} else {
|
|
678
|
+
status.last_delivery_notify = notifyLarkDeliveries({
|
|
679
|
+
notifier: options.deliveryNotifier,
|
|
680
|
+
deliveries,
|
|
681
|
+
appId: bot.appId,
|
|
682
|
+
log,
|
|
683
|
+
});
|
|
684
|
+
}
|
|
685
|
+
if (!isProbe && sendDeliveryReactionEnabled && hasDeliveryForAgents(deliveries, agents) && larkMessage?.message_id) {
|
|
686
|
+
const emojiType = deliveryReactionEmojiType();
|
|
687
|
+
try {
|
|
688
|
+
const client = createLarkApiClient(bot.appId, bot.appSecret);
|
|
689
|
+
const reaction = await addMessageReaction({
|
|
690
|
+
client,
|
|
691
|
+
messageId: larkMessage.message_id,
|
|
692
|
+
emojiType,
|
|
693
|
+
});
|
|
694
|
+
log.log(`[lark/${bot.appId}] added reaction emoji=${emojiType} message=${larkMessage.message_id} reaction=${reaction.reactionId ?? '-'}`);
|
|
695
|
+
} catch (err) {
|
|
696
|
+
log.warn(`[lark/${bot.appId}] add reaction failed message=${larkMessage.message_id}: ${apiErrorMessage(err)}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
} else if (result.status === 'skipped') {
|
|
702
|
+
status.last_ingest_status = `skipped_${result.reason}`;
|
|
703
|
+
status.last_delivery_count = 0;
|
|
704
|
+
status.last_delivery_notify = null;
|
|
705
|
+
log.log(`[lark/${bot.appId}] ingest skipped reason=${result.reason}`);
|
|
354
706
|
}
|
|
355
|
-
}
|
|
356
|
-
|
|
707
|
+
} catch (err) {
|
|
708
|
+
status.last_ingest_status = 'error';
|
|
709
|
+
status.last_delivery_count = 0;
|
|
710
|
+
status.last_delivery_notify = null;
|
|
711
|
+
status.last_error = err instanceof Error ? err.message : String(err);
|
|
712
|
+
log.error(`[lark/${bot.appId}] ingest error:`, err);
|
|
357
713
|
}
|
|
358
|
-
} catch (err) {
|
|
359
|
-
log.error(`[lark/${bot.appId}] ingest error:`, err);
|
|
360
714
|
}
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
|
|
715
|
+
};
|
|
716
|
+
|
|
717
|
+
const handle = startDaemon({
|
|
718
|
+
appId: bot.appId,
|
|
719
|
+
appSecret: bot.appSecret,
|
|
720
|
+
db: msgStore.db,
|
|
721
|
+
logger: { info: log.log, warn: log.warn, error: log.error },
|
|
722
|
+
onEvent,
|
|
723
|
+
onLifecycle,
|
|
724
|
+
});
|
|
725
|
+
return { handle, status, onEvent, onLifecycle };
|
|
726
|
+
};
|
|
364
727
|
|
|
365
728
|
const applyBots = (bots: LarkCredential[]): LarkReloadResult => {
|
|
366
729
|
const nextByApp = new Map(bots.map((bot) => [bot.appId, bot]));
|
|
367
730
|
const started: string[] = [];
|
|
368
731
|
const stopped: string[] = [];
|
|
369
732
|
const restarted: string[] = [];
|
|
370
|
-
const pending = new Map<string, { bot: LarkCredential; handle: LarkDaemonHandle; fingerprint: string; replacing: boolean }>();
|
|
733
|
+
const pending = new Map<string, { bot: LarkCredential; handle: LarkDaemonHandle; fingerprint: string; status: LarkBotRuntimeStatus; onEvent: LarkEventCallback; onLifecycle: LarkWebSocketLifecycleCallback; replacing: boolean }>();
|
|
371
734
|
|
|
372
735
|
try {
|
|
373
736
|
for (const bot of bots) {
|
|
374
737
|
const fingerprint = credentialFingerprint(bot);
|
|
375
738
|
const current = active.get(bot.appId);
|
|
376
739
|
if (current?.fingerprint === fingerprint) continue;
|
|
377
|
-
const
|
|
378
|
-
pending.set(bot.appId, {
|
|
740
|
+
const startedBot = startBot(bot);
|
|
741
|
+
pending.set(bot.appId, {
|
|
742
|
+
bot,
|
|
743
|
+
handle: startedBot.handle,
|
|
744
|
+
fingerprint,
|
|
745
|
+
status: startedBot.status,
|
|
746
|
+
onEvent: startedBot.onEvent,
|
|
747
|
+
onLifecycle: startedBot.onLifecycle,
|
|
748
|
+
replacing: Boolean(current),
|
|
749
|
+
});
|
|
379
750
|
}
|
|
380
751
|
} catch (err) {
|
|
381
752
|
for (const entry of pending.values()) entry.handle.stop();
|
|
@@ -392,7 +763,14 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
|
|
|
392
763
|
}
|
|
393
764
|
|
|
394
765
|
for (const [appId, entry] of pending.entries()) {
|
|
395
|
-
active.set(appId,
|
|
766
|
+
active.set(appId, {
|
|
767
|
+
bot: entry.bot,
|
|
768
|
+
handle: entry.handle,
|
|
769
|
+
fingerprint: entry.fingerprint,
|
|
770
|
+
status: entry.status,
|
|
771
|
+
onEvent: entry.onEvent,
|
|
772
|
+
onLifecycle: entry.onLifecycle,
|
|
773
|
+
});
|
|
396
774
|
if (entry.replacing) restarted.push(appId);
|
|
397
775
|
else started.push(appId);
|
|
398
776
|
}
|
|
@@ -418,6 +796,123 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
|
|
|
418
796
|
return applyBots(bots);
|
|
419
797
|
};
|
|
420
798
|
|
|
799
|
+
const restart = (appIds?: string[]): LarkRestartResult => restartWithReason(appIds, 'manual');
|
|
800
|
+
|
|
801
|
+
const restartWithReason = (appIds: string[] | undefined, reason: string): LarkRestartResult => {
|
|
802
|
+
const requested = appIds && appIds.length > 0 ? Array.from(new Set(appIds.map((id) => id.trim()).filter(Boolean))) : activeAppIds();
|
|
803
|
+
const missing = requested.filter((appId) => !active.has(appId));
|
|
804
|
+
const restarted: string[] = [];
|
|
805
|
+
for (const appId of requested) {
|
|
806
|
+
const current = active.get(appId);
|
|
807
|
+
if (!current) continue;
|
|
808
|
+
let next: { handle: LarkDaemonHandle; status: LarkBotRuntimeStatus; onEvent: LarkEventCallback; onLifecycle: LarkWebSocketLifecycleCallback };
|
|
809
|
+
try {
|
|
810
|
+
next = startBot(current.bot);
|
|
811
|
+
} catch (err) {
|
|
812
|
+
return {
|
|
813
|
+
ok: false,
|
|
814
|
+
bots: activeAppIds(),
|
|
815
|
+
restarted,
|
|
816
|
+
missing,
|
|
817
|
+
error: err instanceof Error ? err.message : String(err),
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const restartedAt = new Date().toISOString();
|
|
821
|
+
let restartState: ReturnType<MessageStore['recordLarkWebsocketRestart']>;
|
|
822
|
+
try {
|
|
823
|
+
restartState = msgStore.recordLarkWebsocketRestart({ appId, restartedAt, reason });
|
|
824
|
+
} catch (err) {
|
|
825
|
+
next.handle.stop();
|
|
826
|
+
return {
|
|
827
|
+
ok: false,
|
|
828
|
+
bots: activeAppIds(),
|
|
829
|
+
restarted,
|
|
830
|
+
missing,
|
|
831
|
+
error: err instanceof Error ? err.message : String(err),
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
next.status.restart_count = restartState.restart_count;
|
|
835
|
+
next.status.last_restart_at = restartState.last_restart_at;
|
|
836
|
+
next.status.last_restart_reason = restartState.last_restart_reason;
|
|
837
|
+
current.handle.stop();
|
|
838
|
+
active.set(appId, {
|
|
839
|
+
bot: current.bot,
|
|
840
|
+
handle: next.handle,
|
|
841
|
+
fingerprint: current.fingerprint,
|
|
842
|
+
status: next.status,
|
|
843
|
+
onEvent: next.onEvent,
|
|
844
|
+
onLifecycle: next.onLifecycle,
|
|
845
|
+
});
|
|
846
|
+
restarted.push(appId);
|
|
847
|
+
}
|
|
848
|
+
if (restarted.length > 0) {
|
|
849
|
+
afterSuccessfulScan(restarted.map((appId) => active.get(appId)?.bot).filter((bot): bot is LarkCredential => Boolean(bot)));
|
|
850
|
+
}
|
|
851
|
+
log.log(`[lark] restarted ${restarted.length} bot websocket(s)${missing.length > 0 ? ` missing=${missing.join(',')}` : ''}`);
|
|
852
|
+
return { ok: true, bots: activeAppIds(), restarted, missing };
|
|
853
|
+
};
|
|
854
|
+
|
|
855
|
+
const restartStale = (restartOptions: LarkRestartStaleOptions): LarkRestartResult => {
|
|
856
|
+
const now = restartOptions.now ?? Date.now();
|
|
857
|
+
const staleAfterMs = restartOptions.staleAfterMs;
|
|
858
|
+
const minRestartIntervalMs = restartOptions.minRestartIntervalMs ?? 0;
|
|
859
|
+
if (!Number.isFinite(staleAfterMs) || staleAfterMs <= 0) {
|
|
860
|
+
return { ok: false, bots: activeAppIds(), restarted: [], missing: [], error: 'staleAfterMs must be positive' };
|
|
861
|
+
}
|
|
862
|
+
if (!Number.isFinite(minRestartIntervalMs) || minRestartIntervalMs < 0) {
|
|
863
|
+
return { ok: false, bots: activeAppIds(), restarted: [], missing: [], error: 'minRestartIntervalMs must be zero or positive' };
|
|
864
|
+
}
|
|
865
|
+
const restartAppIds: string[] = [];
|
|
866
|
+
const skippedRecent: string[] = [];
|
|
867
|
+
const skippedIneffective: string[] = [];
|
|
868
|
+
for (const entry of active.values()) {
|
|
869
|
+
const dbProviderEventCount = countProviderInboundEvents(msgStore.db, entry.bot.appId);
|
|
870
|
+
const status = {
|
|
871
|
+
...entry.status,
|
|
872
|
+
db_event_count: dbProviderEventCount,
|
|
873
|
+
db_provider_event_count: dbProviderEventCount,
|
|
874
|
+
db_probe_event_count: countProbeInboundEvents(msgStore.db, entry.bot.appId),
|
|
875
|
+
db_parse_error_count: countInboundParseFailures(msgStore.db, entry.bot.appId),
|
|
876
|
+
db_last_event: latestInboundEventSummary(msgStore.db, entry.bot.appId),
|
|
877
|
+
db_last_probe_event: latestProbeInboundEventSummary(msgStore.db, entry.bot.appId),
|
|
878
|
+
...larkWsConnectionStatus(entry.handle),
|
|
879
|
+
};
|
|
880
|
+
if (!larkBotNeedsRestart(status, { now, staleAfterMs })) continue;
|
|
881
|
+
const lastRestartAt = parseInboundReceivedAt(entry.status.last_restart_at);
|
|
882
|
+
if (lastRestartAt !== null && minRestartIntervalMs > 0 && now - lastRestartAt < minRestartIntervalMs) {
|
|
883
|
+
skippedRecent.push(entry.bot.appId);
|
|
884
|
+
continue;
|
|
885
|
+
}
|
|
886
|
+
if (
|
|
887
|
+
entry.status.last_restart_reason === 'auto_stale'
|
|
888
|
+
&& (entry.status.restart_count ?? 0) > 0
|
|
889
|
+
&& lastRestartAt !== null
|
|
890
|
+
&& !hasProviderEventAfterRestart(status)
|
|
891
|
+
) {
|
|
892
|
+
skippedIneffective.push(entry.bot.appId);
|
|
893
|
+
continue;
|
|
894
|
+
}
|
|
895
|
+
restartAppIds.push(entry.bot.appId);
|
|
896
|
+
}
|
|
897
|
+
if (restartAppIds.length === 0) {
|
|
898
|
+
return {
|
|
899
|
+
ok: true,
|
|
900
|
+
bots: activeAppIds(),
|
|
901
|
+
restarted: [],
|
|
902
|
+
missing: [],
|
|
903
|
+
...(skippedRecent.length > 0 ? { skipped_recent: skippedRecent } : {}),
|
|
904
|
+
...(skippedIneffective.length > 0 ? { skipped_ineffective: skippedIneffective } : {}),
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
log.warn(`[lark] unhealthy/stale inbound state for ${restartAppIds.join(',')}; restarting websocket(s)`);
|
|
908
|
+
const result = restartWithReason(restartAppIds, 'auto_stale');
|
|
909
|
+
return {
|
|
910
|
+
...result,
|
|
911
|
+
...(skippedRecent.length > 0 ? { skipped_recent: skippedRecent } : {}),
|
|
912
|
+
...(skippedIneffective.length > 0 ? { skipped_ineffective: skippedIneffective } : {}),
|
|
913
|
+
};
|
|
914
|
+
};
|
|
915
|
+
|
|
421
916
|
const initial = reload();
|
|
422
917
|
if (!initial.ok) {
|
|
423
918
|
log.warn(`[lark] initial load failed: ${initial.error}`);
|
|
@@ -427,7 +922,60 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
|
|
|
427
922
|
get handles() {
|
|
428
923
|
return activeHandles();
|
|
429
924
|
},
|
|
925
|
+
status() {
|
|
926
|
+
return Array.from(active.values()).map((entry) => {
|
|
927
|
+
const dbProviderEventCount = countProviderInboundEvents(msgStore.db, entry.bot.appId);
|
|
928
|
+
return {
|
|
929
|
+
...entry.status,
|
|
930
|
+
db_event_count: dbProviderEventCount,
|
|
931
|
+
db_provider_event_count: dbProviderEventCount,
|
|
932
|
+
db_probe_event_count: countProbeInboundEvents(msgStore.db, entry.bot.appId),
|
|
933
|
+
db_parse_error_count: countInboundParseFailures(msgStore.db, entry.bot.appId),
|
|
934
|
+
db_last_event: latestInboundEventSummary(msgStore.db, entry.bot.appId),
|
|
935
|
+
db_last_probe_event: latestProbeInboundEventSummary(msgStore.db, entry.bot.appId),
|
|
936
|
+
agent: entry.bot.agent?.trim() || null,
|
|
937
|
+
label: entry.bot.label?.trim() || null,
|
|
938
|
+
bot_open_id_known: Boolean(entry.bot.botOpenId?.trim() || botOpenIdByApp.get(entry.bot.appId)),
|
|
939
|
+
authorized_user_count: msgStore.listLarkAuthorizedUsers().length,
|
|
940
|
+
...larkWsConnectionStatus(entry.handle),
|
|
941
|
+
};
|
|
942
|
+
});
|
|
943
|
+
},
|
|
430
944
|
reload,
|
|
945
|
+
restart,
|
|
946
|
+
restartStale,
|
|
947
|
+
async injectProbeEvent(input: LarkProbeEventInput): Promise<LarkProbeEventResult> {
|
|
948
|
+
const appId = input.appId.trim();
|
|
949
|
+
const entry = active.get(appId);
|
|
950
|
+
if (!entry) throw new Error(`Lark bot is not active: ${appId}`);
|
|
951
|
+
const envelope = input.envelope ?? 'im.message.receive_v1';
|
|
952
|
+
const raw = JSON.parse(buildLarkEventRawBody(envelope, input.data)) as {
|
|
953
|
+
header?: { event_id?: string; event_type?: string };
|
|
954
|
+
};
|
|
955
|
+
raw.header = raw.header ?? {};
|
|
956
|
+
raw.header.event_type = `pal.probe.${envelope}`;
|
|
957
|
+
raw.header.event_id = raw.header.event_id ? `pal.probe:${raw.header.event_id}` : `pal.probe:${envelope}:${crypto.randomUUID()}`;
|
|
958
|
+
const rawBody = JSON.stringify(raw);
|
|
959
|
+
const storeResult = await storeInboundEvent(msgStore.db, { appId, rawBody });
|
|
960
|
+
if (!storeResult.duplicate) {
|
|
961
|
+
await entry.onEvent({
|
|
962
|
+
appId,
|
|
963
|
+
envelope,
|
|
964
|
+
data: input.data,
|
|
965
|
+
rawBody,
|
|
966
|
+
storeResult,
|
|
967
|
+
});
|
|
968
|
+
}
|
|
969
|
+
return {
|
|
970
|
+
app_id: appId,
|
|
971
|
+
envelope,
|
|
972
|
+
raw_event_id: storeResult.event_id,
|
|
973
|
+
inserted: storeResult.inserted,
|
|
974
|
+
duplicate: storeResult.duplicate,
|
|
975
|
+
parse_ok: storeResult.parse_ok,
|
|
976
|
+
handled: !storeResult.duplicate,
|
|
977
|
+
};
|
|
978
|
+
},
|
|
431
979
|
stop() {
|
|
432
980
|
const handles = activeHandles();
|
|
433
981
|
for (const h of handles) h.stop();
|