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,501 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { DatabaseSync } from "node:sqlite";
|
|
4
|
+
import crypto from "node:crypto";
|
|
5
|
+
|
|
6
|
+
import { loadEnv } from "../config/env.js";
|
|
7
|
+
import { hashPassword } from "../utils/security.js";
|
|
8
|
+
|
|
9
|
+
const env = loadEnv();
|
|
10
|
+
const SCHEMA_VERSION = 1;
|
|
11
|
+
const DEFAULT_ACCESS_TOKEN = "sk-1234567890qwertyuiop";
|
|
12
|
+
const DEFAULT_PORTAL_NAME = "AIOS 管理控制台";
|
|
13
|
+
const DEFAULT_BRAND_SUBTITLE = "Your Company Name";
|
|
14
|
+
const DEFAULT_THEME_COLOR = "#07c160";
|
|
15
|
+
const DEFAULT_ADMIN_USERNAME = "aios";
|
|
16
|
+
const DEFAULT_ADMIN_DISPLAY_NAME = "AIOS 管理员";
|
|
17
|
+
const DEFAULT_ADMIN_PASSWORD = "123456";
|
|
18
|
+
|
|
19
|
+
if (!fs.existsSync(env.dataDir)) {
|
|
20
|
+
fs.mkdirSync(env.dataDir, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const dbPath = path.join(env.dataDir, "management-console.db");
|
|
24
|
+
const db = new DatabaseSync(dbPath);
|
|
25
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
26
|
+
db.exec("PRAGMA journal_mode = WAL;");
|
|
27
|
+
|
|
28
|
+
export function jsonStringify(value, fallback = []) {
|
|
29
|
+
return JSON.stringify(value ?? fallback);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function jsonParse(value, fallback = []) {
|
|
33
|
+
if (!value) {
|
|
34
|
+
return fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(value);
|
|
39
|
+
} catch {
|
|
40
|
+
return fallback;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function withTransaction(fn) {
|
|
45
|
+
db.exec("BEGIN");
|
|
46
|
+
try {
|
|
47
|
+
const result = fn();
|
|
48
|
+
db.exec("COMMIT");
|
|
49
|
+
return result;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
db.exec("ROLLBACK");
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const TRANSACTION_QUEUE = Symbol.for("aios.management.transactionQueue");
|
|
57
|
+
|
|
58
|
+
export async function withSerializedTransaction(database, fn) {
|
|
59
|
+
const previous = database[TRANSACTION_QUEUE] || Promise.resolve();
|
|
60
|
+
let releaseCurrent;
|
|
61
|
+
const current = new Promise((resolve) => {
|
|
62
|
+
releaseCurrent = resolve;
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
database[TRANSACTION_QUEUE] = previous.catch(() => {}).then(() => current);
|
|
66
|
+
await previous.catch(() => {});
|
|
67
|
+
|
|
68
|
+
database.exec("BEGIN");
|
|
69
|
+
try {
|
|
70
|
+
const result = fn();
|
|
71
|
+
database.exec("COMMIT");
|
|
72
|
+
return result;
|
|
73
|
+
} catch (error) {
|
|
74
|
+
try {
|
|
75
|
+
database.exec("ROLLBACK");
|
|
76
|
+
} catch {
|
|
77
|
+
// Ignore rollback failures if SQLite already aborted the transaction.
|
|
78
|
+
}
|
|
79
|
+
throw error;
|
|
80
|
+
} finally {
|
|
81
|
+
releaseCurrent();
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function newAccessToken() {
|
|
86
|
+
return `sk-${crypto.randomBytes(18).toString("base64url")}`;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function currentSchemaVersion() {
|
|
90
|
+
db.exec(`
|
|
91
|
+
CREATE TABLE IF NOT EXISTS schema_meta (
|
|
92
|
+
key TEXT PRIMARY KEY,
|
|
93
|
+
value TEXT NOT NULL
|
|
94
|
+
);
|
|
95
|
+
`);
|
|
96
|
+
|
|
97
|
+
const row = db.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").get();
|
|
98
|
+
return row ? Number(row.value) : 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function setSchemaVersion(version) {
|
|
102
|
+
db.prepare(`
|
|
103
|
+
INSERT INTO schema_meta (key, value)
|
|
104
|
+
VALUES ('schema_version', ?)
|
|
105
|
+
ON CONFLICT(key) DO UPDATE SET value = excluded.value
|
|
106
|
+
`).run(String(version));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function quoteIdentifier(name) {
|
|
110
|
+
return `"${String(name).replaceAll("\"", "\"\"")}"`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function resetSchema() {
|
|
114
|
+
const objects = db.prepare(`
|
|
115
|
+
SELECT type, name
|
|
116
|
+
FROM sqlite_master
|
|
117
|
+
WHERE name NOT LIKE 'sqlite_%'
|
|
118
|
+
AND name <> 'schema_meta'
|
|
119
|
+
AND type IN ('table', 'view')
|
|
120
|
+
ORDER BY CASE WHEN type = 'view' THEN 0 ELSE 1 END, name
|
|
121
|
+
`).all();
|
|
122
|
+
|
|
123
|
+
for (const object of objects) {
|
|
124
|
+
db.exec(`DROP ${object.type.toUpperCase()} IF EXISTS ${quoteIdentifier(object.name)};`);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function createStateTable(name) {
|
|
129
|
+
return `
|
|
130
|
+
CREATE TABLE ${name} (
|
|
131
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
132
|
+
status TEXT NOT NULL DEFAULT 'idle',
|
|
133
|
+
trigger_source TEXT,
|
|
134
|
+
started_at TEXT,
|
|
135
|
+
finished_at TEXT,
|
|
136
|
+
last_success_at TEXT,
|
|
137
|
+
error_message TEXT,
|
|
138
|
+
summary_json TEXT NOT NULL DEFAULT '{}',
|
|
139
|
+
created_at TEXT NOT NULL,
|
|
140
|
+
updated_at TEXT NOT NULL
|
|
141
|
+
);
|
|
142
|
+
`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function createSchema() {
|
|
146
|
+
db.exec(`
|
|
147
|
+
CREATE TABLE settings (
|
|
148
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
149
|
+
portal_name TEXT NOT NULL,
|
|
150
|
+
brand_subtitle TEXT NOT NULL,
|
|
151
|
+
theme_color TEXT NOT NULL,
|
|
152
|
+
created_at TEXT NOT NULL,
|
|
153
|
+
updated_at TEXT NOT NULL
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
CREATE TABLE access_tokens (
|
|
157
|
+
token TEXT PRIMARY KEY,
|
|
158
|
+
created_at TEXT NOT NULL,
|
|
159
|
+
updated_at TEXT NOT NULL
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
${createStateTable("usage_refresh_state")}
|
|
163
|
+
${createStateTable("agent_sync_state")}
|
|
164
|
+
${createStateTable("skill_sync_state")}
|
|
165
|
+
${createStateTable("template_sync_state")}
|
|
166
|
+
${createStateTable("system_sync_state")}
|
|
167
|
+
|
|
168
|
+
CREATE TABLE users (
|
|
169
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
170
|
+
role TEXT NOT NULL CHECK (role IN ('aios-admin')),
|
|
171
|
+
username TEXT NOT NULL UNIQUE,
|
|
172
|
+
display_name TEXT NOT NULL,
|
|
173
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'disabled')),
|
|
174
|
+
password_hash TEXT NOT NULL DEFAULT '',
|
|
175
|
+
must_change_password INTEGER NOT NULL DEFAULT 0,
|
|
176
|
+
is_builtin INTEGER NOT NULL DEFAULT 0,
|
|
177
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
178
|
+
created_at TEXT NOT NULL,
|
|
179
|
+
updated_at TEXT NOT NULL
|
|
180
|
+
);
|
|
181
|
+
|
|
182
|
+
CREATE TABLE sessions (
|
|
183
|
+
token TEXT PRIMARY KEY,
|
|
184
|
+
user_id INTEGER NOT NULL,
|
|
185
|
+
expires_at TEXT NOT NULL,
|
|
186
|
+
created_at TEXT NOT NULL,
|
|
187
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
CREATE TABLE aios_users (
|
|
191
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
192
|
+
username TEXT NOT NULL UNIQUE,
|
|
193
|
+
created_at TEXT NOT NULL,
|
|
194
|
+
updated_at TEXT NOT NULL
|
|
195
|
+
);
|
|
196
|
+
|
|
197
|
+
CREATE TABLE agents (
|
|
198
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
199
|
+
slug TEXT NOT NULL UNIQUE,
|
|
200
|
+
agent_name TEXT NOT NULL,
|
|
201
|
+
description TEXT NOT NULL,
|
|
202
|
+
docs_content TEXT NOT NULL DEFAULT '',
|
|
203
|
+
template_name TEXT NOT NULL DEFAULT 'default',
|
|
204
|
+
status TEXT NOT NULL CHECK (status IN ('normal', 'disabled', 'overlimit')),
|
|
205
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
206
|
+
daily_limit INTEGER NOT NULL,
|
|
207
|
+
usage_snapshot_json TEXT NOT NULL DEFAULT '{}',
|
|
208
|
+
remote_state_json TEXT NOT NULL DEFAULT '{}',
|
|
209
|
+
created_at TEXT NOT NULL,
|
|
210
|
+
updated_at TEXT NOT NULL
|
|
211
|
+
);
|
|
212
|
+
|
|
213
|
+
CREATE TABLE external_sessions (
|
|
214
|
+
session_id TEXT PRIMARY KEY,
|
|
215
|
+
aios_user_id INTEGER NOT NULL,
|
|
216
|
+
agent_id INTEGER NOT NULL,
|
|
217
|
+
created_at TEXT NOT NULL,
|
|
218
|
+
updated_at TEXT NOT NULL,
|
|
219
|
+
UNIQUE (aios_user_id, agent_id),
|
|
220
|
+
FOREIGN KEY (aios_user_id) REFERENCES aios_users(id) ON DELETE CASCADE,
|
|
221
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
CREATE TABLE external_session_cookies (
|
|
225
|
+
session_id TEXT NOT NULL,
|
|
226
|
+
provider TEXT NOT NULL CHECK (provider IN ('hzg', 'phx')),
|
|
227
|
+
cookie TEXT NOT NULL CHECK (length(cookie) <= 16384),
|
|
228
|
+
created_at TEXT NOT NULL,
|
|
229
|
+
updated_at TEXT NOT NULL,
|
|
230
|
+
PRIMARY KEY (session_id, provider),
|
|
231
|
+
FOREIGN KEY (session_id) REFERENCES external_sessions(session_id) ON DELETE CASCADE
|
|
232
|
+
);
|
|
233
|
+
|
|
234
|
+
CREATE TABLE artifacts (
|
|
235
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
236
|
+
kind TEXT NOT NULL,
|
|
237
|
+
bucket TEXT NOT NULL,
|
|
238
|
+
object_key TEXT NOT NULL,
|
|
239
|
+
original_name TEXT NOT NULL,
|
|
240
|
+
mime_type TEXT NOT NULL,
|
|
241
|
+
byte_size INTEGER NOT NULL,
|
|
242
|
+
created_by INTEGER,
|
|
243
|
+
created_at TEXT NOT NULL,
|
|
244
|
+
FOREIGN KEY (created_by) REFERENCES users(id) ON DELETE SET NULL
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
CREATE TABLE agent_templates (
|
|
248
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
249
|
+
template_name TEXT NOT NULL UNIQUE,
|
|
250
|
+
description TEXT NOT NULL,
|
|
251
|
+
artifact_id INTEGER NOT NULL,
|
|
252
|
+
remote_status TEXT NOT NULL DEFAULT 'pending',
|
|
253
|
+
remote_result_json TEXT,
|
|
254
|
+
is_builtin INTEGER NOT NULL DEFAULT 0,
|
|
255
|
+
created_at TEXT NOT NULL,
|
|
256
|
+
updated_at TEXT NOT NULL,
|
|
257
|
+
FOREIGN KEY (artifact_id) REFERENCES artifacts(id) ON DELETE RESTRICT
|
|
258
|
+
);
|
|
259
|
+
|
|
260
|
+
CREATE TABLE skills (
|
|
261
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
262
|
+
slug TEXT NOT NULL UNIQUE,
|
|
263
|
+
description TEXT NOT NULL,
|
|
264
|
+
artifact_id INTEGER,
|
|
265
|
+
remote_status TEXT NOT NULL DEFAULT 'cataloged',
|
|
266
|
+
is_builtin INTEGER NOT NULL DEFAULT 0,
|
|
267
|
+
created_at TEXT NOT NULL,
|
|
268
|
+
updated_at TEXT NOT NULL,
|
|
269
|
+
FOREIGN KEY (artifact_id) REFERENCES artifacts(id) ON DELETE SET NULL
|
|
270
|
+
);
|
|
271
|
+
|
|
272
|
+
CREATE TABLE agent_permissions (
|
|
273
|
+
agent_id INTEGER NOT NULL,
|
|
274
|
+
aios_user_id INTEGER NOT NULL,
|
|
275
|
+
PRIMARY KEY (agent_id, aios_user_id),
|
|
276
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
|
|
277
|
+
FOREIGN KEY (aios_user_id) REFERENCES aios_users(id) ON DELETE CASCADE
|
|
278
|
+
);
|
|
279
|
+
|
|
280
|
+
CREATE TABLE agent_skill_bindings (
|
|
281
|
+
agent_id INTEGER NOT NULL,
|
|
282
|
+
skill_id INTEGER NOT NULL,
|
|
283
|
+
PRIMARY KEY (agent_id, skill_id),
|
|
284
|
+
FOREIGN KEY (agent_id) REFERENCES agents(id) ON DELETE CASCADE,
|
|
285
|
+
FOREIGN KEY (skill_id) REFERENCES skills(id) ON DELETE CASCADE
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
CREATE TABLE business_systems (
|
|
289
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
290
|
+
provider TEXT NOT NULL CHECK (provider IN ('hzg', 'phx')),
|
|
291
|
+
application_name TEXT NOT NULL UNIQUE,
|
|
292
|
+
description TEXT NOT NULL,
|
|
293
|
+
ontology_artifact_id INTEGER,
|
|
294
|
+
scheme TEXT NOT NULL CHECK (scheme IN ('http', 'https')),
|
|
295
|
+
host TEXT NOT NULL,
|
|
296
|
+
port INTEGER NOT NULL,
|
|
297
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'disabled')),
|
|
298
|
+
last_connectivity_test_status TEXT NOT NULL DEFAULT 'unknown',
|
|
299
|
+
last_connectivity_test_result_json TEXT NOT NULL DEFAULT '{}',
|
|
300
|
+
is_builtin INTEGER NOT NULL DEFAULT 0,
|
|
301
|
+
created_at TEXT NOT NULL,
|
|
302
|
+
updated_at TEXT NOT NULL,
|
|
303
|
+
FOREIGN KEY (ontology_artifact_id) REFERENCES artifacts(id) ON DELETE SET NULL
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
CREATE TABLE audit_logs (
|
|
307
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
308
|
+
user_id INTEGER,
|
|
309
|
+
username TEXT NOT NULL,
|
|
310
|
+
action TEXT NOT NULL,
|
|
311
|
+
detail TEXT NOT NULL,
|
|
312
|
+
created_at TEXT NOT NULL,
|
|
313
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL
|
|
314
|
+
);
|
|
315
|
+
|
|
316
|
+
CREATE TABLE system_invocation_logs (
|
|
317
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
318
|
+
trace_id TEXT NOT NULL UNIQUE,
|
|
319
|
+
agent_slug TEXT,
|
|
320
|
+
session_id TEXT,
|
|
321
|
+
provider TEXT NOT NULL,
|
|
322
|
+
application_name TEXT NOT NULL,
|
|
323
|
+
command_name TEXT NOT NULL,
|
|
324
|
+
request_payload_json TEXT NOT NULL,
|
|
325
|
+
response_payload_json TEXT NOT NULL,
|
|
326
|
+
response_time_ms INTEGER NOT NULL,
|
|
327
|
+
success INTEGER NOT NULL,
|
|
328
|
+
error_message TEXT,
|
|
329
|
+
created_at TEXT NOT NULL
|
|
330
|
+
);
|
|
331
|
+
|
|
332
|
+
CREATE INDEX idx_system_invocation_logs_session_id
|
|
333
|
+
ON system_invocation_logs(session_id);
|
|
334
|
+
|
|
335
|
+
CREATE TABLE management_requests (
|
|
336
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
337
|
+
request_id TEXT NOT NULL UNIQUE,
|
|
338
|
+
action TEXT NOT NULL,
|
|
339
|
+
params_json TEXT NOT NULL,
|
|
340
|
+
ok INTEGER,
|
|
341
|
+
result_json TEXT,
|
|
342
|
+
error_json TEXT,
|
|
343
|
+
created_at TEXT NOT NULL,
|
|
344
|
+
completed_at TEXT
|
|
345
|
+
);
|
|
346
|
+
`);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
function seedStateRow(tableName, now) {
|
|
350
|
+
db.prepare(`
|
|
351
|
+
INSERT INTO ${tableName} (
|
|
352
|
+
id, status, trigger_source, started_at, finished_at, last_success_at,
|
|
353
|
+
error_message, summary_json, created_at, updated_at
|
|
354
|
+
) VALUES (1, 'idle', NULL, NULL, NULL, NULL, NULL, '{}', ?, ?)
|
|
355
|
+
`).run(now, now);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
function seed() {
|
|
359
|
+
const now = new Date().toISOString();
|
|
360
|
+
|
|
361
|
+
db.prepare(`
|
|
362
|
+
INSERT INTO settings (
|
|
363
|
+
id, portal_name, brand_subtitle, theme_color, created_at, updated_at
|
|
364
|
+
) VALUES (1, ?, ?, ?, ?, ?)
|
|
365
|
+
`).run(
|
|
366
|
+
DEFAULT_PORTAL_NAME,
|
|
367
|
+
DEFAULT_BRAND_SUBTITLE,
|
|
368
|
+
DEFAULT_THEME_COLOR,
|
|
369
|
+
now,
|
|
370
|
+
now
|
|
371
|
+
);
|
|
372
|
+
|
|
373
|
+
db.prepare(`
|
|
374
|
+
INSERT INTO access_tokens (
|
|
375
|
+
token, created_at, updated_at
|
|
376
|
+
) VALUES (?, ?, ?)
|
|
377
|
+
`).run(DEFAULT_ACCESS_TOKEN, now, now);
|
|
378
|
+
|
|
379
|
+
for (const tableName of [
|
|
380
|
+
"usage_refresh_state",
|
|
381
|
+
"agent_sync_state",
|
|
382
|
+
"skill_sync_state",
|
|
383
|
+
"template_sync_state",
|
|
384
|
+
"system_sync_state"
|
|
385
|
+
]) {
|
|
386
|
+
seedStateRow(tableName, now);
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
db.prepare(`
|
|
390
|
+
INSERT INTO users (
|
|
391
|
+
role, username, display_name, status, password_hash,
|
|
392
|
+
must_change_password, is_builtin, tags_json, created_at, updated_at
|
|
393
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
394
|
+
`).run(
|
|
395
|
+
"aios-admin",
|
|
396
|
+
DEFAULT_ADMIN_USERNAME,
|
|
397
|
+
DEFAULT_ADMIN_DISPLAY_NAME,
|
|
398
|
+
"active",
|
|
399
|
+
hashPassword(DEFAULT_ADMIN_PASSWORD),
|
|
400
|
+
1,
|
|
401
|
+
1,
|
|
402
|
+
jsonStringify(["builtin", "ops"]),
|
|
403
|
+
now,
|
|
404
|
+
now
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function ensureCoreRows() {
|
|
409
|
+
const now = new Date().toISOString();
|
|
410
|
+
|
|
411
|
+
db.prepare(`
|
|
412
|
+
INSERT INTO settings (
|
|
413
|
+
id, portal_name, brand_subtitle, theme_color, created_at, updated_at
|
|
414
|
+
) VALUES (1, ?, ?, ?, ?, ?)
|
|
415
|
+
ON CONFLICT(id) DO NOTHING
|
|
416
|
+
`).run(
|
|
417
|
+
DEFAULT_PORTAL_NAME,
|
|
418
|
+
DEFAULT_BRAND_SUBTITLE,
|
|
419
|
+
DEFAULT_THEME_COLOR,
|
|
420
|
+
now,
|
|
421
|
+
now
|
|
422
|
+
);
|
|
423
|
+
|
|
424
|
+
const accessTokenCount = Number(
|
|
425
|
+
db.prepare("SELECT COUNT(*) AS count FROM access_tokens").get()?.count || 0
|
|
426
|
+
);
|
|
427
|
+
if (accessTokenCount === 0) {
|
|
428
|
+
db.prepare(`
|
|
429
|
+
INSERT INTO access_tokens (
|
|
430
|
+
token, created_at, updated_at
|
|
431
|
+
) VALUES (?, ?, ?)
|
|
432
|
+
`).run(DEFAULT_ACCESS_TOKEN, now, now);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
for (const tableName of [
|
|
436
|
+
"usage_refresh_state",
|
|
437
|
+
"agent_sync_state",
|
|
438
|
+
"skill_sync_state",
|
|
439
|
+
"template_sync_state",
|
|
440
|
+
"system_sync_state"
|
|
441
|
+
]) {
|
|
442
|
+
db.prepare(`
|
|
443
|
+
INSERT INTO ${tableName} (
|
|
444
|
+
id, status, trigger_source, started_at, finished_at, last_success_at,
|
|
445
|
+
error_message, summary_json, created_at, updated_at
|
|
446
|
+
) VALUES (1, 'idle', NULL, NULL, NULL, NULL, NULL, '{}', ?, ?)
|
|
447
|
+
ON CONFLICT(id) DO NOTHING
|
|
448
|
+
`).run(now, now);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
db.prepare(`
|
|
452
|
+
INSERT INTO users (
|
|
453
|
+
role, username, display_name, status, password_hash,
|
|
454
|
+
must_change_password, is_builtin, tags_json, created_at, updated_at
|
|
455
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
456
|
+
ON CONFLICT(username) DO UPDATE SET
|
|
457
|
+
role = 'aios-admin',
|
|
458
|
+
display_name = COALESCE(NULLIF(users.display_name, ''), excluded.display_name),
|
|
459
|
+
status = 'active',
|
|
460
|
+
is_builtin = 1,
|
|
461
|
+
updated_at = excluded.updated_at
|
|
462
|
+
`).run(
|
|
463
|
+
"aios-admin",
|
|
464
|
+
DEFAULT_ADMIN_USERNAME,
|
|
465
|
+
DEFAULT_ADMIN_DISPLAY_NAME,
|
|
466
|
+
"active",
|
|
467
|
+
hashPassword(DEFAULT_ADMIN_PASSWORD),
|
|
468
|
+
1,
|
|
469
|
+
1,
|
|
470
|
+
jsonStringify(["builtin", "ops"]),
|
|
471
|
+
now,
|
|
472
|
+
now
|
|
473
|
+
);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
function migrate() {
|
|
477
|
+
const version = currentSchemaVersion();
|
|
478
|
+
|
|
479
|
+
if (version !== SCHEMA_VERSION) {
|
|
480
|
+
db.exec("PRAGMA foreign_keys = OFF;");
|
|
481
|
+
db.exec("BEGIN");
|
|
482
|
+
try {
|
|
483
|
+
resetSchema();
|
|
484
|
+
createSchema();
|
|
485
|
+
seed();
|
|
486
|
+
setSchemaVersion(SCHEMA_VERSION);
|
|
487
|
+
db.exec("COMMIT");
|
|
488
|
+
} catch (error) {
|
|
489
|
+
db.exec("ROLLBACK");
|
|
490
|
+
throw error;
|
|
491
|
+
} finally {
|
|
492
|
+
db.exec("PRAGMA foreign_keys = ON;");
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
ensureCoreRows();
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
migrate();
|
|
500
|
+
|
|
501
|
+
export { db, dbPath };
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { EventEmitter } from "node:events";
|
|
3
|
+
|
|
4
|
+
import mqtt from "mqtt";
|
|
5
|
+
|
|
6
|
+
import { serviceUnavailable } from "../../utils/errors.js";
|
|
7
|
+
|
|
8
|
+
export class ManagementRpcClient extends EventEmitter {
|
|
9
|
+
constructor({ env, db, mqttFactory = mqtt }) {
|
|
10
|
+
super();
|
|
11
|
+
this.env = env;
|
|
12
|
+
this.db = db;
|
|
13
|
+
this.mqttFactory = mqttFactory;
|
|
14
|
+
this.client = null;
|
|
15
|
+
this.pending = new Map();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
isConfigured() {
|
|
19
|
+
return Boolean(
|
|
20
|
+
this.env.mqtt.brokerUrl &&
|
|
21
|
+
this.env.mqtt.adminInboundTopic &&
|
|
22
|
+
this.env.mqtt.adminOutboundTopic
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async start() {
|
|
27
|
+
if (!this.isConfigured()) {
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (this.client) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const connect = this.mqttFactory.connect || this.mqttFactory;
|
|
36
|
+
this.client = connect(this.env.mqtt.brokerUrl, {
|
|
37
|
+
clientId: `aios-web-admin-${randomUUID().slice(0, 8)}`,
|
|
38
|
+
username: this.env.mqtt.username || undefined,
|
|
39
|
+
password: this.env.mqtt.password || undefined,
|
|
40
|
+
clean: true,
|
|
41
|
+
reconnectPeriod: 2000
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
await new Promise((resolve, reject) => {
|
|
45
|
+
const cleanup = () => {
|
|
46
|
+
this.client?.off("connect", onConnect);
|
|
47
|
+
this.client?.off("error", onError);
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
const onConnect = () => {
|
|
51
|
+
cleanup();
|
|
52
|
+
resolve();
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
const onError = (error) => {
|
|
56
|
+
cleanup();
|
|
57
|
+
reject(error);
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
this.client.once("connect", onConnect);
|
|
61
|
+
this.client.once("error", onError);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await new Promise((resolve, reject) => {
|
|
65
|
+
this.client.subscribe(this.env.mqtt.adminOutboundTopic, { qos: 1 }, (error) => {
|
|
66
|
+
if (error) {
|
|
67
|
+
reject(error);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
resolve();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
this.client.on("message", (_topic, payload) => {
|
|
76
|
+
try {
|
|
77
|
+
const message = JSON.parse(payload.toString("utf8"));
|
|
78
|
+
this.emit("outbound_raw_message", message);
|
|
79
|
+
if (!message?.requestId) {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const pending = this.pending.get(message.requestId);
|
|
84
|
+
if (!pending) {
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
clearTimeout(pending.timer);
|
|
89
|
+
this.pending.delete(message.requestId);
|
|
90
|
+
this.emit("outbound_message", {
|
|
91
|
+
...message,
|
|
92
|
+
request: {
|
|
93
|
+
action: pending.action,
|
|
94
|
+
params: pending.params
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
this.finishLog(message);
|
|
98
|
+
if (message.ok) {
|
|
99
|
+
pending.resolve(message.result);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
pending.reject(
|
|
104
|
+
serviceUnavailable(message.error?.message || "Management CLI returned an error", message.error)
|
|
105
|
+
);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
console.error("Failed to process management response", error);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async stop() {
|
|
113
|
+
if (!this.client) {
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const [requestId, pending] of this.pending.entries()) {
|
|
118
|
+
clearTimeout(pending.timer);
|
|
119
|
+
this.failLog(requestId, { message: "管理 RPC 客户端已停止" });
|
|
120
|
+
pending.reject(serviceUnavailable("管理 RPC 客户端已停止"));
|
|
121
|
+
}
|
|
122
|
+
this.pending.clear();
|
|
123
|
+
|
|
124
|
+
await new Promise((resolve) => {
|
|
125
|
+
this.client.end(false, {}, () => resolve());
|
|
126
|
+
});
|
|
127
|
+
this.client = null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
recordRequest(requestId, action, params) {
|
|
131
|
+
this.db.prepare(`
|
|
132
|
+
INSERT INTO management_requests (
|
|
133
|
+
request_id, action, params_json, created_at
|
|
134
|
+
) VALUES (?, ?, ?, ?)
|
|
135
|
+
`).run(requestId, action, JSON.stringify(params ?? {}), new Date().toISOString());
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
finishLog(response) {
|
|
139
|
+
this.db.prepare(`
|
|
140
|
+
UPDATE management_requests
|
|
141
|
+
SET ok = ?, result_json = ?, error_json = ?, completed_at = ?
|
|
142
|
+
WHERE request_id = ?
|
|
143
|
+
`).run(
|
|
144
|
+
response.ok ? 1 : 0,
|
|
145
|
+
response.result === undefined ? null : JSON.stringify(response.result),
|
|
146
|
+
response.error === undefined ? null : JSON.stringify(response.error),
|
|
147
|
+
new Date().toISOString(),
|
|
148
|
+
response.requestId
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
failLog(requestId, error) {
|
|
153
|
+
this.db.prepare(`
|
|
154
|
+
UPDATE management_requests
|
|
155
|
+
SET ok = 0, error_json = ?, completed_at = ?
|
|
156
|
+
WHERE request_id = ?
|
|
157
|
+
`).run(
|
|
158
|
+
JSON.stringify(error ?? { message: "Management RPC failed" }),
|
|
159
|
+
new Date().toISOString(),
|
|
160
|
+
requestId
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async call(action, params = {}, timeoutMs = this.env.managementTimeoutMs || 120000) {
|
|
165
|
+
if (!this.client || !this.isConfigured()) {
|
|
166
|
+
throw serviceUnavailable("管理端 MQTT 桥接未配置");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
const requestId = randomUUID();
|
|
170
|
+
const message = {
|
|
171
|
+
requestId,
|
|
172
|
+
action,
|
|
173
|
+
params
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
this.recordRequest(requestId, action, params);
|
|
177
|
+
|
|
178
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
179
|
+
const timer = setTimeout(() => {
|
|
180
|
+
this.pending.delete(requestId);
|
|
181
|
+
this.failLog(requestId, { message: `管理动作超时:${action}`, timeout: true, action });
|
|
182
|
+
reject(serviceUnavailable(`管理动作超时:${action}`, { timeout: true, action }));
|
|
183
|
+
}, timeoutMs);
|
|
184
|
+
|
|
185
|
+
this.pending.set(requestId, { resolve, reject, timer, action, params });
|
|
186
|
+
});
|
|
187
|
+
resultPromise.catch(() => {});
|
|
188
|
+
|
|
189
|
+
await new Promise((resolve, reject) => {
|
|
190
|
+
this.client.publish(
|
|
191
|
+
this.env.mqtt.adminInboundTopic,
|
|
192
|
+
JSON.stringify(message),
|
|
193
|
+
{ qos: 1, retain: false },
|
|
194
|
+
(error) => {
|
|
195
|
+
if (error) {
|
|
196
|
+
const pending = this.pending.get(requestId);
|
|
197
|
+
if (pending) {
|
|
198
|
+
clearTimeout(pending.timer);
|
|
199
|
+
this.pending.delete(requestId);
|
|
200
|
+
}
|
|
201
|
+
this.failLog(requestId, { message: error.message || "管理请求发布失败" });
|
|
202
|
+
reject(serviceUnavailable("发布管理请求失败", error));
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
resolve();
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
return resultPromise;
|
|
212
|
+
}
|
|
213
|
+
}
|