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.
- package/.env.json +21 -0
- package/README.md +257 -0
- package/data/management-console.db +0 -0
- package/data/management-console.db-shm +0 -0
- package/data/management-console.db-wal +0 -0
- package/dist/assets/index-CV_wjCAG.js +464 -0
- package/dist/assets/index-DfMPB0eV.css +1 -0
- package/dist/index.html +13 -0
- package/docs/spec.md +199 -0
- package/index.html +12 -0
- package/package.json +37 -0
- package/scripts/reset-kernel.js +59 -0
- package/scripts/reset-password.js +22 -0
- package/server/fakes.js +57 -0
- package/server/index.js +21 -0
- package/server/src/api/middleware/auth.js +29 -0
- package/server/src/api/middleware/internal.js +44 -0
- package/server/src/api/routes/index.js +677 -0
- package/server/src/app.js +90 -0
- package/server/src/background/index.js +106 -0
- package/server/src/background/protocol.js +15 -0
- package/server/src/config/env.js +90 -0
- package/server/src/db/index.js +501 -0
- package/server/src/infra/mqtt/management-rpc-client.js +213 -0
- package/server/src/infra/providers/hzg-provider-client.js +39 -0
- package/server/src/infra/s3/object-storage.js +97 -0
- package/server/src/services/agent-quota.js +54 -0
- package/server/src/services/agent-service.js +696 -0
- package/server/src/services/agent-status-sync-service.js +132 -0
- package/server/src/services/audit-log-service.js +39 -0
- package/server/src/services/auth-service.js +153 -0
- package/server/src/services/catalog-sync-service.js +712 -0
- package/server/src/services/external-service.js +308 -0
- package/server/src/services/kernel-reset-service.js +86 -0
- package/server/src/services/portal-service.js +555 -0
- package/server/src/services/system-service.js +580 -0
- package/server/src/services/topic-ping-service.js +282 -0
- package/server/src/utils/errors.js +36 -0
- package/server/src/utils/security.js +22 -0
- package/server/test/agent-service-alignment.test.js +316 -0
- package/server/test/agent-service-create.test.js +662 -0
- package/server/test/agent-status-sync-service.test.js +167 -0
- package/server/test/agent-update-audit.test.js +63 -0
- package/server/test/auth-middleware.test.js +71 -0
- package/server/test/background-services.test.js +160 -0
- package/server/test/catalog-sync-service.test.js +920 -0
- package/server/test/db-reset-migration.test.js +123 -0
- package/server/test/env-config.test.js +68 -0
- package/server/test/external-service.test.js +380 -0
- package/server/test/hzg-provider-client.test.js +50 -0
- package/server/test/internal-auth-middleware.test.js +66 -0
- package/server/test/kernel-reset-service.test.js +112 -0
- package/server/test/management-rpc-client.test.js +105 -0
- package/server/test/portal-service-access-tokens.test.js +121 -0
- package/server/test/portal-service-alignment.test.js +318 -0
- package/server/test/portal-service-management-logs.test.js +114 -0
- package/server/test/reset-kernel-cli.test.js +23 -0
- package/server/test/service-api-auth-middleware.test.js +59 -0
- package/server/test/system-service-alignment.test.js +265 -0
- package/server/test/topic-ping-service.test.js +182 -0
- package/server/test/usage-refresh-audit-route.test.js +82 -0
- package/src/App.jsx +1 -0
- package/src/api.js +1 -0
- package/src/app/App.jsx +346 -0
- package/src/app/api-client.js +112 -0
- package/src/components/AppShell.jsx +117 -0
- package/src/components/CardTitleWithReload.jsx +20 -0
- package/src/components/DeleteActionButton.jsx +31 -0
- package/src/main.jsx +14 -0
- package/src/pages/AgentsPage.jsx +647 -0
- package/src/pages/AiosUsersPage.jsx +151 -0
- package/src/pages/DashboardPage.jsx +72 -0
- package/src/pages/LoginPage.jsx +41 -0
- package/src/pages/SettingsPage.jsx +431 -0
- package/src/pages/SkillsPage.jsx +175 -0
- package/src/pages/SystemLogsPage.jsx +349 -0
- package/src/pages/SystemsPage.jsx +498 -0
- package/src/pages/TemplatesPage.jsx +207 -0
- package/src/pages/UserManagementPage.jsx +25 -0
- package/src/pages/UsersPage.jsx +192 -0
- package/src/pages/system-logs/SystemLogsTabs.jsx +362 -0
- package/src/styles.css +222 -0
- package/src/utils/format.js +63 -0
- package/test/.reports/fast-2026-05-25T08-32-39-420Z.json +299 -0
- package/test/integration/common.js +208 -0
- package/test/integration/fast.js +135 -0
- package/test/integration/full.js +306 -0
- package/test/run-browser-e2e.js +212 -0
- package/test/run-jasmine.js +21 -0
- package/test/setup.js +1 -0
- 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
|
+
}
|