@controlflow-ai/daemon 0.1.0 → 0.1.1
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 +9 -1
- package/package.json +1 -1
- package/src/app.ts +99 -1
- package/src/console.ts +156 -1
- package/src/db.ts +81 -3
- package/src/lark/event-router.ts +22 -2
- package/src/lark/server-integration.ts +9 -16
- package/src/migrations/022_lark_authorized_users.ts +16 -0
- package/src/migrations.ts +2 -1
- package/src/network.ts +24 -0
- package/src/server.ts +21 -7
- package/src/types.ts +8 -0
- package/src/web.ts +368 -29
package/README.md
CHANGED
|
@@ -267,9 +267,17 @@ The daemon creates one local API for CLI calls, connects the computer to the ser
|
|
|
267
267
|
| `LOCK_DAEMON_URL` | `http://127.0.0.1:4137` | Local daemon API URL used by `bun run cli` |
|
|
268
268
|
| `LOCK_DAEMON_TOKEN` | token file fallback | Local daemon API bearer token |
|
|
269
269
|
| `PAL_LARK_CONFIG` | `$PAL_HOME/lark.json` | Lark bot credential file |
|
|
270
|
-
| `PAL_OWNER_LARK_UNION_ID` | - | Optional Lark sender allowlist for business ingest |
|
|
271
270
|
| `PAL_LARK_ACTION_REACTION_EMOJI` | `Typing` | Reaction added when Lark delivery is created |
|
|
272
271
|
|
|
272
|
+
Lark sender authorization is stored in Pal's database. Manage authorized Lark
|
|
273
|
+
users from the Web Settings Access tab or with:
|
|
274
|
+
|
|
275
|
+
```bash
|
|
276
|
+
bun run console -- lark-users list
|
|
277
|
+
bun run console -- lark-users add --user-id <union-id> --name "Display Name"
|
|
278
|
+
bun run console -- lark-users delete --user-id <union-id>
|
|
279
|
+
```
|
|
280
|
+
|
|
273
281
|
## Runtime Semantics
|
|
274
282
|
|
|
275
283
|
- Rooms are the primary conversation container; `chat` remains as a compatibility alias in several APIs.
|
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -5,7 +5,9 @@ import { assertServerAuth } from './server-auth.js';
|
|
|
5
5
|
import { dashboardHtml } from './web.js';
|
|
6
6
|
import type { RunAction, RunStatus } from './types.js';
|
|
7
7
|
import { createLarkApiClient, sendTextMessage } from './lark/ws-daemon.js';
|
|
8
|
-
import { boundAgents, loadLarkCredentials, type LarkCredentialStore } from './lark/credentials.js';
|
|
8
|
+
import { boundAgents, defaultLarkConfigPath, loadLarkCredentials, type LarkCredentialStore } from './lark/credentials.js';
|
|
9
|
+
import { persistLarkCredential, resolveLarkBotInfo } from './lark/setup.js';
|
|
10
|
+
import { tailscaleAddress } from './network.js';
|
|
9
11
|
|
|
10
12
|
interface SendBody {
|
|
11
13
|
chat?: string;
|
|
@@ -81,6 +83,19 @@ interface OnboardAgentBody {
|
|
|
81
83
|
computer_id?: string;
|
|
82
84
|
}
|
|
83
85
|
|
|
86
|
+
interface LarkSetupBody {
|
|
87
|
+
app_id?: string;
|
|
88
|
+
app_secret?: string;
|
|
89
|
+
label?: string;
|
|
90
|
+
agent?: string;
|
|
91
|
+
config?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface LarkAuthorizedUserBody {
|
|
95
|
+
user_id?: string;
|
|
96
|
+
display_name?: string | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
84
99
|
interface CreateDeliveryBody {
|
|
85
100
|
message_id?: number;
|
|
86
101
|
agent?: string;
|
|
@@ -192,6 +207,17 @@ export function route(store: MessageStore, request: Request): Promise<Response>
|
|
|
192
207
|
return json({ status: 'ok' });
|
|
193
208
|
}
|
|
194
209
|
|
|
210
|
+
if (request.method === 'GET' && pathname === '/api/server/access') {
|
|
211
|
+
const requestUrl = new URL(request.url);
|
|
212
|
+
const port = requestUrl.port || (requestUrl.protocol === 'https:' ? '443' : '80');
|
|
213
|
+
const tailscaleHost = tailscaleAddress();
|
|
214
|
+
return json({
|
|
215
|
+
localUrl: `${requestUrl.protocol}//127.0.0.1:${port}`,
|
|
216
|
+
tailscaleUrl: tailscaleHost ? `${requestUrl.protocol}//${tailscaleHost}:${port}` : null,
|
|
217
|
+
tailscaleHost,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
195
221
|
if (request.method === 'GET' && pathname === '/api/computers') {
|
|
196
222
|
return json({ computers: store.listComputers(numberParam(url, 'limit', 50)) });
|
|
197
223
|
}
|
|
@@ -417,6 +443,78 @@ export function route(store: MessageStore, request: Request): Promise<Response>
|
|
|
417
443
|
return json({ sessions: store.listSessions(numberParam(url, 'limit', 50)) });
|
|
418
444
|
}
|
|
419
445
|
|
|
446
|
+
if (request.method === 'GET' && pathname === '/api/lark/config') {
|
|
447
|
+
const path = stringParam(url, 'config') ?? defaultLarkConfigPath();
|
|
448
|
+
const credentials = loadLarkCredentials(path);
|
|
449
|
+
return json({
|
|
450
|
+
path,
|
|
451
|
+
bots: credentials.bots.map((bot) => ({
|
|
452
|
+
appId: bot.appId,
|
|
453
|
+
label: bot.label ?? null,
|
|
454
|
+
agent: bot.agent ?? null,
|
|
455
|
+
boundAgents: boundAgents(bot),
|
|
456
|
+
botOpenId: bot.botOpenId ?? null,
|
|
457
|
+
hasSecret: Boolean(bot.appSecret),
|
|
458
|
+
})),
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
if (request.method === 'GET' && pathname === '/api/lark/authorized-users') {
|
|
463
|
+
return json({ users: store.listLarkAuthorizedUsers() });
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (request.method === 'POST' && pathname === '/api/lark/authorized-users') {
|
|
467
|
+
return readJson<LarkAuthorizedUserBody>(request).then((body) => {
|
|
468
|
+
const userId = body.user_id?.trim();
|
|
469
|
+
if (!userId) throw new HttpError(400, 'MISSING_USER_ID', 'user_id is required');
|
|
470
|
+
const user = store.upsertLarkAuthorizedUser({ userId, displayName: body.display_name });
|
|
471
|
+
return json({ user }, 201);
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
const larkAuthorizedUserMatch = pathname.match(/^\/api\/lark\/authorized-users\/([^/]+)$/);
|
|
476
|
+
if (request.method === 'DELETE' && larkAuthorizedUserMatch) {
|
|
477
|
+
const userId = decodeURIComponent(larkAuthorizedUserMatch[1]!);
|
|
478
|
+
const deleted = store.deleteLarkAuthorizedUser(userId);
|
|
479
|
+
if (!deleted) throw new HttpError(404, 'LARK_USER_NOT_FOUND', 'authorized Lark user was not found');
|
|
480
|
+
return json({ deleted: true });
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (request.method === 'POST' && pathname === '/api/lark/setup') {
|
|
484
|
+
return readJson<LarkSetupBody>(request).then(async (body) => {
|
|
485
|
+
const appId = body.app_id?.trim();
|
|
486
|
+
const appSecret = body.app_secret?.trim();
|
|
487
|
+
const agent = body.agent?.trim();
|
|
488
|
+
const label = body.label?.trim();
|
|
489
|
+
const configPath = body.config?.trim() || defaultLarkConfigPath();
|
|
490
|
+
if (!appId) throw new HttpError(400, 'MISSING_APP_ID', 'app_id is required');
|
|
491
|
+
if (!appSecret) throw new HttpError(400, 'MISSING_APP_SECRET', 'app_secret is required');
|
|
492
|
+
if (!agent) throw new HttpError(400, 'MISSING_AGENT', 'agent is required');
|
|
493
|
+
if (!store.getAgent(agent)) throw new HttpError(404, 'AGENT_NOT_FOUND', `agent ${agent} not found`);
|
|
494
|
+
|
|
495
|
+
const botInfo = await resolveLarkBotInfo(appId, appSecret);
|
|
496
|
+
if (!botInfo.ok) {
|
|
497
|
+
throw new HttpError(400, 'LARK_CREDENTIALS_REJECTED', `could not resolve bot open_id (${botInfo.error}): ${botInfo.message}`);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const result = persistLarkCredential({
|
|
501
|
+
appId,
|
|
502
|
+
appSecret,
|
|
503
|
+
label,
|
|
504
|
+
agent,
|
|
505
|
+
botOpenId: botInfo.openId,
|
|
506
|
+
configPath,
|
|
507
|
+
});
|
|
508
|
+
const account = store.registerChannelAccount({
|
|
509
|
+
name: label || botInfo.appName || appId,
|
|
510
|
+
appId,
|
|
511
|
+
botOpenId: botInfo.openId,
|
|
512
|
+
agent,
|
|
513
|
+
});
|
|
514
|
+
return json({ ...result, appName: botInfo.appName ?? null, account }, 201);
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
420
518
|
const sessionRunsMatch = pathname.match(/^\/api\/sessions\/([^/]+)\/runs$/);
|
|
421
519
|
if (request.method === 'GET' && sessionRunsMatch) {
|
|
422
520
|
return json({ runs: store.listRunsForSession(sessionRunsMatch[1]!, numberParam(url, 'limit', 50)) });
|
package/src/console.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { boolFlag, parseArgs, flag, numberFlag } from './args.js';
|
|
|
4
4
|
import { LockClient } from './client.js';
|
|
5
5
|
import { defaultServerUrl } from './config.js';
|
|
6
6
|
import { formatMessages } from './format.js';
|
|
7
|
-
import type { Computer } from './types.js';
|
|
7
|
+
import type { Computer, ProvisionedComputer } from './types.js';
|
|
8
8
|
|
|
9
9
|
interface Prompt {
|
|
10
10
|
askLine(label: string): Promise<string>;
|
|
@@ -21,10 +21,15 @@ Usage:
|
|
|
21
21
|
bun run src/console.ts inbox --agent neeko [--after 0] [--limit 50] [--json]
|
|
22
22
|
bun run src/console.ts runs [--json]
|
|
23
23
|
bun run src/console.ts run-action <run-id> kill|restart
|
|
24
|
+
bun run src/console.ts computers list [--json]
|
|
25
|
+
bun run src/console.ts computer onboard [--interactive] [--name <display-name>] [--server-url <url>] [--package-name <npm-package>]
|
|
24
26
|
bun run src/console.ts agents list [--json]
|
|
25
27
|
bun run src/console.ts agents onboard [--interactive] [--key <agent-key>] [--name <display-name>] [--runtime codex] [--computer-id <machine>]
|
|
26
28
|
bun run src/console.ts agents create --key <agent-key> --name <display-name> [--runtime neeko|coco|coco-stream-json|codex] [--desc <description>]
|
|
27
29
|
bun run src/console.ts agents update --key <agent-key> --runtime neeko|coco|coco-stream-json|codex
|
|
30
|
+
bun run src/console.ts lark-users list [--json]
|
|
31
|
+
bun run src/console.ts lark-users add [--interactive] --user-id <union-id> [--name <display-name>]
|
|
32
|
+
bun run src/console.ts lark-users delete --user-id <union-id>
|
|
28
33
|
bun run src/console.ts lark <setup|list|daemon|events|send> [flags]
|
|
29
34
|
|
|
30
35
|
Environment:
|
|
@@ -32,6 +37,13 @@ Environment:
|
|
|
32
37
|
`;
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
async function requestJson<T>(url: string, init?: RequestInit): Promise<T> {
|
|
41
|
+
const response = await fetch(url, init);
|
|
42
|
+
const payload = await response.json() as { data?: T; message?: string; code?: string };
|
|
43
|
+
if (!response.ok) throw new Error(payload.message ?? payload.code ?? `request failed: ${response.status}`);
|
|
44
|
+
return payload.data as T;
|
|
45
|
+
}
|
|
46
|
+
|
|
35
47
|
function printJson(value: unknown): void {
|
|
36
48
|
console.log(JSON.stringify(value, null, 2));
|
|
37
49
|
}
|
|
@@ -120,6 +132,50 @@ async function askComputerId(prompt: Prompt, computers: Computer[]): Promise<str
|
|
|
120
132
|
}
|
|
121
133
|
}
|
|
122
134
|
|
|
135
|
+
async function collectComputerOnboardInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ name: string; serverUrl: string; packageName: string }> {
|
|
136
|
+
const hasAnyProvisionFlag = Boolean(flag(flags, 'name') || flag(flags, 'server-url') || flag(flags, 'package-name'));
|
|
137
|
+
const interactive = boolFlag(flags, 'interactive') || (!hasAnyProvisionFlag && process.stdin.isTTY === true);
|
|
138
|
+
const defaultName = flag(flags, 'name') ?? 'Local computer';
|
|
139
|
+
const defaultServerUrl = flag(flags, 'server-url') ?? serverClient.baseUrl;
|
|
140
|
+
const defaultPackageName = flag(flags, 'package-name') ?? '@slock-ai/daemon@latest';
|
|
141
|
+
|
|
142
|
+
if (!interactive) {
|
|
143
|
+
return {
|
|
144
|
+
name: defaultName,
|
|
145
|
+
serverUrl: defaultServerUrl,
|
|
146
|
+
packageName: defaultPackageName,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const prompt = await createPrompt();
|
|
151
|
+
try {
|
|
152
|
+
console.log('Computer onboarding');
|
|
153
|
+
console.log('Provision a computer credential and daemon start command.');
|
|
154
|
+
const name = await askRequired(prompt, 'Computer display name', defaultName);
|
|
155
|
+
const serverUrl = await askRequired(prompt, 'Server URL', defaultServerUrl);
|
|
156
|
+
const packageName = await askRequired(prompt, 'Daemon package', defaultPackageName);
|
|
157
|
+
console.log('');
|
|
158
|
+
console.log('Summary:');
|
|
159
|
+
console.log(` name: ${name}`);
|
|
160
|
+
console.log(` server_url: ${serverUrl}`);
|
|
161
|
+
console.log(` package_name: ${packageName}`);
|
|
162
|
+
if (!await askYesNo(prompt, 'Provision this computer?', true)) throw new Error('computer onboarding cancelled');
|
|
163
|
+
return { name, serverUrl, packageName };
|
|
164
|
+
} finally {
|
|
165
|
+
prompt.close();
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function printProvisionedComputer(provisioned: ProvisionedComputer): void {
|
|
170
|
+
console.log('Computer onboarded');
|
|
171
|
+
console.log(` id: ${provisioned.computer.id}`);
|
|
172
|
+
console.log(` name: ${provisioned.computer.name}`);
|
|
173
|
+
console.log(` status: ${provisioned.computer.status}`);
|
|
174
|
+
console.log('');
|
|
175
|
+
console.log('Daemon command:');
|
|
176
|
+
console.log(provisioned.command);
|
|
177
|
+
}
|
|
178
|
+
|
|
123
179
|
async function collectOnboardInput(serverClient: LockClient, flags: Record<string, string | boolean>): Promise<{ agentKey: string; displayName: string; runtime: string; desc: string | null; computerId: string | undefined }> {
|
|
124
180
|
const hasRequiredFlags = Boolean(flag(flags, 'key') && flag(flags, 'name'));
|
|
125
181
|
const interactive = boolFlag(flags, 'interactive') || (!hasRequiredFlags && process.stdin.isTTY === true);
|
|
@@ -166,6 +222,31 @@ async function collectOnboardInput(serverClient: LockClient, flags: Record<strin
|
|
|
166
222
|
}
|
|
167
223
|
}
|
|
168
224
|
|
|
225
|
+
async function collectLarkUserInput(flags: Record<string, string | boolean>): Promise<{ userId: string; displayName: string | null }> {
|
|
226
|
+
const userIdFlag = flag(flags, 'user-id') ?? flag(flags, 'id');
|
|
227
|
+
const interactive = boolFlag(flags, 'interactive') || (!userIdFlag && process.stdin.isTTY === true);
|
|
228
|
+
if (!interactive) {
|
|
229
|
+
if (!userIdFlag) throw new Error('lark-users add requires --user-id when not running interactively');
|
|
230
|
+
return { userId: userIdFlag, displayName: flag(flags, 'name') ?? null };
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const prompt = await createPrompt();
|
|
234
|
+
try {
|
|
235
|
+
console.log('Lark authorized user');
|
|
236
|
+
const userId = await askRequired(prompt, 'Lark user ID', userIdFlag ?? '');
|
|
237
|
+
const displayName = await ask(prompt, 'Display name (optional)', flag(flags, 'name') ?? '');
|
|
238
|
+
console.log('');
|
|
239
|
+
console.log('Summary:');
|
|
240
|
+
console.log(` user_id: ${userId}`);
|
|
241
|
+
console.log(` display_name: ${displayName || '-'}`);
|
|
242
|
+
if (!await askYesNo(prompt, 'Authorize this Lark user?', true)) throw new Error('lark user add cancelled');
|
|
243
|
+
return { userId, displayName: displayName || null };
|
|
244
|
+
} finally {
|
|
245
|
+
prompt.close();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
169
250
|
async function main(): Promise<void> {
|
|
170
251
|
const args = parseArgs();
|
|
171
252
|
const serverClient = new LockClient(flag(args.flags, 'server'));
|
|
@@ -247,6 +328,40 @@ async function main(): Promise<void> {
|
|
|
247
328
|
return;
|
|
248
329
|
}
|
|
249
330
|
|
|
331
|
+
if (args.command === 'computer' || args.command === 'computers') {
|
|
332
|
+
const sub = args.values[0];
|
|
333
|
+
|
|
334
|
+
if (sub === 'list') {
|
|
335
|
+
const computers = await serverClient.listComputers();
|
|
336
|
+
if (args.flags.json) {
|
|
337
|
+
printJson(computers);
|
|
338
|
+
} else {
|
|
339
|
+
for (const computer of computers) {
|
|
340
|
+
const seen = computer.last_seen_at ? ` last_seen=${computer.last_seen_at}` : '';
|
|
341
|
+
console.log(`${computer.id} name="${computer.name}" status=${computer.status}${seen}`);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (sub === 'onboard') {
|
|
348
|
+
const input = await collectComputerOnboardInput(serverClient, args.flags);
|
|
349
|
+
const provisioned = await serverClient.provisionComputer({
|
|
350
|
+
name: input.name,
|
|
351
|
+
server_url: input.serverUrl,
|
|
352
|
+
package_name: input.packageName,
|
|
353
|
+
});
|
|
354
|
+
if (args.flags.json) {
|
|
355
|
+
printJson(provisioned);
|
|
356
|
+
} else {
|
|
357
|
+
printProvisionedComputer(provisioned);
|
|
358
|
+
}
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
throw new Error(`unknown ${args.command} subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
|
|
363
|
+
}
|
|
364
|
+
|
|
250
365
|
if (args.command === 'agents') {
|
|
251
366
|
const sub = args.values[0];
|
|
252
367
|
|
|
@@ -331,6 +446,46 @@ async function main(): Promise<void> {
|
|
|
331
446
|
throw new Error(`unknown agents subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
|
|
332
447
|
}
|
|
333
448
|
|
|
449
|
+
if (args.command === 'lark-users') {
|
|
450
|
+
const sub = args.values[0];
|
|
451
|
+
|
|
452
|
+
if (sub === 'list') {
|
|
453
|
+
const data = await requestJson<{ users: Array<Record<string, unknown>> }>(`${serverClient.baseUrl}/api/lark/authorized-users`);
|
|
454
|
+
if (args.flags.json) {
|
|
455
|
+
printJson(data.users);
|
|
456
|
+
} else if (data.users.length === 0) {
|
|
457
|
+
console.log('No authorized Lark users.');
|
|
458
|
+
} else {
|
|
459
|
+
for (const user of data.users) {
|
|
460
|
+
console.log(`${user.user_id} name="${user.display_name ?? '-'}" added=${user.created_at}`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (sub === 'add') {
|
|
467
|
+
const input = await collectLarkUserInput(args.flags);
|
|
468
|
+
const data = await requestJson<{ user: Record<string, unknown> }>(`${serverClient.baseUrl}/api/lark/authorized-users`, {
|
|
469
|
+
method: 'POST',
|
|
470
|
+
headers: { 'content-type': 'application/json' },
|
|
471
|
+
body: JSON.stringify({ user_id: input.userId, display_name: input.displayName }),
|
|
472
|
+
});
|
|
473
|
+
if (args.flags.json) printJson(data.user);
|
|
474
|
+
else console.log(`Authorized Lark user ${data.user.user_id}`);
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
if (sub === 'delete' || sub === 'remove') {
|
|
479
|
+
const userId = flag(args.flags, 'user-id') ?? flag(args.flags, 'id') ?? args.values[1];
|
|
480
|
+
if (!userId) throw new Error('--user-id is required');
|
|
481
|
+
await requestJson<{ deleted: boolean }>(`${serverClient.baseUrl}/api/lark/authorized-users/${encodeURIComponent(userId)}`, { method: 'DELETE' });
|
|
482
|
+
console.log(`Deleted Lark user ${userId}`);
|
|
483
|
+
return;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
throw new Error(`unknown lark-users subcommand: ${sub ?? '(none)'}\n\n${usage()}`);
|
|
487
|
+
}
|
|
488
|
+
|
|
334
489
|
if (args.command === 'lark') {
|
|
335
490
|
const { runLarkCli } = await import('./lark/cli.js');
|
|
336
491
|
const code = await runLarkCli({
|
package/src/db.ts
CHANGED
|
@@ -4,7 +4,9 @@ import { ensureParentDir } from './config.js';
|
|
|
4
4
|
import { runMigrations } from './migrations.js';
|
|
5
5
|
import { artifactExpiry, generateArtifactToken, hashArtifactToken, validateArtifactContent } from './artifacts.js';
|
|
6
6
|
import { palIdentityHandle } from './provider-identity.js';
|
|
7
|
-
import type { AgentDefinition, AgentRun, AgentSession, AgentValidationResult, Artifact, ArtifactMetadata, ChannelAccount, ChannelConversation, ChannelMessageMapping, ChannelOutboxRecord, Chat, ChatKind, Computer, ComputerAgentAssignment, ComputerConnection, DaemonInstance, LarkGroupRoomMapping, LockProviderEvidence, LockTranscriptMessage, Message, MessageDelivery, MessageType, AgentRoomSubscription, AgentRoomSubscriptionMode, PendingInboundEvent, ProvisionedComputer, ProviderExternalType, ProviderIdentityBinding, PalIdentity, RoomChannel, RoomParticipant, RoomParticipantKind, RoomParticipantSource, RoomProvider, RunAction, RunStatus, TranscriptReadModel, WorkbenchArtifact } from './types.js';
|
|
7
|
+
import type { AgentDefinition, AgentRun, AgentSession, AgentValidationResult, Artifact, ArtifactMetadata, ChannelAccount, ChannelConversation, ChannelMessageMapping, ChannelOutboxRecord, Chat, ChatKind, Computer, ComputerAgentAssignment, ComputerConnection, DaemonInstance, LarkAuthorizedUser, LarkGroupRoomMapping, LockProviderEvidence, LockTranscriptMessage, Message, MessageDelivery, MessageType, AgentRoomSubscription, AgentRoomSubscriptionMode, PendingInboundEvent, ProvisionedComputer, ProviderExternalType, ProviderIdentityBinding, PalIdentity, RoomChannel, RoomParticipant, RoomParticipantKind, RoomParticipantSource, RoomProvider, RunAction, RunStatus, TranscriptReadModel, WorkbenchArtifact } from './types.js';
|
|
8
|
+
|
|
9
|
+
export const ALL_AGENTS_MENTION = '__pal_all_agents__';
|
|
8
10
|
|
|
9
11
|
export interface CreateMessageInput {
|
|
10
12
|
chatId?: string;
|
|
@@ -529,6 +531,16 @@ function rowToChannelAccount(row: Record<string, unknown>): ChannelAccount {
|
|
|
529
531
|
};
|
|
530
532
|
}
|
|
531
533
|
|
|
534
|
+
function rowToLarkAuthorizedUser(row: Record<string, unknown>): LarkAuthorizedUser {
|
|
535
|
+
return {
|
|
536
|
+
id: String(row.id),
|
|
537
|
+
user_id: String(row.user_id),
|
|
538
|
+
display_name: row.display_name === null || row.display_name === undefined ? null : String(row.display_name),
|
|
539
|
+
created_at: String(row.created_at),
|
|
540
|
+
updated_at: String(row.updated_at),
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
|
|
532
544
|
function rowToPalIdentity(row: Record<string, unknown>): PalIdentity {
|
|
533
545
|
return {
|
|
534
546
|
id: String(row.id),
|
|
@@ -852,6 +864,43 @@ export class MessageStore {
|
|
|
852
864
|
return this.db.query('SELECT * FROM chat_stats ORDER BY COALESCE(last_message_at, created_at) DESC').all() as Chat[];
|
|
853
865
|
}
|
|
854
866
|
|
|
867
|
+
listLarkAuthorizedUsers(): LarkAuthorizedUser[] {
|
|
868
|
+
const rows = this.db.query('SELECT * FROM lark_authorized_users ORDER BY COALESCE(display_name, user_id), user_id').all() as Record<string, unknown>[];
|
|
869
|
+
return rows.map(rowToLarkAuthorizedUser);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
upsertLarkAuthorizedUser(input: { userId: string; displayName?: string | null }): LarkAuthorizedUser {
|
|
873
|
+
const userId = input.userId.trim();
|
|
874
|
+
if (!userId) throw new Error('lark user_id is required');
|
|
875
|
+
const id = crypto.randomUUID();
|
|
876
|
+
this.db.query(`
|
|
877
|
+
INSERT INTO lark_authorized_users (id, user_id, display_name, created_at, updated_at)
|
|
878
|
+
VALUES (?, ?, ?, datetime('now'), datetime('now'))
|
|
879
|
+
ON CONFLICT(user_id) DO UPDATE SET
|
|
880
|
+
display_name = excluded.display_name,
|
|
881
|
+
updated_at = datetime('now')
|
|
882
|
+
`).run(id, userId, input.displayName?.trim() || null);
|
|
883
|
+
return this.getLarkAuthorizedUser(userId)!;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
getLarkAuthorizedUser(userId: string): LarkAuthorizedUser | null {
|
|
887
|
+
const normalized = userId.trim();
|
|
888
|
+
if (!normalized) return null;
|
|
889
|
+
const row = this.db.query('SELECT * FROM lark_authorized_users WHERE user_id = ?').get(normalized) as Record<string, unknown> | null;
|
|
890
|
+
return row ? rowToLarkAuthorizedUser(row) : null;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
isLarkAuthorizedUser(userId: string | null | undefined): boolean {
|
|
894
|
+
if (!userId?.trim()) return false;
|
|
895
|
+
return this.getLarkAuthorizedUser(userId) !== null;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
deleteLarkAuthorizedUser(userId: string): boolean {
|
|
899
|
+
const normalized = userId.trim();
|
|
900
|
+
if (!normalized) return false;
|
|
901
|
+
return this.db.query('DELETE FROM lark_authorized_users WHERE user_id = ?').run(normalized).changes > 0;
|
|
902
|
+
}
|
|
903
|
+
|
|
855
904
|
listLarkGroupRoomMappings(): LarkGroupRoomMapping[] {
|
|
856
905
|
const rows = this.db.query(`
|
|
857
906
|
SELECT DISTINCT
|
|
@@ -2146,7 +2195,7 @@ export class MessageStore {
|
|
|
2146
2195
|
VALUES (?, ?, ?, 'offline', NULL, NULL, datetime('now'))
|
|
2147
2196
|
`).run(id, name, hashSecret(apiKey));
|
|
2148
2197
|
|
|
2149
|
-
const packageName = input.packageName?.trim() || '@
|
|
2198
|
+
const packageName = input.packageName?.trim() || '@controlflow-ai/daemon@latest';
|
|
2150
2199
|
const serverUrl = input.serverUrl?.trim() || 'http://127.0.0.1:4127';
|
|
2151
2200
|
const command = `npx ${packageName} --server-url ${serverUrl} --api-key ${apiKey} # ${name}`;
|
|
2152
2201
|
return { computer: this.getComputer(id)!, api_key: apiKey, command };
|
|
@@ -2431,7 +2480,11 @@ export class MessageStore {
|
|
|
2431
2480
|
if (!room) throw new Error(`room ${message.chat_id} was not found`);
|
|
2432
2481
|
const candidates = new Set<string>();
|
|
2433
2482
|
if (message.recipient && this.getAgent(message.recipient)) candidates.add(message.recipient);
|
|
2483
|
+
if ((message.mentions ?? []).includes(ALL_AGENTS_MENTION)) {
|
|
2484
|
+
for (const agent of this.listRoomAgents(room)) candidates.add(agent);
|
|
2485
|
+
}
|
|
2434
2486
|
for (const mention of message.mentions ?? []) {
|
|
2487
|
+
if (mention === ALL_AGENTS_MENTION) continue;
|
|
2435
2488
|
if (this.getAgent(mention)) candidates.add(mention);
|
|
2436
2489
|
}
|
|
2437
2490
|
if (room.kind === 'dm') {
|
|
@@ -2460,9 +2513,34 @@ export class MessageStore {
|
|
|
2460
2513
|
return deliveries;
|
|
2461
2514
|
}
|
|
2462
2515
|
|
|
2516
|
+
private listRoomAgents(room: Chat): string[] {
|
|
2517
|
+
if (room.provider === 'web') {
|
|
2518
|
+
const rows = this.db.query(`
|
|
2519
|
+
SELECT participant_id AS agent
|
|
2520
|
+
FROM room_participants
|
|
2521
|
+
WHERE room_id = ? AND kind = 'agent' AND status = 'active'
|
|
2522
|
+
`).all(room.id) as Array<{ agent: string }>;
|
|
2523
|
+
return rows.map((row) => row.agent).filter((agent) => Boolean(this.getAgent(agent)));
|
|
2524
|
+
}
|
|
2525
|
+
|
|
2526
|
+
const rows = this.db.query(`
|
|
2527
|
+
SELECT DISTINCT pa.agent AS agent
|
|
2528
|
+
FROM provider_accounts pa
|
|
2529
|
+
INNER JOIN provider_conversations pc ON pc.provider_account_id = pa.id
|
|
2530
|
+
WHERE pa.provider = ? AND pa.status = 'active' AND pc.room_id = ?
|
|
2531
|
+
UNION
|
|
2532
|
+
SELECT DISTINCT ca.agent AS agent
|
|
2533
|
+
FROM channel_accounts ca
|
|
2534
|
+
INNER JOIN channel_conversations cc ON cc.channel_account_id = ca.id
|
|
2535
|
+
WHERE ca.channel = ? AND ca.status = 'active' AND cc.lock_chat_id = ? AND ca.agent IS NOT NULL AND ca.agent != ''
|
|
2536
|
+
`).all(room.provider, room.id, room.provider, room.id) as Array<{ agent: string }>;
|
|
2537
|
+
return rows.map((row) => row.agent).filter((agent) => Boolean(this.getAgent(agent)));
|
|
2538
|
+
}
|
|
2539
|
+
|
|
2463
2540
|
private shouldCreateDeliveryForAgent(message: Message, room: Chat, agent: string): boolean {
|
|
2464
2541
|
if (!this.canAgentParticipateInRoom(agent, room)) return false;
|
|
2465
|
-
const
|
|
2542
|
+
const allAgentsMentioned = (message.mentions ?? []).includes(ALL_AGENTS_MENTION);
|
|
2543
|
+
const direct = allAgentsMentioned || message.recipient === agent || (message.mentions ?? []).includes(agent);
|
|
2466
2544
|
if (room.kind === 'dm') return direct || message.recipient === null;
|
|
2467
2545
|
const subscription = this.getAgentRoomSubscription(agent, room.id);
|
|
2468
2546
|
const mode = subscription?.mode ?? 'mentions';
|
package/src/lark/event-router.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { MessageStore, CreateMessageInput } from '../db.js';
|
|
2
|
+
import { ALL_AGENTS_MENTION } from '../db.js';
|
|
2
3
|
import type { Message } from '../types.js';
|
|
3
4
|
import { palIdentityHandle } from '../provider-identity.js';
|
|
4
5
|
|
|
@@ -82,6 +83,22 @@ export function extractMentionOpenIds(envelope: LarkMessageEnvelope): string[] {
|
|
|
82
83
|
return ids;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
function isAllMention(mention: NonNullable<NonNullable<LarkMessageEnvelope['message']>['mentions']>[number]): boolean {
|
|
87
|
+
const key = mention.key?.trim().toLowerCase();
|
|
88
|
+
const name = mention.name?.trim().toLowerCase();
|
|
89
|
+
const openId = mention.id?.open_id?.trim().toLowerCase();
|
|
90
|
+
return key === '@all' || key === 'all' || name === 'all' || name === '所有人' || openId === 'all';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function mentionsAllAgents(envelope: LarkMessageEnvelope): boolean {
|
|
94
|
+
const mentions = envelope.message?.mentions ?? [];
|
|
95
|
+
for (const mention of mentions) {
|
|
96
|
+
if (isAllMention(mention)) return true;
|
|
97
|
+
}
|
|
98
|
+
const raw = parseLarkTextContent(envelope.message?.content, envelope.message?.message_type).toLowerCase();
|
|
99
|
+
return /(^|\s)@(all|所有人)(\s|$)/u.test(raw);
|
|
100
|
+
}
|
|
101
|
+
|
|
85
102
|
function normalizeMappedMentions(values: Array<string | null>): string[] {
|
|
86
103
|
const out: string[] = [];
|
|
87
104
|
const seen = new Set<string>();
|
|
@@ -124,8 +141,11 @@ export function mapLarkMessageToCreateInput(input: MapLarkMessageInput): MapLark
|
|
|
124
141
|
const text = parseLarkTextContent(msg.content, msg.message_type);
|
|
125
142
|
if (!text.trim()) return { status: 'skipped', reason: 'empty_text' };
|
|
126
143
|
|
|
127
|
-
const firstMention = msg.mentions?.find((m) => m.id?.open_id)?.id?.open_id;
|
|
128
|
-
const mappedMentions = normalizeMappedMentions(
|
|
144
|
+
const firstMention = msg.mentions?.find((m) => m.id?.open_id && !isAllMention(m))?.id?.open_id;
|
|
145
|
+
const mappedMentions = normalizeMappedMentions([
|
|
146
|
+
...(mentionsAllAgents(envelope) ? [ALL_AGENTS_MENTION] : []),
|
|
147
|
+
...(msg.mentions?.map((m) => m.id?.open_id ? input.recipientByMentionOpenId?.get(m.id.open_id) ?? null : null) ?? []),
|
|
148
|
+
]);
|
|
129
149
|
const recipient = input.recipientOverride !== undefined
|
|
130
150
|
? input.recipientOverride
|
|
131
151
|
: firstMention ? input.recipientByMentionOpenId?.get(firstMention) ?? firstMention : null;
|
|
@@ -52,10 +52,6 @@ function mentionsBotLabel(envelope: LarkMessageEnvelope, bot: LarkCredential): b
|
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
|
|
55
|
-
function configuredOwnerUnionId(): string | null {
|
|
56
|
-
return process.env.PAL_OWNER_LARK_UNION_ID?.trim() || process.env.PAL_LARK_OWNER_UNION_ID?.trim() || null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
55
|
function senderUnionId(envelope: LarkMessageEnvelope): string | null {
|
|
60
56
|
return envelope.sender?.sender_id?.union_id?.trim() || null;
|
|
61
57
|
}
|
|
@@ -261,18 +257,15 @@ export function startLarkOnServer(options: LarkServerIntegrationOptions): LarkSe
|
|
|
261
257
|
if (envelope === 'im.message.receive_v1') {
|
|
262
258
|
try {
|
|
263
259
|
const larkEnvelope = data as LarkMessageEnvelope;
|
|
264
|
-
const
|
|
265
|
-
if (
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
log.log(`[lark/${bot.appId}] business ingest skipped sender_union=${identity.status === 'resolved' ? identity.unionId : '-'} owner_configured=true`);
|
|
274
|
-
return;
|
|
275
|
-
}
|
|
260
|
+
const identity = await resolveSenderUnionId({ bot, envelope: larkEnvelope, store: msgStore });
|
|
261
|
+
if (identity.status === 'pending') {
|
|
262
|
+
msgStore.recordPendingInboundEvent({ rawEventId: storeResult.event_id, provider: 'lark', reason: 'missing_lark_sender_user_id', error: identity.reason });
|
|
263
|
+
log.warn(`[lark/${bot.appId}] sender union_id lookup pending: ${identity.reason}`);
|
|
264
|
+
return;
|
|
265
|
+
}
|
|
266
|
+
if (identity.status !== 'resolved' || !msgStore.isLarkAuthorizedUser(identity.unionId)) {
|
|
267
|
+
log.log(`[lark/${bot.appId}] business ingest skipped sender_union=${identity.status === 'resolved' ? identity.unionId : '-'} authorized=false`);
|
|
268
|
+
return;
|
|
276
269
|
}
|
|
277
270
|
const agents = boundAgents(bot);
|
|
278
271
|
const mentionOpenIds = extractMentionOpenIds(larkEnvelope);
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Database } from 'bun:sqlite';
|
|
2
|
+
|
|
3
|
+
export const version = 22;
|
|
4
|
+
export const name = 'lark_authorized_users';
|
|
5
|
+
|
|
6
|
+
export function up(db: Database): void {
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE IF NOT EXISTS lark_authorized_users (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
user_id TEXT NOT NULL UNIQUE,
|
|
11
|
+
display_name TEXT,
|
|
12
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
13
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
14
|
+
);
|
|
15
|
+
`);
|
|
16
|
+
}
|
package/src/migrations.ts
CHANGED
|
@@ -20,6 +20,7 @@ import * as roomDisplayNames from './migrations/018_room_display_names.js';
|
|
|
20
20
|
import * as computerConnections from './migrations/019_computer_connections.js';
|
|
21
21
|
import * as computerAgentAssignments from './migrations/020_computer_agent_assignments.js';
|
|
22
22
|
import * as providerIdentityBindings from './migrations/021_provider_identity_bindings.js';
|
|
23
|
+
import * as larkAuthorizedUsers from './migrations/022_lark_authorized_users.js';
|
|
23
24
|
|
|
24
25
|
interface Migration {
|
|
25
26
|
version: number;
|
|
@@ -27,7 +28,7 @@ interface Migration {
|
|
|
27
28
|
up(db: Database): void;
|
|
28
29
|
}
|
|
29
30
|
|
|
30
|
-
const migrations: Migration[] = [initial, daemonDeliveries, sessionsRuns, messageIdempotency, artifacts, larkChannelFoundation, agentsA0, b0ChatHistory, b0TranscriptIngestSeq, b0TranscriptShadowExternalIds, b0ChannelConversationAuditOnly, b0CrossConversationInvariant, b10EngInboundRawEvents, agentsRuntime, agentRuntimeSessions, roomParticipants, unifiedRoomDelivery, roomDisplayNames, computerConnections, computerAgentAssignments, providerIdentityBindings].sort((a, b) => a.version - b.version);
|
|
31
|
+
const migrations: Migration[] = [initial, daemonDeliveries, sessionsRuns, messageIdempotency, artifacts, larkChannelFoundation, agentsA0, b0ChatHistory, b0TranscriptIngestSeq, b0TranscriptShadowExternalIds, b0ChannelConversationAuditOnly, b0CrossConversationInvariant, b10EngInboundRawEvents, agentsRuntime, agentRuntimeSessions, roomParticipants, unifiedRoomDelivery, roomDisplayNames, computerConnections, computerAgentAssignments, providerIdentityBindings, larkAuthorizedUsers].sort((a, b) => a.version - b.version);
|
|
31
32
|
|
|
32
33
|
function assertContiguousMigrations(): void {
|
|
33
34
|
for (let index = 0; index < migrations.length; index += 1) {
|
package/src/network.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { networkInterfaces } from 'node:os';
|
|
2
|
+
|
|
3
|
+
export function tailscaleAddress(): string | null {
|
|
4
|
+
const interfaces = networkInterfaces();
|
|
5
|
+
for (const [name, entries] of Object.entries(interfaces)) {
|
|
6
|
+
for (const entry of entries ?? []) {
|
|
7
|
+
if (entry.family !== 'IPv4' || entry.internal) continue;
|
|
8
|
+
if (name.toLowerCase().startsWith('tailscale') || isTailscaleIPv4(entry.address)) {
|
|
9
|
+
return entry.address;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isTailscaleIPv4(address: string): boolean {
|
|
17
|
+
const parts = address.split('.').map((part) => Number(part));
|
|
18
|
+
if (parts.length !== 4 || parts.some((part) => !Number.isInteger(part) || part < 0 || part > 255)) return false;
|
|
19
|
+
return parts[0] === 100 && parts[1]! >= 64 && parts[1]! <= 127;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isLoopbackHost(host: string): boolean {
|
|
23
|
+
return host === '127.0.0.1' || host === 'localhost' || host === '::1';
|
|
24
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -3,25 +3,37 @@ import { DEFAULT_HOST, DEFAULT_PORT, defaultDbPath } from './config.js';
|
|
|
3
3
|
import { MessageStore } from './db.js';
|
|
4
4
|
import { handleRequest } from './app.js';
|
|
5
5
|
import { startLarkOnServer } from './lark/server-integration.js';
|
|
6
|
+
import { isLoopbackHost, tailscaleAddress } from './network.js';
|
|
6
7
|
|
|
7
8
|
const dbPath = defaultDbPath();
|
|
8
9
|
const store = new MessageStore(dbPath);
|
|
9
10
|
const port = Number(process.env.PAL_PORT ?? DEFAULT_PORT);
|
|
10
11
|
const host = process.env.PAL_HOST ?? DEFAULT_HOST;
|
|
12
|
+
const tailscaleHost = process.env.PAL_TAILSCALE_HOST ?? tailscaleAddress();
|
|
13
|
+
|
|
14
|
+
function fetch(request: Request): Promise<Response> | Response {
|
|
15
|
+
const url = new URL(request.url);
|
|
16
|
+
if (request.method === 'POST' && url.pathname === '/api/lark/reload') {
|
|
17
|
+
return reloadLarkIntegration();
|
|
18
|
+
}
|
|
19
|
+
return handleRequest(store, request);
|
|
20
|
+
}
|
|
11
21
|
|
|
12
22
|
const server = Bun.serve({
|
|
13
23
|
hostname: host,
|
|
14
24
|
port,
|
|
15
|
-
|
|
16
|
-
const url = new URL(request.url);
|
|
17
|
-
if (request.method === 'POST' && url.pathname === '/api/lark/reload') {
|
|
18
|
-
return reloadLarkIntegration();
|
|
19
|
-
}
|
|
20
|
-
return handleRequest(store, request);
|
|
21
|
-
},
|
|
25
|
+
fetch,
|
|
22
26
|
});
|
|
27
|
+
const tailscaleServer = tailscaleHost && isLoopbackHost(host)
|
|
28
|
+
? Bun.serve({ hostname: tailscaleHost, port, fetch })
|
|
29
|
+
: null;
|
|
23
30
|
|
|
24
31
|
console.log(`pal server listening on http://${server.hostname}:${server.port}`);
|
|
32
|
+
if (tailscaleServer) {
|
|
33
|
+
console.log(`pal server also listening on Tailscale http://${tailscaleServer.hostname}:${tailscaleServer.port}`);
|
|
34
|
+
} else if (tailscaleHost) {
|
|
35
|
+
console.log(`pal server Tailscale address: http://${tailscaleHost}:${server.port}`);
|
|
36
|
+
}
|
|
25
37
|
console.log(`database: ${dbPath}`);
|
|
26
38
|
|
|
27
39
|
function startLarkIntegration() {
|
|
@@ -50,11 +62,13 @@ if (larkIntegration.handles.length > 0) {
|
|
|
50
62
|
|
|
51
63
|
process.on('SIGINT', () => {
|
|
52
64
|
larkIntegration.stop();
|
|
65
|
+
tailscaleServer?.stop();
|
|
53
66
|
server.stop();
|
|
54
67
|
process.exit(0);
|
|
55
68
|
});
|
|
56
69
|
process.on('SIGTERM', () => {
|
|
57
70
|
larkIntegration.stop();
|
|
71
|
+
tailscaleServer?.stop();
|
|
58
72
|
server.stop();
|
|
59
73
|
process.exit(0);
|
|
60
74
|
});
|
package/src/types.ts
CHANGED
|
@@ -228,6 +228,14 @@ export interface ChannelAccount {
|
|
|
228
228
|
updated_at: string;
|
|
229
229
|
}
|
|
230
230
|
|
|
231
|
+
export interface LarkAuthorizedUser {
|
|
232
|
+
id: string;
|
|
233
|
+
user_id: string;
|
|
234
|
+
display_name: string | null;
|
|
235
|
+
created_at: string;
|
|
236
|
+
updated_at: string;
|
|
237
|
+
}
|
|
238
|
+
|
|
231
239
|
export interface PalIdentity {
|
|
232
240
|
id: string;
|
|
233
241
|
kind: PalIdentityKind;
|
package/src/web.ts
CHANGED
|
@@ -63,8 +63,9 @@ export function dashboardHtml(): string {
|
|
|
63
63
|
grid-template-columns: 300px minmax(360px, 1fr) 330px;
|
|
64
64
|
gap: 14px;
|
|
65
65
|
width: min(1540px, calc(100vw - 28px));
|
|
66
|
-
|
|
66
|
+
height: calc(100vh - 28px);
|
|
67
67
|
margin: 14px auto;
|
|
68
|
+
overflow: hidden;
|
|
68
69
|
}
|
|
69
70
|
.panel {
|
|
70
71
|
min-width: 0;
|
|
@@ -115,7 +116,7 @@ export function dashboardHtml(): string {
|
|
|
115
116
|
}
|
|
116
117
|
.pill.good { color: var(--active); border-color: #a7d7c4; background: var(--active-soft); }
|
|
117
118
|
.pill.blue { color: var(--blue); border-color: #b7c8f6; background: var(--blue-soft); }
|
|
118
|
-
.chat { display: grid; grid-template-rows: auto 1fr auto; overflow: hidden; }
|
|
119
|
+
.chat { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; overflow: hidden; min-height: 0; }
|
|
119
120
|
.panel-head { display: flex; align-items: center; justify-content: space-between; gap: 14px; min-height: 74px; min-width: 0; background: var(--panel); }
|
|
120
121
|
.panel-head > div { min-width: 0; }
|
|
121
122
|
.panel-head h2 { margin: 0; font-size: 22px; letter-spacing: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
@@ -138,6 +139,94 @@ export function dashboardHtml(): string {
|
|
|
138
139
|
.split { display: grid; grid-template-columns: minmax(0, 1fr) 120px; gap: 8px; }
|
|
139
140
|
.toolbox { display: grid; gap: 8px; background: var(--panel-2); }
|
|
140
141
|
.section-title { margin: 0; color: var(--muted); font-size: 12px; font-weight: 900; letter-spacing: .12em; text-transform: uppercase; }
|
|
142
|
+
.field { display: grid; gap: 5px; }
|
|
143
|
+
.field span { color: var(--muted); font-size: 11px; font-weight: 900; text-transform: uppercase; letter-spacing: .08em; }
|
|
144
|
+
.settings-trigger { white-space: nowrap; }
|
|
145
|
+
.settings-backdrop {
|
|
146
|
+
position: fixed;
|
|
147
|
+
inset: 0;
|
|
148
|
+
z-index: 40;
|
|
149
|
+
display: none;
|
|
150
|
+
padding: 18px;
|
|
151
|
+
background: rgba(24, 23, 20, .28);
|
|
152
|
+
}
|
|
153
|
+
.settings-backdrop.open { display: grid; place-items: center; }
|
|
154
|
+
.settings-dialog {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-rows: auto 1fr;
|
|
157
|
+
width: min(1120px, 100%);
|
|
158
|
+
max-height: min(860px, calc(100vh - 36px));
|
|
159
|
+
border: 1px solid var(--ink);
|
|
160
|
+
background: var(--panel);
|
|
161
|
+
box-shadow: var(--shadow);
|
|
162
|
+
overflow: hidden;
|
|
163
|
+
}
|
|
164
|
+
.settings-head {
|
|
165
|
+
display: flex;
|
|
166
|
+
justify-content: space-between;
|
|
167
|
+
gap: 14px;
|
|
168
|
+
padding: 16px;
|
|
169
|
+
border-bottom: 1px solid var(--line);
|
|
170
|
+
background: #fffdf8;
|
|
171
|
+
}
|
|
172
|
+
.settings-head h2 { margin: 0; font-size: 24px; letter-spacing: 0; }
|
|
173
|
+
.settings-body {
|
|
174
|
+
display: grid;
|
|
175
|
+
grid-template-columns: 210px minmax(0, 1fr);
|
|
176
|
+
min-height: 0;
|
|
177
|
+
overflow: hidden;
|
|
178
|
+
}
|
|
179
|
+
.settings-nav {
|
|
180
|
+
display: grid;
|
|
181
|
+
align-content: start;
|
|
182
|
+
gap: 8px;
|
|
183
|
+
padding: 14px;
|
|
184
|
+
border-right: 1px solid var(--line);
|
|
185
|
+
background: var(--panel-2);
|
|
186
|
+
overflow: auto;
|
|
187
|
+
}
|
|
188
|
+
.settings-tab {
|
|
189
|
+
width: 100%;
|
|
190
|
+
background: transparent;
|
|
191
|
+
color: var(--ink);
|
|
192
|
+
border-color: var(--line);
|
|
193
|
+
text-align: left;
|
|
194
|
+
}
|
|
195
|
+
.settings-tab.active { background: var(--ink); color: #fffaf1; border-color: var(--ink); }
|
|
196
|
+
.settings-content { overflow: auto; padding: 16px; }
|
|
197
|
+
.settings-pane { display: none; gap: 12px; }
|
|
198
|
+
.settings-pane.active { display: grid; }
|
|
199
|
+
.settings-section-head {
|
|
200
|
+
display: flex;
|
|
201
|
+
align-items: start;
|
|
202
|
+
justify-content: space-between;
|
|
203
|
+
gap: 12px;
|
|
204
|
+
padding-bottom: 10px;
|
|
205
|
+
border-bottom: 1px solid var(--line);
|
|
206
|
+
}
|
|
207
|
+
.settings-section-head h3 { margin: 0; font-size: 18px; letter-spacing: 0; }
|
|
208
|
+
.settings-section-head .meta { max-width: 620px; }
|
|
209
|
+
.setup-grid { display: grid; grid-template-columns: minmax(0, 1fr); gap: 12px; }
|
|
210
|
+
.setup-panel { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--line); background: #fffdf8; }
|
|
211
|
+
.setup-panel.collapsed { display: none; }
|
|
212
|
+
.setup-panel .section-title { color: var(--ink); }
|
|
213
|
+
.setup-actions { display: grid; grid-template-columns: 1fr auto; gap: 8px; }
|
|
214
|
+
.summary-panel { display: grid; gap: 8px; padding: 12px; border: 1px solid var(--line); background: var(--active-soft); }
|
|
215
|
+
.settings-list { display: grid; gap: 8px; }
|
|
216
|
+
.settings-row {
|
|
217
|
+
display: grid;
|
|
218
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
219
|
+
gap: 10px;
|
|
220
|
+
align-items: start;
|
|
221
|
+
padding: 11px;
|
|
222
|
+
border: 1px solid var(--line);
|
|
223
|
+
background: #fffdf8;
|
|
224
|
+
}
|
|
225
|
+
.settings-row strong { display: block; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
226
|
+
.settings-row .meta { overflow-wrap: anywhere; word-break: break-word; }
|
|
227
|
+
.command-wrap { display: grid; gap: 7px; }
|
|
228
|
+
.command-wrap textarea { font-family: var(--mono); font-size: 11px; min-height: 104px; }
|
|
229
|
+
.secret-note { color: var(--faint); font-size: 11px; line-height: 1.4; }
|
|
141
230
|
.empty, .error, .readonly-note { border: 1px dashed var(--line); padding: 18px; text-align: center; background: rgba(255, 253, 248, .65); }
|
|
142
231
|
.error { display: none; color: var(--danger); border-color: rgba(163, 58, 43, .35); background: #fff1ee; }
|
|
143
232
|
.readonly-note { color: var(--muted); line-height: 1.5; }
|
|
@@ -188,12 +277,14 @@ export function dashboardHtml(): string {
|
|
|
188
277
|
.app { grid-template-columns: 280px minmax(0, 1fr); }
|
|
189
278
|
.inspector { grid-column: 1 / -1; grid-template-rows: auto auto; }
|
|
190
279
|
.inspector .agents { max-height: 320px; }
|
|
280
|
+
.settings-body, .setup-grid, .settings-row { grid-template-columns: 1fr; }
|
|
281
|
+
.settings-nav { grid-template-columns: repeat(2, minmax(0, 1fr)); border-right: 0; border-bottom: 1px solid var(--line); }
|
|
191
282
|
}
|
|
192
283
|
@media (max-width: 760px) {
|
|
193
|
-
.app { width: 100%;
|
|
284
|
+
.app { width: 100%; height: 100vh; margin: 0; grid-template-columns: 1fr; overflow: auto; }
|
|
194
285
|
.sidebar, .inspector { max-height: none; }
|
|
195
|
-
.chat { min-height:
|
|
196
|
-
.composer-controls, .split, .form-row { grid-template-columns: 1fr; }
|
|
286
|
+
.chat { height: 100vh; min-height: 0; }
|
|
287
|
+
.composer-controls, .split, .form-row, .setup-actions { grid-template-columns: 1fr; }
|
|
197
288
|
.message { max-width: 100%; }
|
|
198
289
|
}
|
|
199
290
|
</style>
|
|
@@ -246,6 +337,7 @@ export function dashboardHtml(): string {
|
|
|
246
337
|
<h2>Agents</h2>
|
|
247
338
|
<div class="meta">Use runtime=codex for executable demo agents.</div>
|
|
248
339
|
</div>
|
|
340
|
+
<button id="open-settings" class="secondary settings-trigger" type="button">Settings</button>
|
|
249
341
|
</header>
|
|
250
342
|
<div class="toolbox">
|
|
251
343
|
<form id="invite-form" class="split">
|
|
@@ -259,29 +351,135 @@ export function dashboardHtml(): string {
|
|
|
259
351
|
</select>
|
|
260
352
|
<button>Invite</button>
|
|
261
353
|
</form>
|
|
262
|
-
<form id="agent-form" class="create-agent">
|
|
263
|
-
<p class="section-title">Create or Update Agent</p>
|
|
264
|
-
<input id="agent-key" placeholder="codex" autocomplete="off" required>
|
|
265
|
-
<input id="agent-name" placeholder="Codex" autocomplete="off" required>
|
|
266
|
-
<input id="agent-runtime" placeholder="codex" autocomplete="off">
|
|
267
|
-
<input id="agent-computer" placeholder="machine id" autocomplete="off">
|
|
268
|
-
<button>Create agent</button>
|
|
269
|
-
</form>
|
|
270
|
-
<form id="computer-form" class="create-agent">
|
|
271
|
-
<p class="section-title">Provision Computer</p>
|
|
272
|
-
<input id="computer-name" placeholder="Bill's Team" autocomplete="off">
|
|
273
|
-
<button>Generate command</button>
|
|
274
|
-
<textarea id="computer-command" readonly rows="4" placeholder="Daemon command"></textarea>
|
|
275
|
-
</form>
|
|
276
354
|
<div id="members" class="members"></div>
|
|
277
355
|
</div>
|
|
278
356
|
<div id="agents" class="agents"></div>
|
|
279
357
|
</aside>
|
|
280
358
|
</main>
|
|
359
|
+
<section id="settings-backdrop" class="settings-backdrop" aria-hidden="true">
|
|
360
|
+
<div class="settings-dialog" role="dialog" aria-modal="true" aria-labelledby="settings-title">
|
|
361
|
+
<header class="settings-head">
|
|
362
|
+
<div>
|
|
363
|
+
<h2 id="settings-title">Settings</h2>
|
|
364
|
+
<div class="meta">Configure access, agents, computers, and Feishu/Lark from one place.</div>
|
|
365
|
+
</div>
|
|
366
|
+
<button id="close-settings" class="secondary icon" type="button" aria-label="Close settings">×</button>
|
|
367
|
+
</header>
|
|
368
|
+
<div class="settings-body">
|
|
369
|
+
<nav class="settings-nav" aria-label="Settings sections">
|
|
370
|
+
<button class="settings-tab active" type="button" data-settings-tab="access">Access</button>
|
|
371
|
+
<button class="settings-tab" type="button" data-settings-tab="agents">Agents</button>
|
|
372
|
+
<button class="settings-tab" type="button" data-settings-tab="computers">Computers</button>
|
|
373
|
+
<button class="settings-tab" type="button" data-settings-tab="lark">Lark</button>
|
|
374
|
+
</nav>
|
|
375
|
+
<div class="settings-content">
|
|
376
|
+
<section id="settings-access" class="settings-pane active">
|
|
377
|
+
<div class="summary-panel">
|
|
378
|
+
<p class="section-title">Server Access</p>
|
|
379
|
+
<div id="server-access" class="meta">Listening locally. Tailscale address is detected on load.</div>
|
|
380
|
+
</div>
|
|
381
|
+
<div class="settings-section-head">
|
|
382
|
+
<div>
|
|
383
|
+
<h3>Lark Users</h3>
|
|
384
|
+
<div class="meta">Only listed Lark user IDs may trigger inbound bot handling.</div>
|
|
385
|
+
</div>
|
|
386
|
+
<button class="secondary" type="button" data-add-panel="lark-user-form">Add User</button>
|
|
387
|
+
</div>
|
|
388
|
+
<div class="setup-grid">
|
|
389
|
+
<div id="settings-lark-user-list" class="settings-list"></div>
|
|
390
|
+
<form id="lark-user-form" class="setup-panel collapsed">
|
|
391
|
+
<p class="section-title">Authorized Lark User</p>
|
|
392
|
+
<label class="field"><span>User ID</span><input id="lark-user-id" placeholder="on_xxx union id" autocomplete="off" required></label>
|
|
393
|
+
<label class="field"><span>Display name</span><input id="lark-user-name" placeholder="Optional" autocomplete="off"></label>
|
|
394
|
+
<div class="setup-actions">
|
|
395
|
+
<button>Save user</button>
|
|
396
|
+
<button class="secondary" type="button" data-cancel-panel="lark-user-form">Cancel</button>
|
|
397
|
+
</div>
|
|
398
|
+
</form>
|
|
399
|
+
</div>
|
|
400
|
+
</section>
|
|
401
|
+
<section id="settings-agents" class="settings-pane">
|
|
402
|
+
<div class="settings-section-head">
|
|
403
|
+
<div>
|
|
404
|
+
<h3>Agents</h3>
|
|
405
|
+
<div class="meta">Manage logical agents and assign them to an available computer.</div>
|
|
406
|
+
</div>
|
|
407
|
+
<button class="secondary" type="button" data-add-panel="agent-form">Add Agent</button>
|
|
408
|
+
</div>
|
|
409
|
+
<div class="setup-grid">
|
|
410
|
+
<div id="settings-agent-list" class="settings-list"></div>
|
|
411
|
+
<form id="agent-form" class="setup-panel collapsed">
|
|
412
|
+
<p class="section-title">Agent Onboard</p>
|
|
413
|
+
<label class="field"><span>Key</span><input id="agent-key" placeholder="codex" autocomplete="off" required></label>
|
|
414
|
+
<label class="field"><span>Name</span><input id="agent-name" placeholder="Codex" autocomplete="off" required></label>
|
|
415
|
+
<label class="field"><span>Runtime</span><select id="agent-runtime"><option value="codex">codex</option><option value="neeko">neeko</option><option value="coco">coco</option><option value="coco-stream-json">coco-stream-json</option></select></label>
|
|
416
|
+
<label class="field"><span>Computer</span><select id="agent-computer"><option value="">No assignment</option></select></label>
|
|
417
|
+
<label class="field"><span>Description</span><input id="agent-desc" placeholder="Optional" autocomplete="off"></label>
|
|
418
|
+
<div class="setup-actions">
|
|
419
|
+
<button>Onboard agent</button>
|
|
420
|
+
<button class="secondary" type="button" data-cancel-panel="agent-form">Cancel</button>
|
|
421
|
+
</div>
|
|
422
|
+
</form>
|
|
423
|
+
</div>
|
|
424
|
+
</section>
|
|
425
|
+
<section id="settings-computers" class="settings-pane">
|
|
426
|
+
<div class="settings-section-head">
|
|
427
|
+
<div>
|
|
428
|
+
<h3>Computers</h3>
|
|
429
|
+
<div class="meta">Provision daemon credentials and see connected machines.</div>
|
|
430
|
+
</div>
|
|
431
|
+
<button class="secondary" type="button" data-add-panel="computer-form">Add Computer</button>
|
|
432
|
+
</div>
|
|
433
|
+
<div class="setup-grid">
|
|
434
|
+
<div id="settings-computer-list" class="settings-list"></div>
|
|
435
|
+
<form id="computer-form" class="setup-panel collapsed">
|
|
436
|
+
<p class="section-title">Computer Onboard</p>
|
|
437
|
+
<label class="field"><span>Name</span><input id="computer-name" placeholder="Local computer" autocomplete="off"></label>
|
|
438
|
+
<label class="field"><span>Server URL</span><input id="computer-server" autocomplete="off"></label>
|
|
439
|
+
<label class="field"><span>Daemon package</span><input id="computer-package" value="@controlflow-ai/daemon@latest" autocomplete="off"></label>
|
|
440
|
+
<div class="setup-actions">
|
|
441
|
+
<button>Generate command</button>
|
|
442
|
+
<button id="copy-command" class="secondary" type="button">Copy</button>
|
|
443
|
+
</div>
|
|
444
|
+
<button class="secondary" type="button" data-cancel-panel="computer-form">Cancel</button>
|
|
445
|
+
<div class="command-wrap">
|
|
446
|
+
<textarea id="computer-command" readonly rows="4" placeholder="Daemon command"></textarea>
|
|
447
|
+
</div>
|
|
448
|
+
</form>
|
|
449
|
+
</div>
|
|
450
|
+
</section>
|
|
451
|
+
<section id="settings-lark" class="settings-pane">
|
|
452
|
+
<div class="settings-section-head">
|
|
453
|
+
<div>
|
|
454
|
+
<h3>Lark</h3>
|
|
455
|
+
<div class="meta">Bind Feishu/Lark bot credentials to a Pal agent. Secrets stay in the local runtime profile.</div>
|
|
456
|
+
</div>
|
|
457
|
+
<button class="secondary" type="button" data-add-panel="lark-form">Add Lark Bot</button>
|
|
458
|
+
</div>
|
|
459
|
+
<div class="setup-grid">
|
|
460
|
+
<div id="settings-lark-list" class="settings-list"></div>
|
|
461
|
+
<form id="lark-form" class="setup-panel collapsed">
|
|
462
|
+
<p class="section-title">Lark Setup</p>
|
|
463
|
+
<label class="field"><span>Bind agent</span><select id="lark-agent"></select></label>
|
|
464
|
+
<label class="field"><span>Label</span><input id="lark-label" placeholder="Team bot" autocomplete="off"></label>
|
|
465
|
+
<label class="field"><span>App ID</span><input id="lark-app-id" placeholder="cli_xxx" autocomplete="off" required></label>
|
|
466
|
+
<label class="field"><span>App Secret</span><input id="lark-app-secret" type="password" autocomplete="off" required></label>
|
|
467
|
+
<div class="secret-note">Secret is sent only to this local Pal server, validated with Feishu, and stored in the local Lark config file.</div>
|
|
468
|
+
<div class="setup-actions">
|
|
469
|
+
<button>Save Lark bot</button>
|
|
470
|
+
<button class="secondary" type="button" data-cancel-panel="lark-form">Cancel</button>
|
|
471
|
+
</div>
|
|
472
|
+
</form>
|
|
473
|
+
</div>
|
|
474
|
+
</section>
|
|
475
|
+
</div>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
</section>
|
|
281
479
|
<div id="error" class="error" role="alert"></div>
|
|
282
480
|
<div id="toast" class="toast" role="status"></div>
|
|
283
481
|
<script>
|
|
284
|
-
const state = { rooms: [], agents: [], members: [], mentionables: [], messages: [], selectedRoomId: null, mentionIndex: 0 };
|
|
482
|
+
const state = { rooms: [], agents: [], computers: [], lark: null, larkUsers: [], serverAccess: null, members: [], mentionables: [], messages: [], selectedRoomId: null, mentionIndex: 0 };
|
|
285
483
|
const root = (id) => document.getElementById(id);
|
|
286
484
|
const escapeHtml = (value) => String(value ?? '').replace(/[&<>'"]/g, (char) => ({ '&':'&', '<':'<', '>':'>', "'":''', '"':'"' }[char]));
|
|
287
485
|
async function api(path, options) {
|
|
@@ -358,11 +556,58 @@ export function dashboardHtml(): string {
|
|
|
358
556
|
}
|
|
359
557
|
function renderAgents() {
|
|
360
558
|
root('invite-agent').innerHTML = state.agents.length ? state.agents.map((agent) => '<option value="' + escapeHtml(agent.agent_key) + '">' + escapeHtml(agent.display_name) + ' · ' + escapeHtml(agent.agent_key) + '</option>').join('') : '<option value="">No agents</option>';
|
|
559
|
+
root('lark-agent').innerHTML = state.agents.length ? state.agents.map((agent) => '<option value="' + escapeHtml(agent.agent_key) + '">' + escapeHtml(agent.display_name) + ' · ' + escapeHtml(agent.agent_key) + '</option>').join('') : '<option value="">Onboard an agent first</option>';
|
|
361
560
|
root('agents').innerHTML = state.agents.length ? state.agents.map((agent) => (
|
|
362
561
|
'<article class="agent"><div class="agent-title"><span>' + escapeHtml(agent.display_name) + '</span>' + pill(agent.runtime || 'no runtime', agent.runtime === 'codex' ? 'good' : '') + '</div>' +
|
|
363
562
|
'<div class="pills">' + pill(agent.agent_key) + pill(agent.id) + '</div>' +
|
|
364
563
|
'<div class="meta">' + escapeHtml(agent.description || 'No description') + '</div></article>'
|
|
365
564
|
)).join('') : empty('No agents yet. Create codex to start.');
|
|
565
|
+
root('settings-agent-list').innerHTML = state.agents.length
|
|
566
|
+
? state.agents.map((agent) => '<article class="settings-row"><div><strong>' + escapeHtml(agent.display_name) + '</strong><div class="meta">' + escapeHtml(agent.description || 'No description') + '</div><div class="pills">' + pill(agent.agent_key, 'blue') + pill(agent.runtime || 'no runtime', agent.runtime === 'codex' ? 'good' : '') + '</div></div></article>').join('')
|
|
567
|
+
: empty('No agents yet.');
|
|
568
|
+
}
|
|
569
|
+
function renderComputers() {
|
|
570
|
+
root('agent-computer').innerHTML = '<option value="">No assignment</option>' + state.computers.map((computer) => '<option value="' + escapeHtml(computer.id) + '">' + escapeHtml(computer.name) + ' · ' + escapeHtml(computer.id) + '</option>').join('');
|
|
571
|
+
root('settings-computer-list').innerHTML = state.computers.length
|
|
572
|
+
? state.computers.map((computer) => '<article class="settings-row"><div><strong>' + escapeHtml(computer.name) + '</strong><div class="meta">' + escapeHtml(computer.id) + '</div><div class="pills">' + pill(computer.status, computer.status === 'online' ? 'good' : '') + (computer.last_seen_at ? pill('last seen ' + computer.last_seen_at) : '') + '</div></div></article>').join('')
|
|
573
|
+
: empty('No computers yet.');
|
|
574
|
+
}
|
|
575
|
+
function renderLarkConfig() {
|
|
576
|
+
const el = root('settings-lark-list');
|
|
577
|
+
if (!el) return;
|
|
578
|
+
const bots = state.lark?.bots || [];
|
|
579
|
+
el.innerHTML = bots.length
|
|
580
|
+
? bots.map((bot) => '<article class="settings-row"><div><strong>' + escapeHtml(bot.label || bot.appId) + '</strong><div class="meta">' + escapeHtml(bot.appId) + '</div><div class="pills">' + pill('@' + (bot.agent || '-'), 'blue') + pill(bot.botOpenId ? 'open_id resolved' : 'open_id missing', bot.botOpenId ? 'good' : '') + pill(bot.hasSecret ? 'secret stored' : 'secret missing') + '</div></div></article>').join('') + '<div class="meta">Config: ' + escapeHtml(state.lark.path || '') + '</div>'
|
|
581
|
+
: empty('No Lark bots configured yet.');
|
|
582
|
+
}
|
|
583
|
+
function renderLarkUsers() {
|
|
584
|
+
const el = root('settings-lark-user-list');
|
|
585
|
+
if (!el) return;
|
|
586
|
+
el.innerHTML = state.larkUsers.length
|
|
587
|
+
? state.larkUsers.map((user) => '<article class="settings-row"><div><strong>' + escapeHtml(user.display_name || user.user_id) + '</strong><div class="meta">' + escapeHtml(user.user_id) + '</div><div class="pills">' + pill('authorized', 'good') + '</div></div><button class="secondary" type="button" data-delete-lark-user="' + escapeHtml(user.user_id) + '">Delete</button></article>').join('')
|
|
588
|
+
: empty('No authorized Lark users. Inbound bot messages will be ignored.');
|
|
589
|
+
[...el.querySelectorAll('[data-delete-lark-user]')].forEach((button) => {
|
|
590
|
+
button.addEventListener('click', async () => {
|
|
591
|
+
const userId = button.dataset.deleteLarkUser;
|
|
592
|
+
if (!userId) return;
|
|
593
|
+
const deleted = await api('/api/lark/authorized-users/' + encodeURIComponent(userId), { method: 'DELETE' }).catch((error) => { showError(error); return null; });
|
|
594
|
+
if (!deleted) return;
|
|
595
|
+
await loadLarkUsers();
|
|
596
|
+
showToast('Lark user removed');
|
|
597
|
+
});
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
function renderServerAccess() {
|
|
601
|
+
const access = state.serverAccess;
|
|
602
|
+
const el = root('server-access');
|
|
603
|
+
if (!el) return;
|
|
604
|
+
if (!access) {
|
|
605
|
+
el.textContent = 'Listening locally. Tailscale address is detected on load.';
|
|
606
|
+
return;
|
|
607
|
+
}
|
|
608
|
+
el.textContent = access.tailscaleUrl
|
|
609
|
+
? 'Listening on local ' + access.localUrl + ' and Tailscale ' + access.tailscaleUrl
|
|
610
|
+
: 'Listening on local ' + access.localUrl + '. No Tailscale interface detected.';
|
|
366
611
|
}
|
|
367
612
|
function renderMembers() {
|
|
368
613
|
root('members').innerHTML = state.members.length ? state.members.map((member) => (
|
|
@@ -370,6 +615,25 @@ export function dashboardHtml(): string {
|
|
|
370
615
|
'<div class="pills">' + pill(member.source) + pill(member.status) + '</div></article>'
|
|
371
616
|
)).join('') : empty('No known members in this room.');
|
|
372
617
|
}
|
|
618
|
+
function openSettings(tab) {
|
|
619
|
+
root('settings-backdrop').classList.add('open');
|
|
620
|
+
root('settings-backdrop').setAttribute('aria-hidden', 'false');
|
|
621
|
+
selectSettingsTab(tab || 'access');
|
|
622
|
+
}
|
|
623
|
+
function closeSettings() {
|
|
624
|
+
root('settings-backdrop').classList.remove('open');
|
|
625
|
+
root('settings-backdrop').setAttribute('aria-hidden', 'true');
|
|
626
|
+
}
|
|
627
|
+
function selectSettingsTab(tab) {
|
|
628
|
+
document.querySelectorAll('.settings-tab').forEach((button) => button.classList.toggle('active', button.dataset.settingsTab === tab));
|
|
629
|
+
document.querySelectorAll('.settings-pane').forEach((pane) => pane.classList.toggle('active', pane.id === 'settings-' + tab));
|
|
630
|
+
}
|
|
631
|
+
function showPanel(id) {
|
|
632
|
+
root(id)?.classList.remove('collapsed');
|
|
633
|
+
}
|
|
634
|
+
function hidePanel(id) {
|
|
635
|
+
root(id)?.classList.add('collapsed');
|
|
636
|
+
}
|
|
373
637
|
function currentMentionQuery() {
|
|
374
638
|
const input = root('message-content');
|
|
375
639
|
if (!input) return null;
|
|
@@ -461,6 +725,26 @@ export function dashboardHtml(): string {
|
|
|
461
725
|
state.agents = data.agents || [];
|
|
462
726
|
renderAgents();
|
|
463
727
|
}
|
|
728
|
+
async function loadComputers() {
|
|
729
|
+
const data = await api('/api/computers');
|
|
730
|
+
state.computers = data.computers || [];
|
|
731
|
+
renderComputers();
|
|
732
|
+
}
|
|
733
|
+
async function loadLarkConfig() {
|
|
734
|
+
const data = await api('/api/lark/config');
|
|
735
|
+
state.lark = data;
|
|
736
|
+
renderLarkConfig();
|
|
737
|
+
}
|
|
738
|
+
async function loadLarkUsers() {
|
|
739
|
+
const data = await api('/api/lark/authorized-users');
|
|
740
|
+
state.larkUsers = data.users || [];
|
|
741
|
+
renderLarkUsers();
|
|
742
|
+
}
|
|
743
|
+
async function loadServerAccess() {
|
|
744
|
+
const data = await api('/api/server/access');
|
|
745
|
+
state.serverAccess = data;
|
|
746
|
+
renderServerAccess();
|
|
747
|
+
}
|
|
464
748
|
async function loadMessages() {
|
|
465
749
|
const room = activeRoom();
|
|
466
750
|
if (!room) {
|
|
@@ -483,7 +767,7 @@ export function dashboardHtml(): string {
|
|
|
483
767
|
renderMembers();
|
|
484
768
|
}
|
|
485
769
|
async function refresh() {
|
|
486
|
-
await Promise.all([loadRooms(), loadAgents()]);
|
|
770
|
+
await Promise.all([loadRooms(), loadAgents(), loadComputers(), loadLarkConfig(), loadLarkUsers(), loadServerAccess()]);
|
|
487
771
|
await loadMessages();
|
|
488
772
|
}
|
|
489
773
|
window.selectRoom = async (id) => {
|
|
@@ -492,6 +776,23 @@ export function dashboardHtml(): string {
|
|
|
492
776
|
await loadMessages().catch(showError);
|
|
493
777
|
};
|
|
494
778
|
root('refresh').addEventListener('click', () => refresh().catch(showError));
|
|
779
|
+
root('open-settings').addEventListener('click', () => openSettings('access'));
|
|
780
|
+
root('close-settings').addEventListener('click', closeSettings);
|
|
781
|
+
root('settings-backdrop').addEventListener('mousedown', (event) => {
|
|
782
|
+
if (event.target === root('settings-backdrop')) closeSettings();
|
|
783
|
+
});
|
|
784
|
+
document.addEventListener('keydown', (event) => {
|
|
785
|
+
if (event.key === 'Escape' && root('settings-backdrop').classList.contains('open')) closeSettings();
|
|
786
|
+
});
|
|
787
|
+
document.querySelectorAll('.settings-tab').forEach((button) => {
|
|
788
|
+
button.addEventListener('click', () => selectSettingsTab(button.dataset.settingsTab));
|
|
789
|
+
});
|
|
790
|
+
document.querySelectorAll('[data-add-panel]').forEach((button) => {
|
|
791
|
+
button.addEventListener('click', () => showPanel(button.dataset.addPanel));
|
|
792
|
+
});
|
|
793
|
+
document.querySelectorAll('[data-cancel-panel]').forEach((button) => {
|
|
794
|
+
button.addEventListener('click', () => hidePanel(button.dataset.cancelPanel));
|
|
795
|
+
});
|
|
495
796
|
root('room-form').addEventListener('submit', async (event) => {
|
|
496
797
|
event.preventDefault();
|
|
497
798
|
const name = root('room-name').value.trim();
|
|
@@ -507,29 +808,67 @@ export function dashboardHtml(): string {
|
|
|
507
808
|
event.preventDefault();
|
|
508
809
|
const agent_key = root('agent-key').value.trim();
|
|
509
810
|
const display_name = root('agent-name').value.trim();
|
|
510
|
-
const runtime = root('agent-runtime').value.trim() ||
|
|
511
|
-
const computer_id = root('agent-computer').value.trim();
|
|
811
|
+
const runtime = root('agent-runtime').value.trim() || 'codex';
|
|
812
|
+
const computer_id = root('agent-computer').value.trim() || undefined;
|
|
813
|
+
const description = root('agent-desc').value.trim() || null;
|
|
512
814
|
if (!agent_key || !display_name) return;
|
|
513
|
-
const saved = await api('/api/agents', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent_key, display_name, runtime }) }).catch((error) => { showError(error); return null; });
|
|
815
|
+
const saved = await api('/api/agents/onboard', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent_key, display_name, runtime, description, computer_id }) }).catch((error) => { showError(error); return null; });
|
|
514
816
|
if (!saved) return;
|
|
515
|
-
if (computer_id) {
|
|
516
|
-
await api('/api/agents/' + encodeURIComponent(agent_key) + '/assignment', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ computer_id }) }).catch((error) => { showError(error); return null; });
|
|
517
|
-
}
|
|
518
817
|
root('agent-key').value = '';
|
|
519
818
|
root('agent-name').value = '';
|
|
520
|
-
root('agent-runtime').value = '';
|
|
819
|
+
root('agent-runtime').value = 'codex';
|
|
521
820
|
root('agent-computer').value = '';
|
|
821
|
+
root('agent-desc').value = '';
|
|
522
822
|
await loadAgents();
|
|
823
|
+
hidePanel('agent-form');
|
|
523
824
|
showToast('Agent saved: @' + agent_key);
|
|
524
825
|
});
|
|
525
826
|
root('computer-form').addEventListener('submit', async (event) => {
|
|
526
827
|
event.preventDefault();
|
|
527
828
|
const name = root('computer-name').value.trim();
|
|
528
|
-
const
|
|
829
|
+
const server_url = root('computer-server').value.trim() || location.origin;
|
|
830
|
+
const package_name = root('computer-package').value.trim() || undefined;
|
|
831
|
+
const data = await api('/api/computers/provision', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name: name || undefined, server_url, package_name }) }).catch((error) => { showError(error); return null; });
|
|
529
832
|
if (!data) return;
|
|
530
833
|
root('computer-command').value = data.command;
|
|
834
|
+
await loadComputers();
|
|
531
835
|
showToast('Computer provisioned: ' + data.computer.id);
|
|
532
836
|
});
|
|
837
|
+
root('copy-command').addEventListener('click', async () => {
|
|
838
|
+
const command = root('computer-command').value;
|
|
839
|
+
if (!command) return;
|
|
840
|
+
await navigator.clipboard?.writeText(command).catch(() => null);
|
|
841
|
+
showToast('Daemon command copied');
|
|
842
|
+
});
|
|
843
|
+
root('computer-server').value = location.origin;
|
|
844
|
+
root('lark-user-form').addEventListener('submit', async (event) => {
|
|
845
|
+
event.preventDefault();
|
|
846
|
+
const user_id = root('lark-user-id').value.trim();
|
|
847
|
+
const display_name = root('lark-user-name').value.trim() || null;
|
|
848
|
+
if (!user_id) return;
|
|
849
|
+
const saved = await api('/api/lark/authorized-users', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ user_id, display_name }) }).catch((error) => { showError(error); return null; });
|
|
850
|
+
if (!saved) return;
|
|
851
|
+
root('lark-user-id').value = '';
|
|
852
|
+
root('lark-user-name').value = '';
|
|
853
|
+
await loadLarkUsers();
|
|
854
|
+
hidePanel('lark-user-form');
|
|
855
|
+
showToast('Lark user authorized');
|
|
856
|
+
});
|
|
857
|
+
root('lark-form').addEventListener('submit', async (event) => {
|
|
858
|
+
event.preventDefault();
|
|
859
|
+
const agent = root('lark-agent').value;
|
|
860
|
+
const app_id = root('lark-app-id').value.trim();
|
|
861
|
+
const app_secret = root('lark-app-secret').value.trim();
|
|
862
|
+
const label = root('lark-label').value.trim() || undefined;
|
|
863
|
+
if (!agent || !app_id || !app_secret) return;
|
|
864
|
+
const data = await api('/api/lark/setup', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ agent, app_id, app_secret, label }) }).catch((error) => { showError(error); return null; });
|
|
865
|
+
if (!data) return;
|
|
866
|
+
root('lark-app-secret').value = '';
|
|
867
|
+
await loadLarkConfig();
|
|
868
|
+
const reload = await api('/api/lark/reload', { method: 'POST', headers: { 'content-type': 'application/json' }, body: '{}' }).catch((error) => ({ reloadError: error.message }));
|
|
869
|
+
if (!reload.reloadError) hidePanel('lark-form');
|
|
870
|
+
showToast(reload.reloadError ? 'Lark saved, reload failed: ' + reload.reloadError : 'Lark bot saved and reloaded: ' + data.appId);
|
|
871
|
+
});
|
|
533
872
|
root('invite-form').addEventListener('submit', async (event) => {
|
|
534
873
|
event.preventDefault();
|
|
535
874
|
const room = activeRoom();
|