aios-management-web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.env.json +21 -0
  2. package/README.md +257 -0
  3. package/data/management-console.db +0 -0
  4. package/data/management-console.db-shm +0 -0
  5. package/data/management-console.db-wal +0 -0
  6. package/dist/assets/index-CV_wjCAG.js +464 -0
  7. package/dist/assets/index-DfMPB0eV.css +1 -0
  8. package/dist/index.html +13 -0
  9. package/docs/spec.md +199 -0
  10. package/index.html +12 -0
  11. package/package.json +37 -0
  12. package/scripts/reset-kernel.js +59 -0
  13. package/scripts/reset-password.js +22 -0
  14. package/server/fakes.js +57 -0
  15. package/server/index.js +21 -0
  16. package/server/src/api/middleware/auth.js +29 -0
  17. package/server/src/api/middleware/internal.js +44 -0
  18. package/server/src/api/routes/index.js +677 -0
  19. package/server/src/app.js +90 -0
  20. package/server/src/background/index.js +106 -0
  21. package/server/src/background/protocol.js +15 -0
  22. package/server/src/config/env.js +90 -0
  23. package/server/src/db/index.js +501 -0
  24. package/server/src/infra/mqtt/management-rpc-client.js +213 -0
  25. package/server/src/infra/providers/hzg-provider-client.js +39 -0
  26. package/server/src/infra/s3/object-storage.js +97 -0
  27. package/server/src/services/agent-quota.js +54 -0
  28. package/server/src/services/agent-service.js +696 -0
  29. package/server/src/services/agent-status-sync-service.js +132 -0
  30. package/server/src/services/audit-log-service.js +39 -0
  31. package/server/src/services/auth-service.js +153 -0
  32. package/server/src/services/catalog-sync-service.js +712 -0
  33. package/server/src/services/external-service.js +308 -0
  34. package/server/src/services/kernel-reset-service.js +86 -0
  35. package/server/src/services/portal-service.js +555 -0
  36. package/server/src/services/system-service.js +580 -0
  37. package/server/src/services/topic-ping-service.js +282 -0
  38. package/server/src/utils/errors.js +36 -0
  39. package/server/src/utils/security.js +22 -0
  40. package/server/test/agent-service-alignment.test.js +316 -0
  41. package/server/test/agent-service-create.test.js +662 -0
  42. package/server/test/agent-status-sync-service.test.js +167 -0
  43. package/server/test/agent-update-audit.test.js +63 -0
  44. package/server/test/auth-middleware.test.js +71 -0
  45. package/server/test/background-services.test.js +160 -0
  46. package/server/test/catalog-sync-service.test.js +920 -0
  47. package/server/test/db-reset-migration.test.js +123 -0
  48. package/server/test/env-config.test.js +68 -0
  49. package/server/test/external-service.test.js +380 -0
  50. package/server/test/hzg-provider-client.test.js +50 -0
  51. package/server/test/internal-auth-middleware.test.js +66 -0
  52. package/server/test/kernel-reset-service.test.js +112 -0
  53. package/server/test/management-rpc-client.test.js +105 -0
  54. package/server/test/portal-service-access-tokens.test.js +121 -0
  55. package/server/test/portal-service-alignment.test.js +318 -0
  56. package/server/test/portal-service-management-logs.test.js +114 -0
  57. package/server/test/reset-kernel-cli.test.js +23 -0
  58. package/server/test/service-api-auth-middleware.test.js +59 -0
  59. package/server/test/system-service-alignment.test.js +265 -0
  60. package/server/test/topic-ping-service.test.js +182 -0
  61. package/server/test/usage-refresh-audit-route.test.js +82 -0
  62. package/src/App.jsx +1 -0
  63. package/src/api.js +1 -0
  64. package/src/app/App.jsx +346 -0
  65. package/src/app/api-client.js +112 -0
  66. package/src/components/AppShell.jsx +117 -0
  67. package/src/components/CardTitleWithReload.jsx +20 -0
  68. package/src/components/DeleteActionButton.jsx +31 -0
  69. package/src/main.jsx +14 -0
  70. package/src/pages/AgentsPage.jsx +647 -0
  71. package/src/pages/AiosUsersPage.jsx +151 -0
  72. package/src/pages/DashboardPage.jsx +72 -0
  73. package/src/pages/LoginPage.jsx +41 -0
  74. package/src/pages/SettingsPage.jsx +431 -0
  75. package/src/pages/SkillsPage.jsx +175 -0
  76. package/src/pages/SystemLogsPage.jsx +349 -0
  77. package/src/pages/SystemsPage.jsx +498 -0
  78. package/src/pages/TemplatesPage.jsx +207 -0
  79. package/src/pages/UserManagementPage.jsx +25 -0
  80. package/src/pages/UsersPage.jsx +192 -0
  81. package/src/pages/system-logs/SystemLogsTabs.jsx +362 -0
  82. package/src/styles.css +222 -0
  83. package/src/utils/format.js +63 -0
  84. package/test/.reports/fast-2026-05-25T08-32-39-420Z.json +299 -0
  85. package/test/integration/common.js +208 -0
  86. package/test/integration/fast.js +135 -0
  87. package/test/integration/full.js +306 -0
  88. package/test/run-browser-e2e.js +212 -0
  89. package/test/run-jasmine.js +21 -0
  90. package/test/setup.js +1 -0
  91. package/vite.config.js +12 -0
@@ -0,0 +1,501 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { DatabaseSync } from "node:sqlite";
4
+ import crypto from "node:crypto";
5
+
6
+ import { loadEnv } from "../config/env.js";
7
+ import { hashPassword } from "../utils/security.js";
8
+
9
+ const env = loadEnv();
10
+ const SCHEMA_VERSION = 1;
11
+ const DEFAULT_ACCESS_TOKEN = "sk-1234567890qwertyuiop";
12
+ const DEFAULT_PORTAL_NAME = "AIOS 管理控制台";
13
+ const DEFAULT_BRAND_SUBTITLE = "Your Company Name";
14
+ const DEFAULT_THEME_COLOR = "#07c160";
15
+ const DEFAULT_ADMIN_USERNAME = "aios";
16
+ const DEFAULT_ADMIN_DISPLAY_NAME = "AIOS 管理员";
17
+ const DEFAULT_ADMIN_PASSWORD = "123456";
18
+
19
+ if (!fs.existsSync(env.dataDir)) {
20
+ fs.mkdirSync(env.dataDir, { recursive: true });
21
+ }
22
+
23
+ const dbPath = path.join(env.dataDir, "management-console.db");
24
+ const db = new DatabaseSync(dbPath);
25
+ db.exec("PRAGMA foreign_keys = ON;");
26
+ db.exec("PRAGMA journal_mode = WAL;");
27
+
28
+ export function jsonStringify(value, fallback = []) {
29
+ return JSON.stringify(value ?? fallback);
30
+ }
31
+
32
+ export function jsonParse(value, fallback = []) {
33
+ if (!value) {
34
+ return fallback;
35
+ }
36
+
37
+ try {
38
+ return JSON.parse(value);
39
+ } catch {
40
+ return fallback;
41
+ }
42
+ }
43
+
44
+ export function withTransaction(fn) {
45
+ db.exec("BEGIN");
46
+ try {
47
+ const result = fn();
48
+ db.exec("COMMIT");
49
+ return result;
50
+ } catch (error) {
51
+ db.exec("ROLLBACK");
52
+ throw error;
53
+ }
54
+ }
55
+
56
+ const TRANSACTION_QUEUE = Symbol.for("aios.management.transactionQueue");
57
+
58
+ export async function withSerializedTransaction(database, fn) {
59
+ const previous = database[TRANSACTION_QUEUE] || Promise.resolve();
60
+ let releaseCurrent;
61
+ const current = new Promise((resolve) => {
62
+ releaseCurrent = resolve;
63
+ });
64
+
65
+ database[TRANSACTION_QUEUE] = previous.catch(() => {}).then(() => current);
66
+ await previous.catch(() => {});
67
+
68
+ database.exec("BEGIN");
69
+ try {
70
+ const result = fn();
71
+ database.exec("COMMIT");
72
+ return result;
73
+ } catch (error) {
74
+ try {
75
+ database.exec("ROLLBACK");
76
+ } catch {
77
+ // Ignore rollback failures if SQLite already aborted the transaction.
78
+ }
79
+ throw error;
80
+ } finally {
81
+ releaseCurrent();
82
+ }
83
+ }
84
+
85
+ export function newAccessToken() {
86
+ return `sk-${crypto.randomBytes(18).toString("base64url")}`;
87
+ }
88
+
89
+ function currentSchemaVersion() {
90
+ db.exec(`
91
+ CREATE TABLE IF NOT EXISTS schema_meta (
92
+ key TEXT PRIMARY KEY,
93
+ value TEXT NOT NULL
94
+ );
95
+ `);
96
+
97
+ const row = db.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").get();
98
+ return row ? Number(row.value) : 0;
99
+ }
100
+
101
+ function setSchemaVersion(version) {
102
+ db.prepare(`
103
+ INSERT INTO schema_meta (key, value)
104
+ VALUES ('schema_version', ?)
105
+ ON CONFLICT(key) DO UPDATE SET value = excluded.value
106
+ `).run(String(version));
107
+ }
108
+
109
+ function quoteIdentifier(name) {
110
+ return `"${String(name).replaceAll("\"", "\"\"")}"`;
111
+ }
112
+
113
+ function resetSchema() {
114
+ const objects = db.prepare(`
115
+ SELECT type, name
116
+ FROM sqlite_master
117
+ WHERE name NOT LIKE 'sqlite_%'
118
+ AND name <> 'schema_meta'
119
+ AND type IN ('table', 'view')
120
+ ORDER BY CASE WHEN type = 'view' THEN 0 ELSE 1 END, name
121
+ `).all();
122
+
123
+ for (const object of objects) {
124
+ db.exec(`DROP ${object.type.toUpperCase()} IF EXISTS ${quoteIdentifier(object.name)};`);
125
+ }
126
+ }
127
+
128
+ function createStateTable(name) {
129
+ return `
130
+ CREATE TABLE ${name} (
131
+ id INTEGER PRIMARY KEY CHECK (id = 1),
132
+ status TEXT NOT NULL DEFAULT 'idle',
133
+ trigger_source TEXT,
134
+ started_at TEXT,
135
+ finished_at TEXT,
136
+ last_success_at TEXT,
137
+ error_message TEXT,
138
+ summary_json TEXT NOT NULL DEFAULT '{}',
139
+ created_at TEXT NOT NULL,
140
+ updated_at TEXT NOT NULL
141
+ );
142
+ `;
143
+ }
144
+
145
+ function createSchema() {
146
+ db.exec(`
147
+ CREATE TABLE settings (
148
+ id INTEGER PRIMARY KEY CHECK (id = 1),
149
+ portal_name TEXT NOT NULL,
150
+ brand_subtitle TEXT NOT NULL,
151
+ theme_color TEXT NOT NULL,
152
+ created_at TEXT NOT NULL,
153
+ updated_at TEXT NOT NULL
154
+ );
155
+
156
+ CREATE TABLE access_tokens (
157
+ token TEXT PRIMARY KEY,
158
+ created_at TEXT NOT NULL,
159
+ updated_at TEXT NOT NULL
160
+ );
161
+
162
+ ${createStateTable("usage_refresh_state")}
163
+ ${createStateTable("agent_sync_state")}
164
+ ${createStateTable("skill_sync_state")}
165
+ ${createStateTable("template_sync_state")}
166
+ ${createStateTable("system_sync_state")}
167
+
168
+ CREATE TABLE users (
169
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
170
+ role TEXT NOT NULL CHECK (role IN ('aios-admin')),
171
+ username TEXT NOT NULL UNIQUE,
172
+ display_name TEXT NOT NULL,
173
+ status TEXT NOT NULL CHECK (status IN ('active', 'disabled')),
174
+ password_hash TEXT NOT NULL DEFAULT '',
175
+ must_change_password INTEGER NOT NULL DEFAULT 0,
176
+ is_builtin INTEGER NOT NULL DEFAULT 0,
177
+ tags_json TEXT NOT NULL DEFAULT '[]',
178
+ created_at TEXT NOT NULL,
179
+ updated_at TEXT NOT NULL
180
+ );
181
+
182
+ CREATE TABLE sessions (
183
+ token TEXT PRIMARY KEY,
184
+ user_id INTEGER NOT NULL,
185
+ expires_at TEXT NOT NULL,
186
+ created_at TEXT NOT NULL,
187
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
188
+ );
189
+
190
+ CREATE TABLE aios_users (
191
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
192
+ username TEXT NOT NULL UNIQUE,
193
+ created_at TEXT NOT NULL,
194
+ updated_at TEXT NOT NULL
195
+ );
196
+
197
+ CREATE TABLE agents (
198
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
199
+ slug TEXT NOT NULL UNIQUE,
200
+ agent_name TEXT NOT NULL,
201
+ description TEXT NOT NULL,
202
+ docs_content TEXT NOT NULL DEFAULT '',
203
+ template_name TEXT NOT NULL DEFAULT 'default',
204
+ status TEXT NOT NULL CHECK (status IN ('normal', 'disabled', 'overlimit')),
205
+ tags_json TEXT NOT NULL DEFAULT '[]',
206
+ daily_limit INTEGER NOT NULL,
207
+ usage_snapshot_json TEXT NOT NULL DEFAULT '{}',
208
+ remote_state_json TEXT NOT NULL DEFAULT '{}',
209
+ created_at TEXT NOT NULL,
210
+ updated_at TEXT NOT NULL
211
+ );
212
+
213
+ CREATE TABLE external_sessions (
214
+ session_id TEXT PRIMARY KEY,
215
+ aios_user_id INTEGER NOT NULL,
216
+ agent_id INTEGER NOT NULL,
217
+ created_at TEXT NOT NULL,
218
+ updated_at TEXT NOT NULL,
219
+ UNIQUE (aios_user_id, agent_id),
220
+ FOREIGN KEY (aios_user_id) REFERENCES aios_users(id) ON DELETE CASCADE,
221
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
222
+ );
223
+
224
+ CREATE TABLE external_session_cookies (
225
+ session_id TEXT NOT NULL,
226
+ provider TEXT NOT NULL CHECK (provider IN ('hzg', 'phx')),
227
+ cookie TEXT NOT NULL CHECK (length(cookie) <= 16384),
228
+ created_at TEXT NOT NULL,
229
+ updated_at TEXT NOT NULL,
230
+ PRIMARY KEY (session_id, provider),
231
+ FOREIGN KEY (session_id) REFERENCES external_sessions(session_id) ON DELETE CASCADE
232
+ );
233
+
234
+ CREATE TABLE artifacts (
235
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
236
+ kind TEXT NOT NULL,
237
+ bucket TEXT NOT NULL,
238
+ object_key TEXT NOT NULL,
239
+ original_name TEXT NOT NULL,
240
+ mime_type TEXT NOT NULL,
241
+ byte_size INTEGER NOT NULL,
242
+ created_by INTEGER,
243
+ created_at TEXT NOT NULL,
244
+ FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
245
+ );
246
+
247
+ CREATE TABLE agent_templates (
248
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
249
+ template_name TEXT NOT NULL UNIQUE,
250
+ description TEXT NOT NULL,
251
+ artifact_id INTEGER NOT NULL,
252
+ remote_status TEXT NOT NULL DEFAULT 'pending',
253
+ remote_result_json TEXT,
254
+ is_builtin INTEGER NOT NULL DEFAULT 0,
255
+ created_at TEXT NOT NULL,
256
+ updated_at TEXT NOT NULL,
257
+ FOREIGN KEY (artifact_id) REFERENCES artifacts(id) ON DELETE RESTRICT
258
+ );
259
+
260
+ CREATE TABLE skills (
261
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
262
+ slug TEXT NOT NULL UNIQUE,
263
+ description TEXT NOT NULL,
264
+ artifact_id INTEGER,
265
+ remote_status TEXT NOT NULL DEFAULT 'cataloged',
266
+ is_builtin INTEGER NOT NULL DEFAULT 0,
267
+ created_at TEXT NOT NULL,
268
+ updated_at TEXT NOT NULL,
269
+ FOREIGN KEY (artifact_id) REFERENCES artifacts(id) ON DELETE SET NULL
270
+ );
271
+
272
+ CREATE TABLE agent_permissions (
273
+ agent_id INTEGER NOT NULL,
274
+ aios_user_id INTEGER NOT NULL,
275
+ PRIMARY KEY (agent_id, aios_user_id),
276
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
277
+ FOREIGN KEY (aios_user_id) REFERENCES aios_users(id) ON DELETE CASCADE
278
+ );
279
+
280
+ CREATE TABLE agent_skill_bindings (
281
+ agent_id INTEGER NOT NULL,
282
+ skill_id INTEGER NOT NULL,
283
+ PRIMARY KEY (agent_id, skill_id),
284
+ FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
285
+ FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE
286
+ );
287
+
288
+ CREATE TABLE business_systems (
289
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
290
+ provider TEXT NOT NULL CHECK (provider IN ('hzg', 'phx')),
291
+ application_name TEXT NOT NULL UNIQUE,
292
+ description TEXT NOT NULL,
293
+ ontology_artifact_id INTEGER,
294
+ scheme TEXT NOT NULL CHECK (scheme IN ('http', 'https')),
295
+ host TEXT NOT NULL,
296
+ port INTEGER NOT NULL,
297
+ status TEXT NOT NULL CHECK (status IN ('active', 'disabled')),
298
+ last_connectivity_test_status TEXT NOT NULL DEFAULT 'unknown',
299
+ last_connectivity_test_result_json TEXT NOT NULL DEFAULT '{}',
300
+ is_builtin INTEGER NOT NULL DEFAULT 0,
301
+ created_at TEXT NOT NULL,
302
+ updated_at TEXT NOT NULL,
303
+ FOREIGN KEY (ontology_artifact_id) REFERENCES artifacts(id) ON DELETE SET NULL
304
+ );
305
+
306
+ CREATE TABLE audit_logs (
307
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
308
+ user_id INTEGER,
309
+ username TEXT NOT NULL,
310
+ action TEXT NOT NULL,
311
+ detail TEXT NOT NULL,
312
+ created_at TEXT NOT NULL,
313
+ FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
314
+ );
315
+
316
+ CREATE TABLE system_invocation_logs (
317
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
318
+ trace_id TEXT NOT NULL UNIQUE,
319
+ agent_slug TEXT,
320
+ session_id TEXT,
321
+ provider TEXT NOT NULL,
322
+ application_name TEXT NOT NULL,
323
+ command_name TEXT NOT NULL,
324
+ request_payload_json TEXT NOT NULL,
325
+ response_payload_json TEXT NOT NULL,
326
+ response_time_ms INTEGER NOT NULL,
327
+ success INTEGER NOT NULL,
328
+ error_message TEXT,
329
+ created_at TEXT NOT NULL
330
+ );
331
+
332
+ CREATE INDEX idx_system_invocation_logs_session_id
333
+ ON system_invocation_logs(session_id);
334
+
335
+ CREATE TABLE management_requests (
336
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
337
+ request_id TEXT NOT NULL UNIQUE,
338
+ action TEXT NOT NULL,
339
+ params_json TEXT NOT NULL,
340
+ ok INTEGER,
341
+ result_json TEXT,
342
+ error_json TEXT,
343
+ created_at TEXT NOT NULL,
344
+ completed_at TEXT
345
+ );
346
+ `);
347
+ }
348
+
349
+ function seedStateRow(tableName, now) {
350
+ db.prepare(`
351
+ INSERT INTO ${tableName} (
352
+ id, status, trigger_source, started_at, finished_at, last_success_at,
353
+ error_message, summary_json, created_at, updated_at
354
+ ) VALUES (1, 'idle', NULL, NULL, NULL, NULL, NULL, '{}', ?, ?)
355
+ `).run(now, now);
356
+ }
357
+
358
+ function seed() {
359
+ const now = new Date().toISOString();
360
+
361
+ db.prepare(`
362
+ INSERT INTO settings (
363
+ id, portal_name, brand_subtitle, theme_color, created_at, updated_at
364
+ ) VALUES (1, ?, ?, ?, ?, ?)
365
+ `).run(
366
+ DEFAULT_PORTAL_NAME,
367
+ DEFAULT_BRAND_SUBTITLE,
368
+ DEFAULT_THEME_COLOR,
369
+ now,
370
+ now
371
+ );
372
+
373
+ db.prepare(`
374
+ INSERT INTO access_tokens (
375
+ token, created_at, updated_at
376
+ ) VALUES (?, ?, ?)
377
+ `).run(DEFAULT_ACCESS_TOKEN, now, now);
378
+
379
+ for (const tableName of [
380
+ "usage_refresh_state",
381
+ "agent_sync_state",
382
+ "skill_sync_state",
383
+ "template_sync_state",
384
+ "system_sync_state"
385
+ ]) {
386
+ seedStateRow(tableName, now);
387
+ }
388
+
389
+ db.prepare(`
390
+ INSERT INTO users (
391
+ role, username, display_name, status, password_hash,
392
+ must_change_password, is_builtin, tags_json, created_at, updated_at
393
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
394
+ `).run(
395
+ "aios-admin",
396
+ DEFAULT_ADMIN_USERNAME,
397
+ DEFAULT_ADMIN_DISPLAY_NAME,
398
+ "active",
399
+ hashPassword(DEFAULT_ADMIN_PASSWORD),
400
+ 1,
401
+ 1,
402
+ jsonStringify(["builtin", "ops"]),
403
+ now,
404
+ now
405
+ );
406
+ }
407
+
408
+ function ensureCoreRows() {
409
+ const now = new Date().toISOString();
410
+
411
+ db.prepare(`
412
+ INSERT INTO settings (
413
+ id, portal_name, brand_subtitle, theme_color, created_at, updated_at
414
+ ) VALUES (1, ?, ?, ?, ?, ?)
415
+ ON CONFLICT(id) DO NOTHING
416
+ `).run(
417
+ DEFAULT_PORTAL_NAME,
418
+ DEFAULT_BRAND_SUBTITLE,
419
+ DEFAULT_THEME_COLOR,
420
+ now,
421
+ now
422
+ );
423
+
424
+ const accessTokenCount = Number(
425
+ db.prepare("SELECT COUNT(*) AS count FROM access_tokens").get()?.count || 0
426
+ );
427
+ if (accessTokenCount === 0) {
428
+ db.prepare(`
429
+ INSERT INTO access_tokens (
430
+ token, created_at, updated_at
431
+ ) VALUES (?, ?, ?)
432
+ `).run(DEFAULT_ACCESS_TOKEN, now, now);
433
+ }
434
+
435
+ for (const tableName of [
436
+ "usage_refresh_state",
437
+ "agent_sync_state",
438
+ "skill_sync_state",
439
+ "template_sync_state",
440
+ "system_sync_state"
441
+ ]) {
442
+ db.prepare(`
443
+ INSERT INTO ${tableName} (
444
+ id, status, trigger_source, started_at, finished_at, last_success_at,
445
+ error_message, summary_json, created_at, updated_at
446
+ ) VALUES (1, 'idle', NULL, NULL, NULL, NULL, NULL, '{}', ?, ?)
447
+ ON CONFLICT(id) DO NOTHING
448
+ `).run(now, now);
449
+ }
450
+
451
+ db.prepare(`
452
+ INSERT INTO users (
453
+ role, username, display_name, status, password_hash,
454
+ must_change_password, is_builtin, tags_json, created_at, updated_at
455
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
456
+ ON CONFLICT(username) DO UPDATE SET
457
+ role = 'aios-admin',
458
+ display_name = COALESCE(NULLIF(users.display_name, ''), excluded.display_name),
459
+ status = 'active',
460
+ is_builtin = 1,
461
+ updated_at = excluded.updated_at
462
+ `).run(
463
+ "aios-admin",
464
+ DEFAULT_ADMIN_USERNAME,
465
+ DEFAULT_ADMIN_DISPLAY_NAME,
466
+ "active",
467
+ hashPassword(DEFAULT_ADMIN_PASSWORD),
468
+ 1,
469
+ 1,
470
+ jsonStringify(["builtin", "ops"]),
471
+ now,
472
+ now
473
+ );
474
+ }
475
+
476
+ function migrate() {
477
+ const version = currentSchemaVersion();
478
+
479
+ if (version !== SCHEMA_VERSION) {
480
+ db.exec("PRAGMA foreign_keys = OFF;");
481
+ db.exec("BEGIN");
482
+ try {
483
+ resetSchema();
484
+ createSchema();
485
+ seed();
486
+ setSchemaVersion(SCHEMA_VERSION);
487
+ db.exec("COMMIT");
488
+ } catch (error) {
489
+ db.exec("ROLLBACK");
490
+ throw error;
491
+ } finally {
492
+ db.exec("PRAGMA foreign_keys = ON;");
493
+ }
494
+ }
495
+
496
+ ensureCoreRows();
497
+ }
498
+
499
+ migrate();
500
+
501
+ export { db, dbPath };
@@ -0,0 +1,213 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { EventEmitter } from "node:events";
3
+
4
+ import mqtt from "mqtt";
5
+
6
+ import { serviceUnavailable } from "../../utils/errors.js";
7
+
8
+ export class ManagementRpcClient extends EventEmitter {
9
+ constructor({ env, db, mqttFactory = mqtt }) {
10
+ super();
11
+ this.env = env;
12
+ this.db = db;
13
+ this.mqttFactory = mqttFactory;
14
+ this.client = null;
15
+ this.pending = new Map();
16
+ }
17
+
18
+ isConfigured() {
19
+ return Boolean(
20
+ this.env.mqtt.brokerUrl &&
21
+ this.env.mqtt.adminInboundTopic &&
22
+ this.env.mqtt.adminOutboundTopic
23
+ );
24
+ }
25
+
26
+ async start() {
27
+ if (!this.isConfigured()) {
28
+ return;
29
+ }
30
+
31
+ if (this.client) {
32
+ return;
33
+ }
34
+
35
+ const connect = this.mqttFactory.connect || this.mqttFactory;
36
+ this.client = connect(this.env.mqtt.brokerUrl, {
37
+ clientId: `aios-web-admin-${randomUUID().slice(0, 8)}`,
38
+ username: this.env.mqtt.username || undefined,
39
+ password: this.env.mqtt.password || undefined,
40
+ clean: true,
41
+ reconnectPeriod: 2000
42
+ });
43
+
44
+ await new Promise((resolve, reject) => {
45
+ const cleanup = () => {
46
+ this.client?.off("connect", onConnect);
47
+ this.client?.off("error", onError);
48
+ };
49
+
50
+ const onConnect = () => {
51
+ cleanup();
52
+ resolve();
53
+ };
54
+
55
+ const onError = (error) => {
56
+ cleanup();
57
+ reject(error);
58
+ };
59
+
60
+ this.client.once("connect", onConnect);
61
+ this.client.once("error", onError);
62
+ });
63
+
64
+ await new Promise((resolve, reject) => {
65
+ this.client.subscribe(this.env.mqtt.adminOutboundTopic, { qos: 1 }, (error) => {
66
+ if (error) {
67
+ reject(error);
68
+ return;
69
+ }
70
+
71
+ resolve();
72
+ });
73
+ });
74
+
75
+ this.client.on("message", (_topic, payload) => {
76
+ try {
77
+ const message = JSON.parse(payload.toString("utf8"));
78
+ this.emit("outbound_raw_message", message);
79
+ if (!message?.requestId) {
80
+ return;
81
+ }
82
+
83
+ const pending = this.pending.get(message.requestId);
84
+ if (!pending) {
85
+ return;
86
+ }
87
+
88
+ clearTimeout(pending.timer);
89
+ this.pending.delete(message.requestId);
90
+ this.emit("outbound_message", {
91
+ ...message,
92
+ request: {
93
+ action: pending.action,
94
+ params: pending.params
95
+ }
96
+ });
97
+ this.finishLog(message);
98
+ if (message.ok) {
99
+ pending.resolve(message.result);
100
+ return;
101
+ }
102
+
103
+ pending.reject(
104
+ serviceUnavailable(message.error?.message || "Management CLI returned an error", message.error)
105
+ );
106
+ } catch (error) {
107
+ console.error("Failed to process management response", error);
108
+ }
109
+ });
110
+ }
111
+
112
+ async stop() {
113
+ if (!this.client) {
114
+ return;
115
+ }
116
+
117
+ for (const [requestId, pending] of this.pending.entries()) {
118
+ clearTimeout(pending.timer);
119
+ this.failLog(requestId, { message: "管理 RPC 客户端已停止" });
120
+ pending.reject(serviceUnavailable("管理 RPC 客户端已停止"));
121
+ }
122
+ this.pending.clear();
123
+
124
+ await new Promise((resolve) => {
125
+ this.client.end(false, {}, () => resolve());
126
+ });
127
+ this.client = null;
128
+ }
129
+
130
+ recordRequest(requestId, action, params) {
131
+ this.db.prepare(`
132
+ INSERT INTO management_requests (
133
+ request_id, action, params_json, created_at
134
+ ) VALUES (?, ?, ?, ?)
135
+ `).run(requestId, action, JSON.stringify(params ?? {}), new Date().toISOString());
136
+ }
137
+
138
+ finishLog(response) {
139
+ this.db.prepare(`
140
+ UPDATE management_requests
141
+ SET ok = ?, result_json = ?, error_json = ?, completed_at = ?
142
+ WHERE request_id = ?
143
+ `).run(
144
+ response.ok ? 1 : 0,
145
+ response.result === undefined ? null : JSON.stringify(response.result),
146
+ response.error === undefined ? null : JSON.stringify(response.error),
147
+ new Date().toISOString(),
148
+ response.requestId
149
+ );
150
+ }
151
+
152
+ failLog(requestId, error) {
153
+ this.db.prepare(`
154
+ UPDATE management_requests
155
+ SET ok = 0, error_json = ?, completed_at = ?
156
+ WHERE request_id = ?
157
+ `).run(
158
+ JSON.stringify(error ?? { message: "Management RPC failed" }),
159
+ new Date().toISOString(),
160
+ requestId
161
+ );
162
+ }
163
+
164
+ async call(action, params = {}, timeoutMs = this.env.managementTimeoutMs || 120000) {
165
+ if (!this.client || !this.isConfigured()) {
166
+ throw serviceUnavailable("管理端 MQTT 桥接未配置");
167
+ }
168
+
169
+ const requestId = randomUUID();
170
+ const message = {
171
+ requestId,
172
+ action,
173
+ params
174
+ };
175
+
176
+ this.recordRequest(requestId, action, params);
177
+
178
+ const resultPromise = new Promise((resolve, reject) => {
179
+ const timer = setTimeout(() => {
180
+ this.pending.delete(requestId);
181
+ this.failLog(requestId, { message: `管理动作超时:${action}`, timeout: true, action });
182
+ reject(serviceUnavailable(`管理动作超时:${action}`, { timeout: true, action }));
183
+ }, timeoutMs);
184
+
185
+ this.pending.set(requestId, { resolve, reject, timer, action, params });
186
+ });
187
+ resultPromise.catch(() => {});
188
+
189
+ await new Promise((resolve, reject) => {
190
+ this.client.publish(
191
+ this.env.mqtt.adminInboundTopic,
192
+ JSON.stringify(message),
193
+ { qos: 1, retain: false },
194
+ (error) => {
195
+ if (error) {
196
+ const pending = this.pending.get(requestId);
197
+ if (pending) {
198
+ clearTimeout(pending.timer);
199
+ this.pending.delete(requestId);
200
+ }
201
+ this.failLog(requestId, { message: error.message || "管理请求发布失败" });
202
+ reject(serviceUnavailable("发布管理请求失败", error));
203
+ return;
204
+ }
205
+
206
+ resolve();
207
+ }
208
+ );
209
+ });
210
+
211
+ return resultPromise;
212
+ }
213
+ }