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,555 @@
1
+ import AdmZip from "adm-zip";
2
+
3
+ import { jsonParse, jsonStringify, newAccessToken } from "../db/index.js";
4
+ import { badRequest, conflict, notFound } from "../utils/errors.js";
5
+ import { hashPassword } from "../utils/security.js";
6
+
7
+ const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
8
+
9
+ function ensureZipContains(buffer, requiredName) {
10
+ const zip = new AdmZip(buffer);
11
+ const names = zip
12
+ .getEntries()
13
+ .filter((entry) => !entry.isDirectory)
14
+ .map((entry) => entry.entryName.replace(/^\/+/, ""));
15
+
16
+ if (!names.includes(requiredName)) {
17
+ throw badRequest(`Zip 包中必须包含顶层文件 ${requiredName}`);
18
+ }
19
+ }
20
+
21
+ function parseJsonOrFallback(value, fallback = null) {
22
+ if (value === undefined || value === null || value === "") {
23
+ return fallback;
24
+ }
25
+
26
+ return jsonParse(value, fallback);
27
+ }
28
+
29
+ function getDurationMs(startedAt, completedAt) {
30
+ if (!startedAt || !completedAt) {
31
+ return null;
32
+ }
33
+
34
+ const started = new Date(startedAt);
35
+ const completed = new Date(completedAt);
36
+ if (Number.isNaN(started.getTime()) || Number.isNaN(completed.getTime())) {
37
+ return null;
38
+ }
39
+
40
+ return Math.max(0, completed.getTime() - started.getTime());
41
+ }
42
+
43
+ function validateSlug(value, label) {
44
+ const normalized = String(value || "").trim();
45
+ if (!normalized) {
46
+ throw badRequest(`${label}不能为空`);
47
+ }
48
+ if (!SLUG_PATTERN.test(normalized)) {
49
+ throw badRequest(`${label}需符合 slug 规则:仅允许小写字母、数字和中划线,且不能以中划线开头或结尾`);
50
+ }
51
+ return normalized;
52
+ }
53
+
54
+ function validateUbuntuDirName(value, label) {
55
+ const normalized = String(value || "").trim();
56
+ if (!normalized) {
57
+ throw badRequest(`${label}不能为空`);
58
+ }
59
+ if (!SLUG_PATTERN.test(normalized)) {
60
+ throw badRequest(`${label}需符合 Ubuntu 目录名规则:仅允许小写字母、数字和中划线,且不能以中划线开头或结尾`);
61
+ }
62
+ return normalized;
63
+ }
64
+
65
+ function normalizeUserStatus(value) {
66
+ const status = String(value || "active").trim();
67
+ if (status !== "active" && status !== "disabled") {
68
+ throw badRequest("用户状态只能是 active 或 disabled");
69
+ }
70
+ return status;
71
+ }
72
+
73
+ export class PortalService {
74
+ constructor({ db, objectStorage, rpcClient, authService }) {
75
+ this.db = db;
76
+ this.objectStorage = objectStorage;
77
+ this.rpcClient = rpcClient;
78
+ this.authService = authService;
79
+ }
80
+
81
+ getSettings() {
82
+ return this.db.prepare("SELECT * FROM settings WHERE id = 1").get();
83
+ }
84
+
85
+ listAccessTokens() {
86
+ return this.db.prepare(`
87
+ SELECT token, created_at, updated_at
88
+ FROM access_tokens
89
+ ORDER BY created_at DESC, token DESC
90
+ `).all();
91
+ }
92
+
93
+ hasAccessToken(token) {
94
+ if (!token) {
95
+ return false;
96
+ }
97
+
98
+ const row = this.db.prepare("SELECT token FROM access_tokens WHERE token = ?").get(token);
99
+ return Boolean(row);
100
+ }
101
+
102
+ createAccessToken() {
103
+ const now = new Date().toISOString();
104
+ let token = "";
105
+
106
+ do {
107
+ token = newAccessToken();
108
+ } while (this.hasAccessToken(token));
109
+
110
+ this.db.prepare(`
111
+ INSERT INTO access_tokens (
112
+ token, created_at, updated_at
113
+ ) VALUES (?, ?, ?)
114
+ `).run(token, now, now);
115
+
116
+ return this.db.prepare(`
117
+ SELECT token, created_at, updated_at
118
+ FROM access_tokens
119
+ WHERE token = ?
120
+ `).get(token);
121
+ }
122
+
123
+ deleteAccessToken(token) {
124
+ const normalizedToken = String(token || "").trim();
125
+ if (!normalizedToken) {
126
+ throw badRequest("缺少访问令牌");
127
+ }
128
+
129
+ const current = this.db.prepare("SELECT token FROM access_tokens WHERE token = ?").get(normalizedToken);
130
+ if (!current) {
131
+ throw notFound("访问令牌不存在");
132
+ }
133
+
134
+ const count = Number(this.db.prepare("SELECT COUNT(*) AS count FROM access_tokens").get()?.count || 0);
135
+ if (count <= 1) {
136
+ throw conflict("至少需要保留一个访问令牌");
137
+ }
138
+
139
+ this.db.prepare("DELETE FROM access_tokens WHERE token = ?").run(normalizedToken);
140
+ return { ok: true };
141
+ }
142
+
143
+ updateSettings(payload) {
144
+ const current = this.getSettings();
145
+ const next = {
146
+ ...current,
147
+ ...payload,
148
+ updated_at: new Date().toISOString()
149
+ };
150
+ this.db.prepare(`
151
+ UPDATE settings
152
+ SET portal_name = ?, brand_subtitle = ?, theme_color = ?, updated_at = ?
153
+ WHERE id = 1
154
+ `).run(
155
+ next.portal_name,
156
+ next.brand_subtitle,
157
+ next.theme_color,
158
+ next.updated_at
159
+ );
160
+ return this.getSettings();
161
+ }
162
+
163
+ listManagementRequests({ page = 1, pageSize = 50 } = {}) {
164
+ const safePage = Math.max(1, Number(page) || 1);
165
+ const safePageSize = Math.max(1, Math.min(200, Number(pageSize) || 50));
166
+ const offset = (safePage - 1) * safePageSize;
167
+ const total = Number(
168
+ this.db.prepare("SELECT COUNT(*) AS count FROM management_requests").get()?.count || 0
169
+ );
170
+ const items = this.db.prepare(`
171
+ SELECT *
172
+ FROM management_requests
173
+ ORDER BY created_at DESC, id DESC
174
+ LIMIT ? OFFSET ?
175
+ `).all(safePageSize, offset).map((row) => ({
176
+ ...row,
177
+ ok: row.ok === null || row.ok === undefined ? null : Boolean(row.ok),
178
+ params: parseJsonOrFallback(row.params_json, {}),
179
+ result: parseJsonOrFallback(row.result_json, null),
180
+ error: parseJsonOrFallback(row.error_json, null),
181
+ response_time_ms: getDurationMs(row.created_at, row.completed_at)
182
+ }));
183
+
184
+ return {
185
+ items,
186
+ total,
187
+ page: safePage,
188
+ pageSize: safePageSize
189
+ };
190
+ }
191
+
192
+ listUsers(role) {
193
+ const rows = role
194
+ ? this.db.prepare("SELECT * FROM users WHERE role = ? ORDER BY updated_at DESC").all(role)
195
+ : this.db.prepare("SELECT * FROM users ORDER BY role, updated_at DESC").all();
196
+ return rows.map((row) => this.authService.mapUser(row));
197
+ }
198
+
199
+ assertUserEditable(row) {
200
+ if (row.is_builtin || row.username === "aios") {
201
+ throw conflict("默认管理员不支持编辑");
202
+ }
203
+ }
204
+
205
+ createUser(payload) {
206
+ if (!payload.username || !payload.display_name || !payload.role) {
207
+ throw badRequest("缺少必要字段:用户名、显示名或角色");
208
+ }
209
+
210
+ const passwordHash = payload.role === "aios-admin" ? hashPassword(payload.password || "123456") : "";
211
+ const now = new Date().toISOString();
212
+ const result = this.db.prepare(`
213
+ INSERT INTO users (
214
+ role, username, display_name, status, password_hash,
215
+ must_change_password, is_builtin, tags_json, created_at, updated_at
216
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
217
+ `).run(
218
+ payload.role,
219
+ payload.username,
220
+ payload.display_name,
221
+ normalizeUserStatus(payload.status),
222
+ passwordHash,
223
+ payload.role === "aios-admin" ? 1 : 0,
224
+ 0,
225
+ jsonStringify(payload.tags),
226
+ now,
227
+ now
228
+ );
229
+ return this.authService.mapUser(
230
+ this.db.prepare("SELECT * FROM users WHERE id = ?").get(result.lastInsertRowid)
231
+ );
232
+ }
233
+
234
+ updateUser(userId, payload) {
235
+ const current = this.db.prepare("SELECT * FROM users WHERE id = ?").get(userId);
236
+ if (!current) {
237
+ throw notFound("用户不存在");
238
+ }
239
+ this.assertUserEditable(current);
240
+
241
+ const next = {
242
+ ...current,
243
+ ...payload,
244
+ status: normalizeUserStatus(payload.status ?? current.status),
245
+ tags_json: jsonStringify(payload.tags ?? jsonParse(current.tags_json)),
246
+ updated_at: new Date().toISOString()
247
+ };
248
+ this.db.prepare(`
249
+ UPDATE users
250
+ SET display_name = ?, status = ?, tags_json = ?, updated_at = ?
251
+ WHERE id = ?
252
+ `).run(
253
+ next.display_name,
254
+ next.status,
255
+ next.tags_json,
256
+ next.updated_at,
257
+ userId
258
+ );
259
+ return this.authService.mapUser(this.db.prepare("SELECT * FROM users WHERE id = ?").get(userId));
260
+ }
261
+
262
+ deleteUser(userId, operatorUserId) {
263
+ const current = this.db.prepare("SELECT * FROM users WHERE id = ?").get(userId);
264
+ if (!current) {
265
+ throw notFound("用户不存在");
266
+ }
267
+
268
+ if (Number(userId) === Number(operatorUserId)) {
269
+ throw conflict("不允许删除当前登录用户");
270
+ }
271
+
272
+ if (current.is_builtin || current.username === "aios") {
273
+ throw conflict("默认管理员不支持删除");
274
+ }
275
+
276
+ this.db.prepare("DELETE FROM users WHERE id = ?").run(userId);
277
+ }
278
+
279
+ async persistArtifact({ kind, file, createdBy }) {
280
+ const upload = await this.objectStorage.uploadAdminArtifact({ kind, file });
281
+ const result = this.db.prepare(`
282
+ INSERT INTO artifacts (
283
+ kind, bucket, object_key, original_name, mime_type, byte_size, created_by, created_at
284
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)
285
+ `).run(
286
+ kind,
287
+ upload.bucket,
288
+ upload.objectKey,
289
+ file.originalname,
290
+ file.mimetype || "application/octet-stream",
291
+ file.size,
292
+ createdBy,
293
+ new Date().toISOString()
294
+ );
295
+ return {
296
+ id: result.lastInsertRowid,
297
+ ...upload
298
+ };
299
+ }
300
+
301
+ listTemplates() {
302
+ return this.db.prepare(`
303
+ SELECT
304
+ t.*,
305
+ a.original_name AS artifact_name
306
+ FROM agent_templates t
307
+ JOIN artifacts a ON a.id = t.artifact_id
308
+ ORDER BY t.updated_at DESC
309
+ `).all().map((row) => ({
310
+ ...row,
311
+ is_builtin: Boolean(row.is_builtin),
312
+ agent_slugs: this.db.prepare(`
313
+ SELECT slug
314
+ FROM agents
315
+ WHERE template_name = ?
316
+ ORDER BY slug
317
+ `).all(row.template_name).map((item) => item.slug),
318
+ remote_result: jsonParse(row.remote_result_json, {})
319
+ }));
320
+ }
321
+
322
+ async createTemplate({ templateName, description, file, createdBy }) {
323
+ templateName = validateUbuntuDirName(templateName, "模板名称");
324
+ if (!file) {
325
+ throw badRequest("模板名称和模板文件不能为空");
326
+ }
327
+
328
+ ensureZipContains(file.buffer, "AGENTS.md");
329
+ const artifact = await this.persistArtifact({ kind: "template", file, createdBy });
330
+ const remoteResult = await this.rpcClient.call("agent.template.create", {
331
+ templateName,
332
+ bucket: artifact.bucket,
333
+ objectKey: artifact.objectKey,
334
+ replace: true
335
+ });
336
+ const now = new Date().toISOString();
337
+ this.db.prepare(`
338
+ INSERT INTO agent_templates (
339
+ template_name, description, artifact_id, remote_status, remote_result_json, created_at, updated_at
340
+ ) VALUES (?, ?, ?, ?, ?, ?, ?)
341
+ ON CONFLICT(template_name) DO UPDATE SET
342
+ description = excluded.description,
343
+ artifact_id = excluded.artifact_id,
344
+ remote_status = excluded.remote_status,
345
+ remote_result_json = excluded.remote_result_json,
346
+ updated_at = excluded.updated_at
347
+ `).run(
348
+ templateName,
349
+ description || "",
350
+ artifact.id,
351
+ "ready",
352
+ JSON.stringify(remoteResult ?? {}),
353
+ now,
354
+ now
355
+ );
356
+ return this.listTemplates().find((item) => item.template_name === templateName);
357
+ }
358
+
359
+ async deleteTemplate(templateName) {
360
+ const existing = this.db.prepare("SELECT * FROM agent_templates WHERE template_name = ?").get(templateName);
361
+ if (!existing) {
362
+ throw notFound("模板不存在");
363
+ }
364
+
365
+ if (existing.is_builtin) {
366
+ throw conflict("内置模板不允许删除");
367
+ }
368
+
369
+ const usageCount = Number(this.db.prepare(`
370
+ SELECT COUNT(*) AS count
371
+ FROM agents
372
+ WHERE template_name = ?
373
+ `).get(templateName)?.count || 0);
374
+ if (usageCount > 0) {
375
+ throw conflict("模板仍被数字员工使用,无法删除");
376
+ }
377
+
378
+ await this.rpcClient.call("agent.template.delete", { templateName });
379
+ this.db.prepare("DELETE FROM agent_templates WHERE template_name = ?").run(templateName);
380
+ return { ok: true };
381
+ }
382
+
383
+ listSkills() {
384
+ return this.db.prepare(`
385
+ SELECT
386
+ s.*,
387
+ a.original_name AS artifact_name
388
+ FROM skills s
389
+ LEFT JOIN artifacts a ON a.id = s.artifact_id
390
+ ORDER BY s.updated_at DESC
391
+ `).all().map((row) => ({
392
+ ...row,
393
+ is_builtin: Boolean(row.is_builtin),
394
+ agent_slugs: this.db.prepare(`
395
+ SELECT ag.slug
396
+ FROM agent_skill_bindings b
397
+ JOIN agents ag ON ag.id = b.agent_id
398
+ WHERE b.skill_id = ?
399
+ ORDER BY ag.slug
400
+ `).all(row.id).map((item) => item.slug)
401
+ }));
402
+ }
403
+
404
+ async createSkill({ payload, file, createdBy }) {
405
+ const slug = validateSlug(payload.slug, "技能ID");
406
+ if (!file) {
407
+ throw badRequest("请上传技能 zip 文件");
408
+ }
409
+
410
+ ensureZipContains(file.buffer, "SKILL.md");
411
+ const artifact = await this.persistArtifact({ kind: "skill", file, createdBy });
412
+
413
+ await this.rpcClient.call("skills.global.install.local", {
414
+ slug,
415
+ bucket: artifact.bucket,
416
+ objectKey: artifact.objectKey,
417
+ force: false
418
+ });
419
+ const remoteStatus = "installed";
420
+
421
+ const now = new Date().toISOString();
422
+ const result = this.db.prepare(`
423
+ INSERT INTO skills (
424
+ slug, description, artifact_id, remote_status, created_at, updated_at
425
+ ) VALUES (?, ?, ?, ?, ?, ?)
426
+ `).run(
427
+ slug,
428
+ payload.description || "",
429
+ artifact.id,
430
+ remoteStatus,
431
+ now,
432
+ now
433
+ );
434
+
435
+ const skillId = result.lastInsertRowid;
436
+ return this.listSkills().find((item) => item.id === skillId);
437
+ }
438
+
439
+ async updateSkill(skillId, { payload, file, createdBy }) {
440
+ const current = this.db.prepare("SELECT * FROM skills WHERE id = ?").get(skillId);
441
+ if (!current) {
442
+ throw notFound("技能不存在");
443
+ }
444
+ const slug = validateSlug(current.slug, "技能ID");
445
+ if (!file) {
446
+ throw badRequest("请上传技能 zip 文件");
447
+ }
448
+
449
+ ensureZipContains(file.buffer, "SKILL.md");
450
+ const artifact = await this.persistArtifact({ kind: "skill", file, createdBy });
451
+
452
+ const next = {
453
+ ...current,
454
+ ...payload,
455
+ artifact_id: artifact.id,
456
+ updated_at: new Date().toISOString()
457
+ };
458
+
459
+ await this.rpcClient.call("skills.global.install.local", {
460
+ slug,
461
+ bucket: artifact.bucket,
462
+ objectKey: artifact.objectKey,
463
+ force: true
464
+ });
465
+ const remoteStatus = "installed";
466
+
467
+ this.db.prepare(`
468
+ UPDATE skills
469
+ SET description = ?, artifact_id = ?, remote_status = ?, updated_at = ?
470
+ WHERE id = ?
471
+ `).run(
472
+ next.description ?? current.description,
473
+ next.artifact_id,
474
+ remoteStatus,
475
+ next.updated_at,
476
+ skillId
477
+ );
478
+ return this.listSkills().find((item) => item.id === skillId);
479
+ }
480
+
481
+ replaceSkillBindings(skillId, agentSlugs) {
482
+ this.db.prepare("DELETE FROM agent_skill_bindings WHERE skill_id = ?").run(skillId);
483
+ const insert = this.db.prepare(`
484
+ INSERT INTO agent_skill_bindings (agent_id, skill_id)
485
+ VALUES (?, ?)
486
+ `);
487
+ for (const slug of agentSlugs) {
488
+ const agent = this.db.prepare("SELECT id FROM agents WHERE slug = ?").get(slug);
489
+ if (agent) {
490
+ insert.run(agent.id, skillId);
491
+ }
492
+ }
493
+ }
494
+
495
+ async deleteSkill(skillId) {
496
+ const current = this.db.prepare("SELECT * FROM skills WHERE id = ?").get(skillId);
497
+ if (!current) {
498
+ throw notFound("技能不存在");
499
+ }
500
+
501
+ if (current.is_builtin) {
502
+ throw conflict("内置技能不允许删除");
503
+ }
504
+
505
+ if (current.remote_status === "installed") {
506
+ await this.rpcClient.call("skills.global.delete", { slug: current.slug });
507
+ }
508
+
509
+ this.db.prepare("DELETE FROM skills WHERE id = ?").run(skillId);
510
+ return { ok: true };
511
+ }
512
+
513
+ getDashboard() {
514
+ const agentSyncState = this.db.prepare(`
515
+ SELECT status, last_success_at
516
+ FROM agent_sync_state
517
+ WHERE id = 1
518
+ `).get();
519
+ const usageRefreshState = this.db.prepare(`
520
+ SELECT status, last_success_at
521
+ FROM usage_refresh_state
522
+ WHERE id = 1
523
+ `).get();
524
+ const agentStatsReady = Boolean(agentSyncState?.last_success_at) && agentSyncState?.status === "success";
525
+ const usageStatsReady = Boolean(usageRefreshState?.last_success_at) && usageRefreshState?.status === "success";
526
+ const agentCount = this.db.prepare("SELECT COUNT(*) AS count FROM agents").get().count;
527
+ const templateCount = this.db.prepare("SELECT COUNT(*) AS count FROM agent_templates").get().count;
528
+ const skillCount = this.db.prepare("SELECT COUNT(*) AS count FROM skills").get().count;
529
+ const systemCount = this.db.prepare("SELECT COUNT(*) AS count FROM business_systems").get().count;
530
+ const totalTokenUsage = Number(this.db.prepare(`
531
+ SELECT COALESCE(SUM(COALESCE(json_extract(usage_snapshot_json, '$.usage.daily'), 0)), 0) AS total
532
+ FROM agents
533
+ `).get()?.total || 0);
534
+ const recentInvocations = this.db.prepare(`
535
+ SELECT * FROM system_invocation_logs
536
+ ORDER BY created_at DESC
537
+ LIMIT 8
538
+ `).all().map((row) => ({
539
+ ...row,
540
+ request_payload: jsonParse(row.request_payload_json, {}),
541
+ response_payload: jsonParse(row.response_payload_json, {})
542
+ }));
543
+
544
+ return {
545
+ stats: {
546
+ agents: agentStatsReady ? agentCount : null,
547
+ templates: templateCount,
548
+ skills: skillCount,
549
+ systems: systemCount,
550
+ today_tokens: usageStatsReady ? totalTokenUsage : null
551
+ },
552
+ recentInvocations
553
+ };
554
+ }
555
+ }