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,696 @@
1
+ import { jsonParse, jsonStringify } from "../db/index.js";
2
+ import { badRequest, conflict, notFound } from "../utils/errors.js";
3
+ import {
4
+ findQuotaViolation,
5
+ isAgentStatus,
6
+ normalizeAgentStatus,
7
+ normalizeUsage
8
+ } from "./agent-quota.js";
9
+
10
+ const SLUG_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
11
+
12
+ function normalizeStringList(value) {
13
+ if (Array.isArray(value)) {
14
+ return value
15
+ .map((item) => String(item || "").trim())
16
+ .filter(Boolean);
17
+ }
18
+
19
+ if (typeof value === "string") {
20
+ return value
21
+ .split(",")
22
+ .map((item) => item.trim())
23
+ .filter(Boolean);
24
+ }
25
+
26
+ return [];
27
+ }
28
+
29
+ function resolvePermissionUsernames(payload) {
30
+ return [...new Set(normalizeStringList(payload?.permission_usernames ?? payload?.users))];
31
+ }
32
+
33
+ function hasOwnValue(payload, key) {
34
+ return Boolean(payload) && Object.prototype.hasOwnProperty.call(payload, key);
35
+ }
36
+
37
+ function hasModelConfig(payload) {
38
+ return Boolean(
39
+ payload?.model_primary
40
+ || (Array.isArray(payload?.model_fallbacks) && payload.model_fallbacks.length > 0)
41
+ );
42
+ }
43
+
44
+ function firstText(...values) {
45
+ for (const value of values) {
46
+ if (typeof value === "string" && value.trim()) {
47
+ return value.trim();
48
+ }
49
+ }
50
+
51
+ return "";
52
+ }
53
+
54
+ function renderTopic(template, agentId) {
55
+ return String(template || "").replaceAll("{agentId}", agentId);
56
+ }
57
+
58
+ function validateSlug(value, label = "ID") {
59
+ const normalized = String(value || "").trim();
60
+ if (!normalized) {
61
+ throw badRequest(`${label}不能为空`);
62
+ }
63
+ if (!SLUG_PATTERN.test(normalized)) {
64
+ throw badRequest(`${label}需符合 slug 规则:仅允许小写字母、数字和中划线,且不能以中划线开头或结尾`);
65
+ }
66
+ return normalized;
67
+ }
68
+
69
+ function normalizeRemoteAgentStatus(value) {
70
+ return normalizeAgentStatus(value);
71
+ }
72
+
73
+ function isRemoteAgentMissingError(error) {
74
+ const message = String(error?.message || error?.details?.message || "").toLowerCase();
75
+ const code = String(error?.code || "").toLowerCase();
76
+ const status = Number(error?.status || 0);
77
+
78
+ return (
79
+ code === "not_found"
80
+ || status === 404
81
+ || message.includes("数字员工不存在")
82
+ || message.includes("agent not found")
83
+ || (message.includes("agent") && message.includes("不存在"))
84
+ );
85
+ }
86
+
87
+ function computeHealth(agent, usage) {
88
+ if (agent.status === "disabled") {
89
+ return "disabled";
90
+ }
91
+ if (findQuotaViolation(agent, usage)) {
92
+ return "overlimit";
93
+ }
94
+ return "normal";
95
+ }
96
+
97
+ function mergeRemoteStatus(currentStatus, remoteStatus) {
98
+ if (remoteStatus === "disabled") {
99
+ return "disabled";
100
+ }
101
+ if (currentStatus === "overlimit") {
102
+ return "overlimit";
103
+ }
104
+ return "normal";
105
+ }
106
+
107
+ function runtimeActionForStatusChange(currentStatus, nextStatus) {
108
+ if (currentStatus === "disabled" && nextStatus !== "disabled") {
109
+ return "agent.enable";
110
+ }
111
+ if (currentStatus !== "disabled" && nextStatus === "disabled") {
112
+ return "agent.disable";
113
+ }
114
+ return null;
115
+ }
116
+
117
+ function formatQuotaViolationMessage(agent, violation) {
118
+ return `Cannot set agent ${agent.slug} to normal: ${violation.period} quota exceeded (limit ${violation.limit}, current ${violation.current}). Resolve the quota overage before setting status to normal, or set status to disabled.`;
119
+ }
120
+
121
+ function runInTransaction(database, fn) {
122
+ if (typeof database?.exec !== "function") {
123
+ return fn();
124
+ }
125
+
126
+ database.exec("BEGIN");
127
+ try {
128
+ const result = fn();
129
+ database.exec("COMMIT");
130
+ return result;
131
+ } catch (error) {
132
+ try {
133
+ database.exec("ROLLBACK");
134
+ } catch {}
135
+ throw error;
136
+ }
137
+ }
138
+
139
+ export class AgentService {
140
+ constructor({ db, rpcClient, objectStorage, env }) {
141
+ this.db = db;
142
+ this.rpcClient = rpcClient;
143
+ this.objectStorage = objectStorage;
144
+ this.env = env;
145
+ }
146
+
147
+ localRows() {
148
+ return this.db.prepare("SELECT * FROM agents ORDER BY updated_at DESC").all();
149
+ }
150
+
151
+ getAgentRowById(agentId) {
152
+ return this.db.prepare("SELECT * FROM agents WHERE id = ?").get(agentId);
153
+ }
154
+
155
+ async refreshAgentFromRemote(row) {
156
+ if (!row || typeof this.rpcClient?.call !== "function" || typeof this.rpcClient?.isConfigured !== "function" || !this.rpcClient.isConfigured()) {
157
+ return false;
158
+ }
159
+
160
+ let remoteItem = null;
161
+ try {
162
+ remoteItem = await this.rpcClient.call("agent.get", { agentId: row.slug }, 15000);
163
+ } catch {
164
+ return false;
165
+ }
166
+
167
+ if (!remoteItem) {
168
+ return false;
169
+ }
170
+
171
+ const nextAgentName = firstText(remoteItem.name, row.agent_name, row.slug);
172
+ const nextStatus = mergeRemoteStatus(row.status, normalizeRemoteAgentStatus(remoteItem.status));
173
+ const nextRemoteStateJson = JSON.stringify(remoteItem ?? {});
174
+ const changed =
175
+ nextAgentName !== row.agent_name
176
+ || nextStatus !== row.status
177
+ || nextRemoteStateJson !== row.remote_state_json;
178
+
179
+ if (!changed) {
180
+ return false;
181
+ }
182
+
183
+ this.db.prepare(`
184
+ UPDATE agents
185
+ SET agent_name = ?, status = ?, remote_state_json = ?, updated_at = ?
186
+ WHERE id = ?
187
+ `).run(nextAgentName, nextStatus, nextRemoteStateJson, new Date().toISOString(), row.id);
188
+ return true;
189
+ }
190
+
191
+ getPermissions(agentId) {
192
+ return this.db.prepare(`
193
+ SELECT d.id, d.username
194
+ FROM agent_permissions ap
195
+ JOIN aios_users d ON d.id = ap.aios_user_id
196
+ WHERE ap.agent_id = ?
197
+ ORDER BY d.username
198
+ `).all(agentId);
199
+ }
200
+
201
+ getSkillSlugs(agentId) {
202
+ return this.db.prepare(`
203
+ SELECT s.slug
204
+ FROM agent_skill_bindings b
205
+ JOIN skills s ON s.id = b.skill_id
206
+ WHERE b.agent_id = ?
207
+ ORDER BY s.slug
208
+ `).all(agentId).map((item) => item.slug);
209
+ }
210
+
211
+ async listAgents() {
212
+ const rows = this.localRows();
213
+ return rows.map((row) => {
214
+ const usage = normalizeUsage(jsonParse(row.usage_snapshot_json, {}).usage);
215
+ const remote = jsonParse(row.remote_state_json, {});
216
+ const permissions = this.getPermissions(row.id);
217
+ const skills = this.getSkillSlugs(row.id);
218
+ return {
219
+ ...row,
220
+ tags: jsonParse(row.tags_json),
221
+ usage,
222
+ permissions,
223
+ skills,
224
+ remote_state: remote,
225
+ health: computeHealth(row, usage)
226
+ };
227
+ });
228
+ }
229
+
230
+ async getAgentBySlug(slug) {
231
+ const normalizedSlug = String(slug || "").trim();
232
+ if (!normalizedSlug) {
233
+ throw notFound("数字员工不存在");
234
+ }
235
+
236
+ const current = this.localRows().find((item) => item.slug === normalizedSlug);
237
+ if (!current) {
238
+ throw notFound("数字员工不存在");
239
+ }
240
+
241
+ await this.refreshAgentFromRemote(current);
242
+
243
+ const agent = (await this.listAgents()).find((item) => item.slug === normalizedSlug);
244
+ if (!agent) {
245
+ throw notFound("数字员工不存在");
246
+ }
247
+
248
+ return agent;
249
+ }
250
+
251
+ async listActiveAgentDirectory() {
252
+ const rows = this.db.prepare(`
253
+ SELECT * FROM agents
254
+ WHERE status = 'normal'
255
+ ORDER BY slug
256
+ `).all();
257
+
258
+ return rows.map((row) => {
259
+ const remote = jsonParse(row.remote_state_json, {});
260
+ const permissions = this.getPermissions(row.id);
261
+ return {
262
+ agent_id: row.slug,
263
+ agent_name: firstText(row.agent_name, row.slug),
264
+ status: row.status,
265
+ inbound_topic: firstText(remote?.inboundTopic, remote?.["inbound-topic"]),
266
+ outbound_topic: firstText(remote?.outboundTopic, remote?.["outbound-topic"]),
267
+ users: permissions.map((item) => item.username)
268
+ };
269
+ });
270
+ }
271
+
272
+ async listAgentDirectoryForUser(username) {
273
+ const normalizedUsername = String(username || "").trim();
274
+ const rows = this.db.prepare(`
275
+ SELECT ag.*
276
+ FROM agents ag
277
+ JOIN agent_permissions ap ON ap.agent_id = ag.id
278
+ JOIN aios_users d ON d.id = ap.aios_user_id
279
+ WHERE d.username = ?
280
+ ORDER BY ag.slug
281
+ `).all(normalizedUsername);
282
+
283
+ return rows.map((row) => {
284
+ const remote = jsonParse(row.remote_state_json, {});
285
+ return {
286
+ agent_id: row.slug,
287
+ agent_name: firstText(row.agent_name, row.slug),
288
+ status: row.status,
289
+ inbound_topic: firstText(remote?.inboundTopic, remote?.["inbound-topic"]),
290
+ outbound_topic: firstText(remote?.outboundTopic, remote?.["outbound-topic"]),
291
+ users: [normalizedUsername]
292
+ };
293
+ });
294
+ }
295
+
296
+ buildLocalRemoteState(slug, agentName) {
297
+ return {
298
+ agentId: slug,
299
+ name: agentName,
300
+ inboundTopic: renderTopic(this.env?.mqtt?.agentInboundTopicTemplate, slug),
301
+ outboundTopic: renderTopic(this.env?.mqtt?.agentOutboundTopicTemplate, slug)
302
+ };
303
+ }
304
+
305
+ replacePermissions(agentId, usernames) {
306
+ this.db.prepare("DELETE FROM agent_permissions WHERE agent_id = ?").run(agentId);
307
+ this.ensureAiosUsers(usernames);
308
+ const insert = this.db.prepare(`
309
+ INSERT INTO agent_permissions (agent_id, aios_user_id)
310
+ VALUES (?, ?)
311
+ `);
312
+ for (const username of usernames) {
313
+ const aiosUser = this.db.prepare(`
314
+ SELECT id
315
+ FROM aios_users
316
+ WHERE username = ?
317
+ `).get(username);
318
+ if (aiosUser) {
319
+ insert.run(agentId, aiosUser.id);
320
+ }
321
+ }
322
+ }
323
+
324
+ ensureAiosUsers(usernames) {
325
+ const insert = this.db.prepare(`
326
+ INSERT INTO aios_users (username, created_at, updated_at)
327
+ VALUES (?, ?, ?)
328
+ ON CONFLICT(username) DO UPDATE SET
329
+ updated_at = excluded.updated_at
330
+ `);
331
+ const now = new Date().toISOString();
332
+ for (const username of usernames) {
333
+ insert.run(username, now, now);
334
+ }
335
+ }
336
+
337
+ listAiosUsers(query = "", limit = 20) {
338
+ const normalizedQuery = String(query || "").trim();
339
+ const safeLimit = Math.max(1, Math.min(50, Number(limit) || 20));
340
+ if (!normalizedQuery) {
341
+ return this.db.prepare(`
342
+ SELECT id, username
343
+ FROM aios_users
344
+ ORDER BY updated_at DESC, username ASC
345
+ LIMIT ?
346
+ `).all(safeLimit);
347
+ }
348
+
349
+ return this.db.prepare(`
350
+ SELECT id, username
351
+ FROM aios_users
352
+ WHERE username LIKE ?
353
+ ORDER BY
354
+ CASE WHEN username = ? THEN 0 ELSE 1 END,
355
+ CASE WHEN username LIKE ? THEN 0 ELSE 1 END,
356
+ updated_at DESC,
357
+ username ASC
358
+ LIMIT ?
359
+ `).all(`%${normalizedQuery}%`, normalizedQuery, `${normalizedQuery}%`, safeLimit);
360
+ }
361
+
362
+ getAiosUserUsageCount(aiosUserId) {
363
+ const row = this.db.prepare(`
364
+ SELECT COUNT(*) AS count
365
+ FROM agent_permissions
366
+ WHERE aios_user_id = ?
367
+ `).get(aiosUserId);
368
+ return Number(row?.count || 0);
369
+ }
370
+
371
+ getAiosUserAssignedAgents(aiosUserId) {
372
+ return this.db.prepare(`
373
+ SELECT ag.id, ag.slug, ag.agent_name
374
+ FROM agent_permissions ap
375
+ JOIN agents ag ON ag.id = ap.agent_id
376
+ WHERE ap.aios_user_id = ?
377
+ ORDER BY ag.slug
378
+ `).all(aiosUserId).map((item) => ({
379
+ id: item.id,
380
+ slug: item.slug,
381
+ agent_name: item.agent_name
382
+ }));
383
+ }
384
+
385
+ countAiosUsers(query = "") {
386
+ const normalizedQuery = String(query || "").trim();
387
+ if (!normalizedQuery) {
388
+ return Number(this.db.prepare(`
389
+ SELECT COUNT(*) AS count
390
+ FROM aios_users
391
+ `).get()?.count || 0);
392
+ }
393
+
394
+ return Number(this.db.prepare(`
395
+ SELECT COUNT(*) AS count
396
+ FROM aios_users
397
+ WHERE username LIKE ?
398
+ `).get(`%${normalizedQuery}%`)?.count || 0);
399
+ }
400
+
401
+ listAiosUsersWithUsage(query = "", limit = 50, offset = 0) {
402
+ const normalizedQuery = String(query || "").trim();
403
+ const safeLimit = Math.max(1, Math.min(200, Number(limit) || 50));
404
+ const safeOffset = Math.max(0, Number(offset) || 0);
405
+ const rows = !normalizedQuery
406
+ ? this.db.prepare(`
407
+ SELECT id, username
408
+ FROM aios_users
409
+ ORDER BY updated_at DESC, username ASC
410
+ LIMIT ? OFFSET ?
411
+ `).all(safeLimit, safeOffset)
412
+ : this.db.prepare(`
413
+ SELECT id, username
414
+ FROM aios_users
415
+ WHERE username LIKE ?
416
+ ORDER BY
417
+ CASE WHEN username = ? THEN 0 ELSE 1 END,
418
+ CASE WHEN username LIKE ? THEN 0 ELSE 1 END,
419
+ updated_at DESC,
420
+ username ASC
421
+ LIMIT ? OFFSET ?
422
+ `).all(`%${normalizedQuery}%`, normalizedQuery, `${normalizedQuery}%`, safeLimit, safeOffset);
423
+
424
+ return rows.map((item) => ({
425
+ ...item,
426
+ assigned_agents: this.getAiosUserUsageCount(item.id),
427
+ assigned_agent_items: this.getAiosUserAssignedAgents(item.id)
428
+ }));
429
+ }
430
+
431
+ createAiosUser(username) {
432
+ const normalizedUsername = String(username || "").trim();
433
+ if (!normalizedUsername) {
434
+ throw badRequest("用户名不能为空");
435
+ }
436
+
437
+ this.ensureAiosUsers([normalizedUsername]);
438
+ const row = this.db.prepare(`
439
+ SELECT id, username
440
+ FROM aios_users
441
+ WHERE username = ?
442
+ `).get(normalizedUsername);
443
+
444
+ return {
445
+ ...row,
446
+ assigned_agents: row ? this.getAiosUserUsageCount(row.id) : 0,
447
+ assigned_agent_items: row ? this.getAiosUserAssignedAgents(row.id) : []
448
+ };
449
+ }
450
+
451
+ importAiosUsers(content) {
452
+ const usernames = [...new Set(
453
+ String(content || "")
454
+ .split(/\r?\n/)
455
+ .map((item) => item.trim())
456
+ .filter(Boolean)
457
+ )];
458
+
459
+ if (usernames.length === 0) {
460
+ throw badRequest("请至少输入一个用户名");
461
+ }
462
+
463
+ const existing = new Set(this.listAiosUsers("", 100000).map((item) => item.username));
464
+ this.ensureAiosUsers(usernames);
465
+
466
+ return {
467
+ total: usernames.length,
468
+ created: usernames.filter((username) => !existing.has(username)).length,
469
+ items: usernames.map((username) => ({
470
+ username,
471
+ existed: existing.has(username)
472
+ }))
473
+ };
474
+ }
475
+
476
+ deleteAiosUser(aiosUserId) {
477
+ const current = this.db.prepare(`
478
+ SELECT id, username
479
+ FROM aios_users
480
+ WHERE id = ?
481
+ `).get(aiosUserId);
482
+ if (!current) {
483
+ throw notFound("AIOS 用户不存在");
484
+ }
485
+
486
+ const usageCount = this.getAiosUserUsageCount(aiosUserId);
487
+ if (usageCount > 0) {
488
+ throw conflict("该用户名仍已分配给数字员工,无法删除");
489
+ }
490
+
491
+ this.db.prepare("DELETE FROM aios_users WHERE id = ?").run(aiosUserId);
492
+ return { ok: true, username: current.username };
493
+ }
494
+
495
+ replaceSkills(agentId, skillSlugs) {
496
+ this.db.prepare("DELETE FROM agent_skill_bindings WHERE agent_id = ?").run(agentId);
497
+ const insert = this.db.prepare(`
498
+ INSERT INTO agent_skill_bindings (agent_id, skill_id)
499
+ VALUES (?, ?)
500
+ `);
501
+ for (const slug of skillSlugs) {
502
+ const skill = this.db.prepare("SELECT id FROM skills WHERE slug = ?").get(slug);
503
+ if (skill) {
504
+ insert.run(agentId, skill.id);
505
+ }
506
+ }
507
+ }
508
+
509
+ async createAgent(payload) {
510
+ const slug = validateSlug(payload.slug || payload.id || "", "数字员工 ID");
511
+ const agentName = firstText(payload.agent_name, payload.name, slug);
512
+ if (hasModelConfig(payload)) {
513
+ throw badRequest("当前版本不支持在创建时配置模型");
514
+ }
515
+
516
+ const exists = this.db.prepare("SELECT 1 FROM agents WHERE slug = ?").get(slug);
517
+ if (exists) {
518
+ throw conflict("数字员工 ID 已存在");
519
+ }
520
+
521
+ const templateName = String(payload.template_name || payload.template || "default").trim() || "default";
522
+ await this.rpcClient.call("agent.create", {
523
+ agentId: slug,
524
+ name: agentName,
525
+ agentName,
526
+ templateName,
527
+ restart: payload.restart !== false
528
+ });
529
+
530
+ if (payload.docs_content) {
531
+ try {
532
+ await this.rpcClient.call("agent.docs.update", {
533
+ agentId: slug,
534
+ content: payload.docs_content
535
+ });
536
+ } catch (error) {
537
+ try {
538
+ await this.rpcClient.call("agent.delete", {
539
+ agentId: slug,
540
+ restart: true
541
+ });
542
+ } catch {}
543
+ throw error;
544
+ }
545
+ }
546
+
547
+ let agentId;
548
+ try {
549
+ agentId = runInTransaction(this.db, () => {
550
+ const now = new Date().toISOString();
551
+ const remoteState = this.buildLocalRemoteState(slug, agentName);
552
+ const result = this.db.prepare(`
553
+ INSERT INTO agents (
554
+ slug, agent_name, description, docs_content, template_name, status, tags_json,
555
+ daily_limit, remote_state_json, created_at, updated_at
556
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
557
+ `).run(
558
+ slug,
559
+ agentName,
560
+ payload.description || "",
561
+ payload.docs_content || "",
562
+ templateName,
563
+ "normal",
564
+ jsonStringify(payload.tags),
565
+ Number(payload.daily_limit ?? -1),
566
+ JSON.stringify(remoteState ?? {}),
567
+ now,
568
+ now
569
+ );
570
+
571
+ const insertedAgentId = result.lastInsertRowid;
572
+ if (hasOwnValue(payload, "permission_usernames") || hasOwnValue(payload, "users")) {
573
+ this.replacePermissions(insertedAgentId, resolvePermissionUsernames(payload));
574
+ }
575
+
576
+ if (hasOwnValue(payload, "skill_slugs")) {
577
+ this.replaceSkills(insertedAgentId, payload.skill_slugs || []);
578
+ }
579
+
580
+ return insertedAgentId;
581
+ });
582
+ } catch (error) {
583
+ try {
584
+ await this.rpcClient.call("agent.delete", {
585
+ agentId: slug,
586
+ restart: true
587
+ });
588
+ } catch {}
589
+ throw error;
590
+ }
591
+
592
+ return (await this.listAgents()).find((item) => item.id === agentId);
593
+ }
594
+
595
+ async listActiveAgentDirectoryForUser(username) {
596
+ const items = await this.listAgentDirectoryForUser(username);
597
+ return items.filter((item) => item.status === "normal");
598
+ }
599
+
600
+ async updateAgent(agentId, payload) {
601
+ const current = this.db.prepare("SELECT * FROM agents WHERE id = ?").get(agentId);
602
+ if (!current) {
603
+ throw notFound("数字员工不存在");
604
+ }
605
+ if (hasModelConfig(payload)) {
606
+ throw badRequest("当前版本不支持在编辑时配置模型");
607
+ }
608
+
609
+ const nextStatus = payload.status || current.status;
610
+ if (!isAgentStatus(nextStatus)) {
611
+ throw badRequest("状态只能是 normal、disabled 或 overlimit");
612
+ }
613
+ const nextQuotaFields = {
614
+ ...current,
615
+ daily_limit: Number(payload.daily_limit ?? current.daily_limit)
616
+ };
617
+ if (nextStatus === "normal") {
618
+ const usage = normalizeUsage(jsonParse(current.usage_snapshot_json, {}).usage);
619
+ const violation = findQuotaViolation(nextQuotaFields, usage);
620
+ if (violation) {
621
+ throw badRequest(formatQuotaViolationMessage(current, violation), {
622
+ agentId: current.slug,
623
+ requestedStatus: nextStatus,
624
+ quota: {
625
+ period: violation.period,
626
+ limit: violation.limit,
627
+ current: violation.current
628
+ }
629
+ });
630
+ }
631
+ }
632
+
633
+ const nextAgentName = firstText(payload.agent_name, payload.name, current.agent_name, current.slug);
634
+ const runtimeAction = runtimeActionForStatusChange(current.status, nextStatus);
635
+ if (runtimeAction) {
636
+ await this.rpcClient.call(runtimeAction, {
637
+ agentId: current.slug,
638
+ restart: payload.restart !== false
639
+ });
640
+ }
641
+
642
+ if (payload.docs_content !== undefined && payload.docs_content !== current.docs_content) {
643
+ await this.rpcClient.call("agent.docs.update", {
644
+ agentId: current.slug,
645
+ content: payload.docs_content
646
+ });
647
+ }
648
+
649
+ this.db.prepare(`
650
+ UPDATE agents
651
+ SET agent_name = ?, description = ?, docs_content = ?, status = ?, tags_json = ?,
652
+ daily_limit = ?, updated_at = ?
653
+ WHERE id = ?
654
+ `).run(
655
+ nextAgentName,
656
+ payload.description ?? current.description,
657
+ payload.docs_content ?? current.docs_content,
658
+ nextStatus,
659
+ jsonStringify(payload.tags ?? jsonParse(current.tags_json)),
660
+ Number(payload.daily_limit ?? current.daily_limit),
661
+ new Date().toISOString(),
662
+ agentId
663
+ );
664
+
665
+ if (hasOwnValue(payload, "permission_usernames") || hasOwnValue(payload, "users")) {
666
+ this.replacePermissions(agentId, resolvePermissionUsernames(payload));
667
+ }
668
+
669
+ if (hasOwnValue(payload, "skill_slugs")) {
670
+ this.replaceSkills(agentId, payload.skill_slugs || []);
671
+ }
672
+ return (await this.listAgents()).find((item) => item.id === agentId);
673
+ }
674
+
675
+ async deleteAgent(agentId) {
676
+ const current = this.db.prepare("SELECT * FROM agents WHERE id = ?").get(agentId);
677
+ if (!current) {
678
+ throw notFound("数字员工不存在");
679
+ }
680
+
681
+ try {
682
+ await this.rpcClient.call("agent.delete", {
683
+ agentId: current.slug,
684
+ restart: true
685
+ });
686
+ } catch (error) {
687
+ if (!isRemoteAgentMissingError(error)) {
688
+ throw error;
689
+ }
690
+ }
691
+
692
+ this.db.prepare("DELETE FROM agents WHERE id = ?").run(agentId);
693
+ return { ok: true };
694
+ }
695
+
696
+ }