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,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
|
+
}
|