@controlflow-ai/daemon 0.1.1 → 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 +66 -24
- package/package.json +16 -3
- 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 +810 -28
- package/src/agent-workspace.ts +183 -0
- package/src/app.ts +2183 -79
- package/src/args.ts +54 -7
- package/src/cli.ts +873 -14
- package/src/client.ts +482 -12
- package/src/coco.ts +9 -40
- package/src/codex.ts +33 -5
- package/src/config.ts +28 -4
- package/src/console.ts +460 -26
- package/src/daemon-client.ts +116 -3
- package/src/daemon.ts +958 -101
- package/src/db.ts +3216 -113
- package/src/delivery-ws.ts +269 -0
- package/src/format.ts +4 -1
- package/src/lark/app-registration.ts +141 -0
- package/src/lark/cli.ts +7 -137
- package/src/lark/credentials.ts +36 -3
- package/src/lark/event-router.ts +61 -5
- package/src/lark/inbound-events.ts +156 -3
- package/src/lark/server-integration.ts +659 -111
- package/src/lark/setup.ts +74 -5
- package/src/lark/ws-daemon.ts +136 -10
- package/src/local-api.ts +611 -14
- package/src/local-auth.ts +36 -3
- package/src/message-attachments.ts +71 -0
- package/src/messaging-cli.ts +741 -0
- package/src/messaging-status.ts +669 -0
- package/src/migrations/023_projects.ts +65 -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 +70 -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 +394 -0
- package/src/workflow-runtime.ts +275 -0
- package/src/web.ts +0 -904
package/src/lark/setup.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { createInterface } from 'node:readline';
|
|
2
|
+
import QRCode from 'qrcode';
|
|
2
3
|
import type { LarkCredential } from './credentials.js';
|
|
3
4
|
import { loadLarkCredentials, saveLarkCredentials, upsertCredential } from './credentials.js';
|
|
5
|
+
import { beginLarkAppRegistration, pollLarkAppRegistration, type LarkRegistrationComplete } from './app-registration.js';
|
|
4
6
|
|
|
5
7
|
export interface CredentialValidationOk {
|
|
6
8
|
ok: true;
|
|
@@ -62,6 +64,7 @@ export interface InteractiveSetupOptions {
|
|
|
62
64
|
listAgents?: () => Promise<LarkSetupAgentOption[]>;
|
|
63
65
|
validateCredentials?: typeof validateLarkCredentials;
|
|
64
66
|
resolveBotInfo?: typeof resolveLarkBotInfo;
|
|
67
|
+
registerApp?: typeof registerLarkAppFromDeviceFlow;
|
|
65
68
|
}
|
|
66
69
|
|
|
67
70
|
function clean(value: string | undefined): string | undefined {
|
|
@@ -69,6 +72,41 @@ function clean(value: string | undefined): string | undefined {
|
|
|
69
72
|
return trimmed ? trimmed : undefined;
|
|
70
73
|
}
|
|
71
74
|
|
|
75
|
+
export async function registerLarkAppFromDeviceFlow(options: {
|
|
76
|
+
log: Pick<Console, 'log' | 'warn' | 'error'>;
|
|
77
|
+
qrCode?: (url: string) => Promise<string>;
|
|
78
|
+
fetchImpl?: FetchLike;
|
|
79
|
+
}): Promise<LarkRegistrationComplete | null> {
|
|
80
|
+
const begin = await beginLarkAppRegistration({ fetchImpl: options.fetchImpl, source: 'pal' });
|
|
81
|
+
options.log.log('Create a Feishu app by scanning this QR code or opening the link:');
|
|
82
|
+
options.log.log('');
|
|
83
|
+
const terminalQr = options.qrCode
|
|
84
|
+
? await options.qrCode(begin.url)
|
|
85
|
+
: await QRCode.toString(begin.url, { type: 'terminal', small: true });
|
|
86
|
+
options.log.log(terminalQr);
|
|
87
|
+
options.log.log(begin.url);
|
|
88
|
+
options.log.log('');
|
|
89
|
+
options.log.log(`Waiting for confirmation. The code expires in about ${Math.max(1, Math.round(begin.expiresIn / 60))} minutes.`);
|
|
90
|
+
|
|
91
|
+
let intervalMs = Math.max(1, begin.interval) * 1000;
|
|
92
|
+
const deadline = Date.now() + begin.expiresIn * 1000;
|
|
93
|
+
while (Date.now() < deadline) {
|
|
94
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
95
|
+
const result = await pollLarkAppRegistration({ deviceCode: begin.deviceCode, fetchImpl: options.fetchImpl });
|
|
96
|
+
if (result.status === 'pending') continue;
|
|
97
|
+
if (result.status === 'slow_down') {
|
|
98
|
+
intervalMs = Math.max(intervalMs + 5000, result.interval * 1000);
|
|
99
|
+
options.log.warn(`Feishu asked us to slow polling; retrying every ${Math.round(intervalMs / 1000)}s.`);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
if (result.status === 'complete') return result.registration;
|
|
103
|
+
options.log.warn(`App registration did not complete (${result.status}): ${result.message}`);
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
options.log.warn('App registration expired before it completed.');
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
72
110
|
async function chooseAgent(options: InteractiveSetupOptions, ask: (question: string) => Promise<string>): Promise<string | null> {
|
|
73
111
|
if (!options.listAgents) {
|
|
74
112
|
return clean(await ask('Bind to agent key [codex]: ')) ?? 'codex';
|
|
@@ -79,7 +117,7 @@ async function chooseAgent(options: InteractiveSetupOptions, ask: (question: str
|
|
|
79
117
|
agents = await options.listAgents();
|
|
80
118
|
} catch (error) {
|
|
81
119
|
options.log.error(`Could not load agent list: ${error instanceof Error ? error.message : String(error)}`);
|
|
82
|
-
options.log.error('Set up an agent first, then
|
|
120
|
+
options.log.error('Set up an agent first, then bind the bot from agent settings.');
|
|
83
121
|
return null;
|
|
84
122
|
}
|
|
85
123
|
|
|
@@ -275,9 +313,37 @@ export async function runInteractiveLarkSetup(options: InteractiveSetupOptions):
|
|
|
275
313
|
const agent = await chooseAgent(options, ask);
|
|
276
314
|
if (!agent) return null;
|
|
277
315
|
options.log.log('');
|
|
278
|
-
options.log.log('
|
|
279
|
-
|
|
280
|
-
|
|
316
|
+
options.log.log('Create a Feishu app by scan/link, or paste an existing credential.');
|
|
317
|
+
options.log.log(' 1. Scan or open link to create app');
|
|
318
|
+
options.log.log(' 2. Paste App ID / App Secret');
|
|
319
|
+
const method = clean(await ask('Setup method [1]: ')) ?? '1';
|
|
320
|
+
let appId: string | undefined;
|
|
321
|
+
let appSecret: string | undefined;
|
|
322
|
+
let scannedUserOpenId: string | undefined;
|
|
323
|
+
if (method !== '2') {
|
|
324
|
+
const registerApp = options.registerApp ?? registerLarkAppFromDeviceFlow;
|
|
325
|
+
const registration = await registerApp({ log: options.log }).catch((error) => {
|
|
326
|
+
options.log.warn(`App registration failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
327
|
+
return null;
|
|
328
|
+
});
|
|
329
|
+
if (!registration) {
|
|
330
|
+
options.log.warn('Falling back to manual App ID / App Secret entry.');
|
|
331
|
+
} else if (registration.tenantBrand === 'lark') {
|
|
332
|
+
options.log.error('Lark international tenants are not supported yet; Pal currently expects Feishu (feishu.cn) credentials.');
|
|
333
|
+
options.log.error('No config was written.');
|
|
334
|
+
return null;
|
|
335
|
+
} else {
|
|
336
|
+
appId = registration.appId;
|
|
337
|
+
appSecret = registration.appSecret;
|
|
338
|
+
scannedUserOpenId = registration.userOpenId;
|
|
339
|
+
options.log.log(`App created: ${appId}`);
|
|
340
|
+
if (scannedUserOpenId) options.log.log(`Scanner open_id: ${scannedUserOpenId}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (!appId || !appSecret) {
|
|
344
|
+
appId = clean(await ask('App ID (cli_xxx): '));
|
|
345
|
+
appSecret = clean(await ask('App Secret: '));
|
|
346
|
+
}
|
|
281
347
|
if (!appId || !appSecret) {
|
|
282
348
|
options.log.error('App ID and App Secret are required; no config was written.');
|
|
283
349
|
return null;
|
|
@@ -315,8 +381,11 @@ export async function runInteractiveLarkSetup(options: InteractiveSetupOptions):
|
|
|
315
381
|
agent,
|
|
316
382
|
configPath: options.configPath,
|
|
317
383
|
});
|
|
384
|
+
if (scannedUserOpenId) {
|
|
385
|
+
options.log.log(`Scanner open_id ${scannedUserOpenId} was detected; add it under Lark authorized users if this user should operate Pal.`);
|
|
386
|
+
}
|
|
318
387
|
if (result.replaced) {
|
|
319
|
-
options.log.warn(`[lark
|
|
388
|
+
options.log.warn(`[lark] overwrote existing credential for appId=${appId} in ${options.configPath}`);
|
|
320
389
|
}
|
|
321
390
|
options.log.log(formatLarkSetupNextSteps(result));
|
|
322
391
|
return result;
|
package/src/lark/ws-daemon.ts
CHANGED
|
@@ -10,13 +10,22 @@ export type LarkEventCallback = (event: {
|
|
|
10
10
|
storeResult: StoreInboundEventResult;
|
|
11
11
|
}) => void | Promise<void>;
|
|
12
12
|
|
|
13
|
+
export type LarkWebSocketLifecycleCallback = (event: {
|
|
14
|
+
appId: string;
|
|
15
|
+
type: 'ready' | 'reconnecting' | 'reconnected' | 'error';
|
|
16
|
+
error?: string;
|
|
17
|
+
}) => void;
|
|
18
|
+
|
|
13
19
|
export interface StartLarkDaemonOptions {
|
|
14
20
|
appId: string;
|
|
15
21
|
appSecret: string;
|
|
16
22
|
db: Database;
|
|
17
23
|
onEvent?: LarkEventCallback;
|
|
24
|
+
onLifecycle?: LarkWebSocketLifecycleCallback;
|
|
18
25
|
logger?: Partial<Pick<Console, 'info' | 'warn' | 'error'>>;
|
|
19
26
|
loggerLevel?: Lark.LoggerLevel;
|
|
27
|
+
handshakeTimeoutMs?: number;
|
|
28
|
+
pingTimeoutSec?: number;
|
|
20
29
|
}
|
|
21
30
|
|
|
22
31
|
export interface LarkDaemonHandle {
|
|
@@ -26,6 +35,44 @@ export interface LarkDaemonHandle {
|
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
const SUPPORTED_EVENTS = ['im.message.receive_v1', 'card.action.trigger'] as const;
|
|
38
|
+
const silentLarkLogger = {
|
|
39
|
+
error() {},
|
|
40
|
+
warn() {},
|
|
41
|
+
info() {},
|
|
42
|
+
debug() {},
|
|
43
|
+
trace() {},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
function nestedString(value: unknown, path: string[]): string | null {
|
|
47
|
+
let current = value;
|
|
48
|
+
for (const key of path) {
|
|
49
|
+
if (!current || typeof current !== 'object') return null;
|
|
50
|
+
current = (current as Record<string, unknown>)[key];
|
|
51
|
+
}
|
|
52
|
+
return typeof current === 'string' && current.length > 0 ? current : null;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function stableLarkEventId(eventName: string, data: unknown): string | null {
|
|
56
|
+
const headerId = nestedString(data, ['header', 'event_id'])
|
|
57
|
+
?? nestedString(data, ['event_id']);
|
|
58
|
+
if (headerId) return headerId;
|
|
59
|
+
const derivedId = nestedString(data, ['message', 'message_id'])
|
|
60
|
+
?? nestedString(data, ['message', 'open_message_id'])
|
|
61
|
+
?? nestedString(data, ['open_message_id']);
|
|
62
|
+
if (derivedId) return `${eventName}:${derivedId}`;
|
|
63
|
+
const operatorId = nestedString(data, ['operator', 'operator_id', 'open_id']);
|
|
64
|
+
if (operatorId) return `${eventName}:${operatorId}:${nestedString(data, ['token']) ?? ''}`;
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function buildLarkEventRawBody(eventName: string, data: unknown): string {
|
|
69
|
+
const eventId = stableLarkEventId(eventName, data);
|
|
70
|
+
return JSON.stringify({
|
|
71
|
+
schema: '2.0',
|
|
72
|
+
header: eventId ? { event_id: eventId, event_type: eventName } : { event_type: eventName },
|
|
73
|
+
event: data,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
29
76
|
|
|
30
77
|
/**
|
|
31
78
|
* Start a Lark WSClient that subscribes to im.message.receive_v1 +
|
|
@@ -45,7 +92,7 @@ export function startLarkDaemon(options: StartLarkDaemonOptions): LarkDaemonHand
|
|
|
45
92
|
const handlers: Record<string, (data: unknown) => Promise<unknown>> = {};
|
|
46
93
|
for (const eventName of SUPPORTED_EVENTS) {
|
|
47
94
|
handlers[eventName] = async (data) => {
|
|
48
|
-
const rawBody =
|
|
95
|
+
const rawBody = buildLarkEventRawBody(eventName, data);
|
|
49
96
|
try {
|
|
50
97
|
const storeResult = await storeInboundEvent(options.db, { appId: options.appId, rawBody });
|
|
51
98
|
if (storeResult.duplicate) {
|
|
@@ -75,6 +122,24 @@ export function startLarkDaemon(options: StartLarkDaemonOptions): LarkDaemonHand
|
|
|
75
122
|
appId: options.appId,
|
|
76
123
|
appSecret: options.appSecret,
|
|
77
124
|
loggerLevel: options.loggerLevel ?? Lark.LoggerLevel.warn,
|
|
125
|
+
handshakeTimeoutMs: options.handshakeTimeoutMs ?? 15_000,
|
|
126
|
+
wsConfig: { pingTimeout: options.pingTimeoutSec ?? 10 },
|
|
127
|
+
onReady: () => {
|
|
128
|
+
options.onLifecycle?.({ appId: options.appId, type: 'ready' });
|
|
129
|
+
log.info?.(`[lark/${options.appId}] WSClient ready`);
|
|
130
|
+
},
|
|
131
|
+
onReconnecting: () => {
|
|
132
|
+
options.onLifecycle?.({ appId: options.appId, type: 'reconnecting' });
|
|
133
|
+
log.warn?.(`[lark/${options.appId}] WSClient reconnecting`);
|
|
134
|
+
},
|
|
135
|
+
onReconnected: () => {
|
|
136
|
+
options.onLifecycle?.({ appId: options.appId, type: 'reconnected' });
|
|
137
|
+
log.info?.(`[lark/${options.appId}] WSClient reconnected`);
|
|
138
|
+
},
|
|
139
|
+
onError: (err) => {
|
|
140
|
+
options.onLifecycle?.({ appId: options.appId, type: 'error', error: err.message });
|
|
141
|
+
log.error?.(`[lark/${options.appId}] WSClient error: ${err.message}`);
|
|
142
|
+
},
|
|
78
143
|
});
|
|
79
144
|
|
|
80
145
|
wsClient.start({ eventDispatcher });
|
|
@@ -103,35 +168,96 @@ export function createLarkApiClient(appId: string, appSecret: string): Lark.Clie
|
|
|
103
168
|
return new Lark.Client({
|
|
104
169
|
appId,
|
|
105
170
|
appSecret,
|
|
106
|
-
loggerLevel: Lark.LoggerLevel.
|
|
171
|
+
loggerLevel: Lark.LoggerLevel.fatal,
|
|
172
|
+
logger: silentLarkLogger,
|
|
107
173
|
});
|
|
108
174
|
}
|
|
109
175
|
|
|
110
|
-
export
|
|
176
|
+
export type LarkReceiveIdType = 'chat_id' | 'open_id' | 'union_id' | 'email' | 'user_id';
|
|
177
|
+
|
|
178
|
+
export interface LarkMentionTarget {
|
|
179
|
+
openId: string;
|
|
180
|
+
displayName?: string | null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
interface SendMessageBaseInput {
|
|
111
184
|
client: Lark.Client;
|
|
112
|
-
receiveIdType:
|
|
185
|
+
receiveIdType: LarkReceiveIdType;
|
|
113
186
|
receiveId: string;
|
|
114
187
|
text: string;
|
|
188
|
+
mention?: LarkMentionTarget | null;
|
|
115
189
|
}
|
|
116
190
|
|
|
191
|
+
export interface SendTextMessageInput extends SendMessageBaseInput {}
|
|
192
|
+
|
|
193
|
+
export interface SendCardMessageInput extends SendMessageBaseInput {}
|
|
194
|
+
|
|
117
195
|
export interface SendTextMessageResult {
|
|
118
196
|
messageId?: string;
|
|
119
197
|
raw: unknown;
|
|
120
198
|
}
|
|
121
199
|
|
|
122
|
-
export
|
|
200
|
+
export interface LarkMessageCreatePayload {
|
|
201
|
+
receive_id: string;
|
|
202
|
+
msg_type: 'text' | 'interactive';
|
|
203
|
+
content: string;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function escapeLarkText(value: string): string {
|
|
207
|
+
return value.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function larkMentionSuffix(mention: LarkMentionTarget | null | undefined, tag: 'text' | 'card'): string {
|
|
211
|
+
if (!mention?.openId) return '';
|
|
212
|
+
const idAttribute = tag === 'text' ? 'user_id' : 'id';
|
|
213
|
+
return `\n\n<at ${idAttribute}="${mention.openId}">${escapeLarkText(mention.displayName ?? '')}</at>`;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function larkMessageBody(input: Pick<SendMessageBaseInput, 'text' | 'mention'>, tag: 'text' | 'card'): string {
|
|
217
|
+
return `${input.text}${larkMentionSuffix(input.mention, tag)}`;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
export function buildLarkTextMessagePayload(input: Pick<SendTextMessageInput, 'receiveId' | 'text' | 'mention'>): LarkMessageCreatePayload {
|
|
221
|
+
return {
|
|
222
|
+
receive_id: input.receiveId,
|
|
223
|
+
msg_type: 'text',
|
|
224
|
+
content: JSON.stringify({ text: larkMessageBody(input, 'text') }),
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export function buildLarkMentionCardPayload(input: Pick<SendCardMessageInput, 'receiveId' | 'text' | 'mention'>): LarkMessageCreatePayload {
|
|
229
|
+
return {
|
|
230
|
+
receive_id: input.receiveId,
|
|
231
|
+
msg_type: 'interactive',
|
|
232
|
+
content: JSON.stringify({
|
|
233
|
+
config: { wide_screen_mode: true },
|
|
234
|
+
elements: [
|
|
235
|
+
{
|
|
236
|
+
tag: 'markdown',
|
|
237
|
+
content: larkMessageBody(input, 'card'),
|
|
238
|
+
},
|
|
239
|
+
],
|
|
240
|
+
}),
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function createLarkMessage(input: SendMessageBaseInput, payload: LarkMessageCreatePayload): Promise<SendTextMessageResult> {
|
|
123
245
|
const response = await input.client.im.message.create({
|
|
124
246
|
params: { receive_id_type: input.receiveIdType },
|
|
125
|
-
data:
|
|
126
|
-
receive_id: input.receiveId,
|
|
127
|
-
msg_type: 'text',
|
|
128
|
-
content: JSON.stringify({ text: input.text }),
|
|
129
|
-
},
|
|
247
|
+
data: payload,
|
|
130
248
|
});
|
|
131
249
|
const messageId = (response?.data as { message_id?: string } | undefined)?.message_id;
|
|
132
250
|
return { messageId, raw: response };
|
|
133
251
|
}
|
|
134
252
|
|
|
253
|
+
export async function sendTextMessage(input: SendTextMessageInput): Promise<SendTextMessageResult> {
|
|
254
|
+
return createLarkMessage(input, buildLarkTextMessagePayload(input));
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export async function sendCardMessage(input: SendCardMessageInput): Promise<SendTextMessageResult> {
|
|
258
|
+
return createLarkMessage(input, buildLarkMentionCardPayload(input));
|
|
259
|
+
}
|
|
260
|
+
|
|
135
261
|
export interface AddMessageReactionInput {
|
|
136
262
|
client: Lark.Client;
|
|
137
263
|
messageId: string;
|