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,132 @@
1
+ import { withSerializedTransaction } from "../db/index.js";
2
+ import { normalizeAgentStatus } from "./agent-quota.js";
3
+
4
+ const STATUS_SYNC_TIMEOUT_MS = 15000;
5
+
6
+ function firstText(...values) {
7
+ for (const value of values) {
8
+ if (typeof value === "string" && value.trim()) {
9
+ return value.trim();
10
+ }
11
+ }
12
+
13
+ return "";
14
+ }
15
+
16
+ export class AgentStatusSyncService {
17
+ constructor({ db, rpcClient, auditLogService = null }) {
18
+ this.db = db;
19
+ this.rpcClient = rpcClient;
20
+ this.auditLogService = auditLogService;
21
+ this.pendingRefresh = null;
22
+ }
23
+
24
+ async refreshStatuses({ trigger = "scheduled" } = {}) {
25
+ if (this.pendingRefresh) {
26
+ return await this.pendingRefresh;
27
+ }
28
+
29
+ this.pendingRefresh = this.runRefresh(trigger).finally(() => {
30
+ this.pendingRefresh = null;
31
+ });
32
+ return await this.pendingRefresh;
33
+ }
34
+
35
+ async runRefresh(trigger) {
36
+ if (!this.rpcClient.isConfigured()) {
37
+ return {
38
+ status: "skipped",
39
+ trigger,
40
+ checked_agents: 0,
41
+ updated_agents: 0
42
+ };
43
+ }
44
+
45
+ const localRows = this.db.prepare(`
46
+ SELECT *
47
+ FROM agents
48
+ ORDER BY slug
49
+ `).all();
50
+ const remote = await this.rpcClient.call("agent.list", {}, STATUS_SYNC_TIMEOUT_MS);
51
+ const remoteItems = Array.isArray(remote?.items) ? remote.items : [];
52
+ const remoteMap = new Map(
53
+ remoteItems
54
+ .map((item) => [String(item?.agentId || "").trim(), item])
55
+ .filter(([slug]) => slug)
56
+ );
57
+ const now = new Date().toISOString();
58
+ const insert = this.db.prepare(`
59
+ INSERT INTO agents (
60
+ slug, agent_name, description, docs_content, template_name, status, tags_json,
61
+ daily_limit, usage_snapshot_json, remote_state_json, created_at, updated_at
62
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
63
+ `);
64
+ const update = this.db.prepare(`
65
+ UPDATE agents
66
+ SET agent_name = ?, status = ?, remote_state_json = ?, updated_at = ?
67
+ WHERE id = ?
68
+ `);
69
+
70
+ let updatedAgents = 0;
71
+
72
+ await withSerializedTransaction(this.db, () => {
73
+ for (const row of localRows) {
74
+ const remoteItem = remoteMap.get(row.slug);
75
+ if (!remoteItem) {
76
+ continue;
77
+ }
78
+
79
+ const nextAgentName = firstText(remoteItem.name, row.slug);
80
+ const nextStatus = row.status === "disabled"
81
+ ? "disabled"
82
+ : (row.status === "overlimit" ? "overlimit" : normalizeAgentStatus(remoteItem?.status));
83
+ const nextRemoteStateJson = JSON.stringify(remoteItem ?? {});
84
+ const changed =
85
+ nextAgentName !== row.agent_name
86
+ || nextStatus !== row.status
87
+ || nextRemoteStateJson !== row.remote_state_json;
88
+
89
+ if (changed) {
90
+ update.run(nextAgentName, nextStatus, nextRemoteStateJson, now, row.id);
91
+ updatedAgents += 1;
92
+ }
93
+
94
+ remoteMap.delete(row.slug);
95
+ }
96
+
97
+ for (const [slug, remoteItem] of remoteMap.entries()) {
98
+ insert.run(
99
+ slug,
100
+ firstText(remoteItem?.name, slug),
101
+ "",
102
+ "",
103
+ firstText(remoteItem?.templateName, remoteItem?.template, "default"),
104
+ normalizeAgentStatus(remoteItem?.status),
105
+ "[]",
106
+ -1,
107
+ "{}",
108
+ JSON.stringify(remoteItem ?? {}),
109
+ now,
110
+ now
111
+ );
112
+ updatedAgents += 1;
113
+ }
114
+ });
115
+
116
+ if (updatedAgents > 0) {
117
+ this.auditLogService?.write({
118
+ userId: null,
119
+ username: "system",
120
+ action: "刷新数字员工状态",
121
+ detail: `触发方式:${trigger},更新 ${updatedAgents} 个数字员工状态`
122
+ });
123
+ }
124
+
125
+ return {
126
+ status: "success",
127
+ trigger,
128
+ checked_agents: localRows.length,
129
+ updated_agents: updatedAgents
130
+ };
131
+ }
132
+ }
@@ -0,0 +1,39 @@
1
+ export class AuditLogService {
2
+ constructor({ db }) {
3
+ this.db = db;
4
+ }
5
+
6
+ write({ userId, username, action, detail }) {
7
+ this.db.prepare(`
8
+ INSERT INTO audit_logs (
9
+ user_id, username, action, detail, created_at
10
+ ) VALUES (?, ?, ?, ?, ?)
11
+ `).run(
12
+ userId || null,
13
+ username || "",
14
+ action,
15
+ detail || "",
16
+ new Date().toISOString()
17
+ );
18
+ }
19
+
20
+ list({ page = 1, pageSize = 50 } = {}) {
21
+ const safePage = Math.max(1, Number(page) || 1);
22
+ const safePageSize = Math.max(1, Math.min(200, Number(pageSize) || 50));
23
+ const offset = (safePage - 1) * safePageSize;
24
+ const total = this.db.prepare("SELECT COUNT(*) AS count FROM audit_logs").get().count;
25
+ const items = this.db.prepare(`
26
+ SELECT *
27
+ FROM audit_logs
28
+ ORDER BY created_at DESC, id DESC
29
+ LIMIT ? OFFSET ?
30
+ `).all(safePageSize, offset);
31
+
32
+ return {
33
+ items,
34
+ total,
35
+ page: safePage,
36
+ pageSize: safePageSize
37
+ };
38
+ }
39
+ }
@@ -0,0 +1,153 @@
1
+ import { jsonParse } from "../db/index.js";
2
+ import { badRequest, conflict, forbidden, notFound, unauthorized } from "../utils/errors.js";
3
+ import { hashPassword, newToken, verifyPassword } from "../utils/security.js";
4
+
5
+ const ADMIN_ROLE = "aios-admin";
6
+
7
+ export class AuthService {
8
+ constructor({ db, env }) {
9
+ this.db = db;
10
+ this.env = env;
11
+ }
12
+
13
+ mapUser(row) {
14
+ if (!row) {
15
+ return null;
16
+ }
17
+
18
+ return {
19
+ id: row.id,
20
+ role: row.role,
21
+ username: row.username,
22
+ display_name: row.display_name,
23
+ status: row.status,
24
+ is_builtin: Boolean(row.is_builtin),
25
+ must_change_password: Boolean(row.must_change_password),
26
+ tags: jsonParse(row.tags_json)
27
+ };
28
+ }
29
+
30
+ getLocalApiUser() {
31
+ const row = this.db.prepare(`
32
+ SELECT *
33
+ FROM users
34
+ WHERE username = 'aios'
35
+ ORDER BY id
36
+ LIMIT 1
37
+ `).get();
38
+
39
+ if (row) {
40
+ return this.mapUser(row);
41
+ }
42
+
43
+ const fallback = this.db.prepare(`
44
+ SELECT *
45
+ FROM users
46
+ WHERE role = ? AND status = 'active'
47
+ ORDER BY is_builtin DESC, id ASC
48
+ LIMIT 1
49
+ `).get(ADMIN_ROLE);
50
+
51
+ return this.mapUser(fallback);
52
+ }
53
+
54
+ createSession(userId) {
55
+ const now = new Date();
56
+ const expiresAt = new Date(now.getTime() + this.env.sessionTtlHours * 60 * 60 * 1000);
57
+ const token = newToken();
58
+ this.db.prepare(`
59
+ INSERT INTO sessions (token, user_id, expires_at, created_at)
60
+ VALUES (?, ?, ?, ?)
61
+ `).run(token, userId, expiresAt.toISOString(), now.toISOString());
62
+ return token;
63
+ }
64
+
65
+ cleanupExpiredSessions() {
66
+ this.db.prepare("DELETE FROM sessions WHERE expires_at < ?").run(new Date().toISOString());
67
+ }
68
+
69
+ login({ username, password }) {
70
+ if (!username || !password) {
71
+ throw badRequest("请输入用户名和密码");
72
+ }
73
+
74
+ this.cleanupExpiredSessions();
75
+ const row = this.db.prepare("SELECT * FROM users WHERE username = ?").get(username);
76
+ if (!row || row.role !== ADMIN_ROLE || row.status !== "active" || !verifyPassword(password, row.password_hash)) {
77
+ throw unauthorized("用户名或密码错误");
78
+ }
79
+
80
+ const token = this.createSession(row.id);
81
+ return {
82
+ token,
83
+ user: this.mapUser(row)
84
+ };
85
+ }
86
+
87
+ getSessionUser(token) {
88
+ this.cleanupExpiredSessions();
89
+ const row = this.db.prepare(`
90
+ SELECT u.*, s.token
91
+ FROM sessions s
92
+ JOIN users u ON u.id = s.user_id
93
+ WHERE s.token = ? AND s.expires_at >= ?
94
+ `).get(token, new Date().toISOString());
95
+
96
+ if (!row) {
97
+ throw unauthorized();
98
+ }
99
+
100
+ return this.mapUser(row);
101
+ }
102
+
103
+ logout(token) {
104
+ this.db.prepare("DELETE FROM sessions WHERE token = ?").run(token);
105
+ }
106
+
107
+ assertAdmin(user) {
108
+ if (!user || user.role !== ADMIN_ROLE) {
109
+ throw forbidden();
110
+ }
111
+ }
112
+
113
+ changePassword(userId, { currentPassword, nextPassword }) {
114
+ const row = this.db.prepare("SELECT * FROM users WHERE id = ?").get(userId);
115
+ if (!row) {
116
+ throw notFound("用户不存在");
117
+ }
118
+
119
+ if (row.role !== ADMIN_ROLE) {
120
+ throw forbidden("仅AIOS管理员可以修改门户密码");
121
+ }
122
+
123
+ if (!verifyPassword(currentPassword, row.password_hash)) {
124
+ throw badRequest("当前密码不正确");
125
+ }
126
+
127
+ this.db.prepare(`
128
+ UPDATE users
129
+ SET password_hash = ?, must_change_password = 0, updated_at = ?
130
+ WHERE id = ?
131
+ `).run(hashPassword(nextPassword), new Date().toISOString(), userId);
132
+
133
+ return { ok: true };
134
+ }
135
+
136
+ resetPassword(targetUserId, nextPassword) {
137
+ const row = this.db.prepare("SELECT * FROM users WHERE id = ?").get(targetUserId);
138
+ if (!row) {
139
+ throw notFound("用户不存在");
140
+ }
141
+ if (row.is_builtin || row.username === "aios") {
142
+ throw conflict("默认AIOS管理员不支持重置密码");
143
+ }
144
+
145
+ this.db.prepare(`
146
+ UPDATE users
147
+ SET password_hash = ?, must_change_password = 1, updated_at = ?
148
+ WHERE id = ?
149
+ `).run(hashPassword(nextPassword), new Date().toISOString(), targetUserId);
150
+
151
+ return { ok: true };
152
+ }
153
+ }