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,123 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { DatabaseSync } from "node:sqlite";
|
|
5
|
+
|
|
6
|
+
it("resets drifted legacy schemas to the latest database structure", async () => {
|
|
7
|
+
const previousDataDir = process.env.AIOS_WEB_DATA_DIR;
|
|
8
|
+
const dataDir = fs.mkdtempSync(path.join(os.tmpdir(), "aios-web-db-reset-test-"));
|
|
9
|
+
const dbFile = path.join(dataDir, "management-console.db");
|
|
10
|
+
const setupDb = new DatabaseSync(dbFile);
|
|
11
|
+
|
|
12
|
+
setupDb.exec(`
|
|
13
|
+
CREATE TABLE schema_meta (
|
|
14
|
+
key TEXT PRIMARY KEY,
|
|
15
|
+
value TEXT NOT NULL
|
|
16
|
+
);
|
|
17
|
+
INSERT INTO schema_meta (key, value) VALUES ('schema_version', '14');
|
|
18
|
+
|
|
19
|
+
CREATE TABLE users (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
role TEXT NOT NULL,
|
|
22
|
+
username TEXT NOT NULL UNIQUE,
|
|
23
|
+
display_name TEXT NOT NULL,
|
|
24
|
+
status TEXT NOT NULL,
|
|
25
|
+
password_hash TEXT NOT NULL DEFAULT '',
|
|
26
|
+
must_change_password INTEGER NOT NULL DEFAULT 0,
|
|
27
|
+
is_builtin INTEGER NOT NULL DEFAULT 0,
|
|
28
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
29
|
+
created_at TEXT NOT NULL,
|
|
30
|
+
updated_at TEXT NOT NULL
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
CREATE TABLE agents (
|
|
34
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
35
|
+
slug TEXT NOT NULL UNIQUE,
|
|
36
|
+
agent_name TEXT NOT NULL,
|
|
37
|
+
description TEXT NOT NULL,
|
|
38
|
+
docs_content TEXT NOT NULL DEFAULT '',
|
|
39
|
+
template_name TEXT NOT NULL,
|
|
40
|
+
status TEXT NOT NULL CHECK (status IN ('active', 'disabled')),
|
|
41
|
+
tags_json TEXT NOT NULL DEFAULT '[]',
|
|
42
|
+
daily_limit INTEGER NOT NULL,
|
|
43
|
+
usage_snapshot_json TEXT NOT NULL DEFAULT '{}',
|
|
44
|
+
remote_state_json TEXT NOT NULL DEFAULT '{}',
|
|
45
|
+
created_at TEXT NOT NULL,
|
|
46
|
+
updated_at TEXT NOT NULL
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
CREATE TABLE agents_legacy_v14 (
|
|
50
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
CREATE TABLE agent_permissions (
|
|
54
|
+
agent_id INTEGER NOT NULL,
|
|
55
|
+
user_id INTEGER NOT NULL,
|
|
56
|
+
PRIMARY KEY (agent_id, user_id),
|
|
57
|
+
FOREIGN KEY (agent_id) REFERENCES agents_legacy_v14(id) ON DELETE CASCADE,
|
|
58
|
+
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
|
59
|
+
);
|
|
60
|
+
`);
|
|
61
|
+
setupDb.close();
|
|
62
|
+
|
|
63
|
+
process.env.AIOS_WEB_DATA_DIR = dataDir;
|
|
64
|
+
const moduleUrl = new URL(`../src/db/index.js?case=${Date.now()}`, import.meta.url);
|
|
65
|
+
const { db } = await import(moduleUrl.href);
|
|
66
|
+
|
|
67
|
+
const schemaVersion = db.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").get();
|
|
68
|
+
const permissionColumns = db.prepare("PRAGMA table_info(agent_permissions)").all().map((row) => row.name);
|
|
69
|
+
const skillColumns = db.prepare("PRAGMA table_info(skills)").all().map((row) => row.name);
|
|
70
|
+
const systemColumns = db.prepare("PRAGMA table_info(business_systems)").all().map((row) => row.name);
|
|
71
|
+
const invocationColumns = db.prepare("PRAGMA table_info(system_invocation_logs)").all().map((row) => row.name);
|
|
72
|
+
const sessionCookieColumns = db.prepare("PRAGMA table_info(external_session_cookies)").all();
|
|
73
|
+
const externalSessionColumns = db.prepare("PRAGMA table_info(external_sessions)").all();
|
|
74
|
+
const aiosUsersTable = db.prepare(`
|
|
75
|
+
SELECT name
|
|
76
|
+
FROM sqlite_master
|
|
77
|
+
WHERE type = 'table' AND name = 'aios_users'
|
|
78
|
+
`).get();
|
|
79
|
+
const sessionIdIndexes = db.prepare(`
|
|
80
|
+
SELECT name
|
|
81
|
+
FROM sqlite_master
|
|
82
|
+
WHERE type = 'index' AND name = 'idx_system_invocation_logs_session_id'
|
|
83
|
+
`).get();
|
|
84
|
+
const legacyTables = db.prepare(`
|
|
85
|
+
SELECT name
|
|
86
|
+
FROM sqlite_master
|
|
87
|
+
WHERE type = 'table' AND name LIKE '%legacy%'
|
|
88
|
+
ORDER BY name
|
|
89
|
+
`).all().map((row) => row.name);
|
|
90
|
+
|
|
91
|
+
expect(schemaVersion?.value).toBe("1");
|
|
92
|
+
expect(aiosUsersTable?.name).toBe("aios_users");
|
|
93
|
+
expect(permissionColumns).toContain("aios_user_id");
|
|
94
|
+
expect(permissionColumns).not.toContain("directory_user_id");
|
|
95
|
+
expect(permissionColumns).not.toContain("user_id");
|
|
96
|
+
expect(skillColumns).not.toContain("remote_slug");
|
|
97
|
+
expect(skillColumns).not.toContain("source_type");
|
|
98
|
+
expect(skillColumns).not.toContain("distribution_scope");
|
|
99
|
+
expect(skillColumns).toContain("is_builtin");
|
|
100
|
+
expect(systemColumns).toContain("scheme");
|
|
101
|
+
expect(systemColumns).not.toContain("schema");
|
|
102
|
+
expect(systemColumns).toContain("last_connectivity_test_status");
|
|
103
|
+
expect(systemColumns).toContain("last_connectivity_test_result_json");
|
|
104
|
+
expect(systemColumns).not.toContain("name");
|
|
105
|
+
expect(systemColumns).not.toContain("connectivity_status");
|
|
106
|
+
expect(systemColumns).not.toContain("last_test_result_json");
|
|
107
|
+
expect(invocationColumns).not.toContain("system_name");
|
|
108
|
+
expect(sessionCookieColumns.find((column) => column.name === "cookie")?.notnull).toBe(1);
|
|
109
|
+
expect(externalSessionColumns.find((column) => column.name === "session_id")?.pk).toBe(1);
|
|
110
|
+
expect(sessionIdIndexes?.name).toBe("idx_system_invocation_logs_session_id");
|
|
111
|
+
expect(() => db.prepare(`
|
|
112
|
+
INSERT INTO external_session_cookies (
|
|
113
|
+
session_id, provider, cookie, created_at, updated_at
|
|
114
|
+
) VALUES (?, ?, ?, ?, ?)
|
|
115
|
+
`).run("missing", "hzg", "a".repeat(16385), "2026-05-26T00:00:00.000Z", "2026-05-26T00:00:00.000Z")).toThrow();
|
|
116
|
+
expect(legacyTables).toEqual([]);
|
|
117
|
+
|
|
118
|
+
if (previousDataDir === undefined) {
|
|
119
|
+
delete process.env.AIOS_WEB_DATA_DIR;
|
|
120
|
+
} else {
|
|
121
|
+
process.env.AIOS_WEB_DATA_DIR = previousDataDir;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
|
|
3
|
+
import { loadEnv } from "../src/config/env.js";
|
|
4
|
+
|
|
5
|
+
it("falls back to .env.json for runtime integrations", () => {
|
|
6
|
+
const originalBroker = process.env.AIOS_MQTT_CHANNEL_BROKER;
|
|
7
|
+
const originalS3 = process.env.AIOS_S3_ENDPOINT;
|
|
8
|
+
delete process.env.AIOS_MQTT_CHANNEL_BROKER;
|
|
9
|
+
delete process.env.AIOS_S3_ENDPOINT;
|
|
10
|
+
|
|
11
|
+
try {
|
|
12
|
+
const env = loadEnv();
|
|
13
|
+
expect(env.mqtt.brokerUrl).toBe("mqtt://172.16.12.2:1883");
|
|
14
|
+
expect(env.s3.endpoint).toBe("http://172.16.12.2:9000");
|
|
15
|
+
} finally {
|
|
16
|
+
if (originalBroker === undefined) {
|
|
17
|
+
delete process.env.AIOS_MQTT_CHANNEL_BROKER;
|
|
18
|
+
} else {
|
|
19
|
+
process.env.AIOS_MQTT_CHANNEL_BROKER = originalBroker;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (originalS3 === undefined) {
|
|
23
|
+
delete process.env.AIOS_S3_ENDPOINT;
|
|
24
|
+
} else {
|
|
25
|
+
process.env.AIOS_S3_ENDPOINT = originalS3;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("defaults web data dir under AIOS_ROOT when available", () => {
|
|
31
|
+
const originalRoot = process.env.AIOS_ROOT;
|
|
32
|
+
const originalDataDir = process.env.AIOS_WEB_DATA_DIR;
|
|
33
|
+
process.env.AIOS_ROOT = "/var/aios-apps";
|
|
34
|
+
delete process.env.AIOS_WEB_DATA_DIR;
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
const env = loadEnv();
|
|
38
|
+
expect(env.dataDir).toBe(path.resolve("/var/aios-apps/data/web"));
|
|
39
|
+
} finally {
|
|
40
|
+
if (originalRoot === undefined) {
|
|
41
|
+
delete process.env.AIOS_ROOT;
|
|
42
|
+
} else {
|
|
43
|
+
process.env.AIOS_ROOT = originalRoot;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (originalDataDir === undefined) {
|
|
47
|
+
delete process.env.AIOS_WEB_DATA_DIR;
|
|
48
|
+
} else {
|
|
49
|
+
process.env.AIOS_WEB_DATA_DIR = originalDataDir;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("reads management website timeout from environment in seconds", () => {
|
|
55
|
+
const originalTimeout = process.env.AIOS_MANAGEMENT_WEBSITE_TIMEOUT;
|
|
56
|
+
process.env.AIOS_MANAGEMENT_WEBSITE_TIMEOUT = "180";
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const env = loadEnv();
|
|
60
|
+
expect(env.managementTimeoutMs).toBe(180000);
|
|
61
|
+
} finally {
|
|
62
|
+
if (originalTimeout === undefined) {
|
|
63
|
+
delete process.env.AIOS_MANAGEMENT_WEBSITE_TIMEOUT;
|
|
64
|
+
} else {
|
|
65
|
+
process.env.AIOS_MANAGEMENT_WEBSITE_TIMEOUT = originalTimeout;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
});
|
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { DatabaseSync } from "node:sqlite";
|
|
2
|
+
|
|
3
|
+
import { ExternalService } from "../src/services/external-service.js";
|
|
4
|
+
|
|
5
|
+
function createDb() {
|
|
6
|
+
const db = new DatabaseSync(":memory:");
|
|
7
|
+
db.exec(`
|
|
8
|
+
CREATE TABLE agents (
|
|
9
|
+
id INTEGER PRIMARY KEY,
|
|
10
|
+
slug TEXT NOT NULL UNIQUE,
|
|
11
|
+
agent_name TEXT NOT NULL DEFAULT '',
|
|
12
|
+
status TEXT NOT NULL,
|
|
13
|
+
remote_state_json TEXT NOT NULL DEFAULT '{}'
|
|
14
|
+
);
|
|
15
|
+
|
|
16
|
+
CREATE TABLE aios_users (
|
|
17
|
+
id INTEGER PRIMARY KEY,
|
|
18
|
+
username TEXT NOT NULL UNIQUE
|
|
19
|
+
);
|
|
20
|
+
|
|
21
|
+
CREATE TABLE agent_permissions (
|
|
22
|
+
agent_id INTEGER NOT NULL,
|
|
23
|
+
aios_user_id INTEGER NOT NULL,
|
|
24
|
+
PRIMARY KEY (agent_id, aios_user_id)
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
CREATE TABLE external_sessions (
|
|
28
|
+
session_id TEXT PRIMARY KEY,
|
|
29
|
+
aios_user_id INTEGER NOT NULL,
|
|
30
|
+
agent_id INTEGER NOT NULL,
|
|
31
|
+
created_at TEXT NOT NULL,
|
|
32
|
+
updated_at TEXT NOT NULL,
|
|
33
|
+
UNIQUE (aios_user_id, agent_id)
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
CREATE TABLE external_session_cookies (
|
|
37
|
+
session_id TEXT NOT NULL,
|
|
38
|
+
provider TEXT NOT NULL,
|
|
39
|
+
cookie TEXT NOT NULL,
|
|
40
|
+
created_at TEXT NOT NULL,
|
|
41
|
+
updated_at TEXT NOT NULL,
|
|
42
|
+
PRIMARY KEY (session_id, provider)
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
CREATE TABLE business_systems (
|
|
46
|
+
id INTEGER PRIMARY KEY,
|
|
47
|
+
provider TEXT NOT NULL,
|
|
48
|
+
application_name TEXT NOT NULL,
|
|
49
|
+
description TEXT NOT NULL DEFAULT '',
|
|
50
|
+
ontology_artifact_id INTEGER,
|
|
51
|
+
scheme TEXT NOT NULL,
|
|
52
|
+
host TEXT NOT NULL,
|
|
53
|
+
port INTEGER NOT NULL,
|
|
54
|
+
status TEXT NOT NULL,
|
|
55
|
+
last_connectivity_test_status TEXT NOT NULL DEFAULT 'unknown',
|
|
56
|
+
last_connectivity_test_result_json TEXT NOT NULL DEFAULT '{}',
|
|
57
|
+
created_at TEXT NOT NULL DEFAULT '2026-05-26T00:00:00.000Z',
|
|
58
|
+
updated_at TEXT NOT NULL DEFAULT '2026-05-26T00:00:00.000Z'
|
|
59
|
+
);
|
|
60
|
+
|
|
61
|
+
INSERT INTO aios_users (id, username) VALUES (1, 'zhangsan');
|
|
62
|
+
INSERT INTO agents (id, slug, agent_name, status, remote_state_json)
|
|
63
|
+
VALUES (
|
|
64
|
+
1,
|
|
65
|
+
'agent-a',
|
|
66
|
+
'Agent Alpha',
|
|
67
|
+
'normal',
|
|
68
|
+
'{"inboundTopic":"aios/agent-a/inbound","outboundTopic":"aios/agent-a/outbound"}'
|
|
69
|
+
);
|
|
70
|
+
INSERT INTO agent_permissions (agent_id, aios_user_id) VALUES (1, 1);
|
|
71
|
+
INSERT INTO business_systems (
|
|
72
|
+
id, provider, application_name, scheme, host, port, status
|
|
73
|
+
) VALUES (1, 'hzg', 'crm-app', 'https', 'example.com', 443, 'active');
|
|
74
|
+
`);
|
|
75
|
+
|
|
76
|
+
return db;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function createService(db, overrides = {}) {
|
|
80
|
+
return new ExternalService({
|
|
81
|
+
db,
|
|
82
|
+
agentService: {
|
|
83
|
+
async listAgentDirectoryForUser() {
|
|
84
|
+
return [];
|
|
85
|
+
}
|
|
86
|
+
},
|
|
87
|
+
systemService: {
|
|
88
|
+
findLatestActiveSystemByProvider() {
|
|
89
|
+
return {
|
|
90
|
+
provider: "hzg",
|
|
91
|
+
application_name: "crm-app",
|
|
92
|
+
base_url: "https://example.com:443/crm-app"
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
findSystemForInvocation(_provider, applicationName) {
|
|
96
|
+
return {
|
|
97
|
+
provider: "hzg",
|
|
98
|
+
application_name: applicationName,
|
|
99
|
+
base_url: `https://example.com:443/${applicationName}`
|
|
100
|
+
};
|
|
101
|
+
},
|
|
102
|
+
async recordInvocation() {}
|
|
103
|
+
},
|
|
104
|
+
hzgProviderClient: {},
|
|
105
|
+
auditLogService: {
|
|
106
|
+
write() {}
|
|
107
|
+
},
|
|
108
|
+
...overrides
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
it("creates and reuses external session ids per user-agent pair and returns topics", async () => {
|
|
113
|
+
const db = createDb();
|
|
114
|
+
const auditWrites = [];
|
|
115
|
+
const service = createService(db, {
|
|
116
|
+
auditLogService: {
|
|
117
|
+
write(entry) {
|
|
118
|
+
auditWrites.push(entry);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const first = service.getOrCreateSession({
|
|
124
|
+
userName: "zhangsan",
|
|
125
|
+
agentId: "agent-a"
|
|
126
|
+
});
|
|
127
|
+
const second = service.getOrCreateSession({
|
|
128
|
+
userName: "zhangsan",
|
|
129
|
+
agentId: "agent-a"
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(first).toEqual(jasmine.objectContaining({
|
|
133
|
+
sessionId: jasmine.stringMatching(/^s-/),
|
|
134
|
+
inboundTopic: "aios/agent-a/inbound",
|
|
135
|
+
outboundTopic: "aios/agent-a/outbound"
|
|
136
|
+
}));
|
|
137
|
+
expect(second).toEqual(first);
|
|
138
|
+
expect(auditWrites.length).toBe(1);
|
|
139
|
+
expect(auditWrites[0].detail).toContain("agent状态=normal");
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("throws internal error with reason when creating session for disabled agent", () => {
|
|
143
|
+
const db = createDb();
|
|
144
|
+
db.prepare("UPDATE agents SET status = 'disabled' WHERE slug = ?").run("agent-a");
|
|
145
|
+
const service = createService(db);
|
|
146
|
+
|
|
147
|
+
expect(() => service.getOrCreateSession({
|
|
148
|
+
userName: "zhangsan",
|
|
149
|
+
agentId: "agent-a"
|
|
150
|
+
})).toThrowMatching((error) => (
|
|
151
|
+
error.status === 500
|
|
152
|
+
&& error.code === "internal_error"
|
|
153
|
+
&& error.message.includes("已停用")
|
|
154
|
+
));
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
it("throws internal error with reason when creating session for overlimit agent", () => {
|
|
158
|
+
const db = createDb();
|
|
159
|
+
db.prepare("UPDATE agents SET status = 'overlimit' WHERE slug = ?").run("agent-a");
|
|
160
|
+
const service = createService(db);
|
|
161
|
+
|
|
162
|
+
expect(() => service.getOrCreateSession({
|
|
163
|
+
userName: "zhangsan",
|
|
164
|
+
agentId: "agent-a"
|
|
165
|
+
})).toThrowMatching((error) => (
|
|
166
|
+
error.status === 500
|
|
167
|
+
&& error.code === "internal_error"
|
|
168
|
+
&& error.message.includes("已超出用量限额")
|
|
169
|
+
));
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it("lists every assigned agent for a user with status", async () => {
|
|
173
|
+
const db = createDb();
|
|
174
|
+
const service = createService(db, {
|
|
175
|
+
agentService: {
|
|
176
|
+
async listAgentDirectoryForUser(username) {
|
|
177
|
+
expect(username).toBe("zhangsan");
|
|
178
|
+
return [
|
|
179
|
+
{ agent_id: "agent-a", agent_name: "Agent Alpha", status: "normal" },
|
|
180
|
+
{ agent_id: "agent-disabled", agent_name: "Agent Disabled", status: "disabled" },
|
|
181
|
+
{ agent_id: "agent-overlimit", agent_name: "Agent Overlimit", status: "overlimit" }
|
|
182
|
+
];
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(await service.listAgentsForUser("zhangsan")).toEqual([
|
|
188
|
+
{ agent_id: "agent-a", agent_name: "Agent Alpha", status: "normal" },
|
|
189
|
+
{ agent_id: "agent-disabled", agent_name: "Agent Disabled", status: "disabled" },
|
|
190
|
+
{ agent_id: "agent-overlimit", agent_name: "Agent Overlimit", status: "overlimit" }
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("persists caller cookie on session creation and reuses it for external cookie lookup", async () => {
|
|
195
|
+
const db = createDb();
|
|
196
|
+
const service = createService(db, {
|
|
197
|
+
hzgProviderClient: {
|
|
198
|
+
async createSessionCookie() {
|
|
199
|
+
throw new Error("should not create cookie when caller already provided one");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
const session = service.getOrCreateSession({
|
|
205
|
+
userName: "zhangsan",
|
|
206
|
+
agentId: "agent-a",
|
|
207
|
+
cookie: "ForguncyServer=caller-cookie"
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
const stored = db.prepare(`
|
|
211
|
+
SELECT provider, cookie
|
|
212
|
+
FROM external_session_cookies
|
|
213
|
+
WHERE session_id = ?
|
|
214
|
+
`).get(session.sessionId);
|
|
215
|
+
expect(stored).toEqual({
|
|
216
|
+
provider: "hzg",
|
|
217
|
+
cookie: "ForguncyServer=caller-cookie"
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
const cookie = await service.getCookie(session.sessionId, "hzg");
|
|
221
|
+
expect(cookie).toEqual({
|
|
222
|
+
provider: "hzg",
|
|
223
|
+
cookie: "ForguncyServer=caller-cookie"
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("updates stored caller cookie when the session already exists", async () => {
|
|
228
|
+
const db = createDb();
|
|
229
|
+
const service = createService(db);
|
|
230
|
+
|
|
231
|
+
const first = service.getOrCreateSession({
|
|
232
|
+
userName: "zhangsan",
|
|
233
|
+
agentId: "agent-a",
|
|
234
|
+
cookie: "ForguncyServer=old-cookie"
|
|
235
|
+
});
|
|
236
|
+
const second = service.getOrCreateSession({
|
|
237
|
+
userName: "zhangsan",
|
|
238
|
+
agentId: "agent-a",
|
|
239
|
+
cookie: "ForguncyServer=new-cookie"
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
expect(second.sessionId).toBe(first.sessionId);
|
|
243
|
+
expect(second.inboundTopic).toBe("aios/agent-a/inbound");
|
|
244
|
+
expect(second.outboundTopic).toBe("aios/agent-a/outbound");
|
|
245
|
+
|
|
246
|
+
const stored = db.prepare(`
|
|
247
|
+
SELECT cookie
|
|
248
|
+
FROM external_session_cookies
|
|
249
|
+
WHERE session_id = ? AND provider = 'hzg'
|
|
250
|
+
`).get(first.sessionId);
|
|
251
|
+
expect(stored?.cookie).toBe("ForguncyServer=new-cookie");
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("loads cached cookie and lazily creates missing hzg cookie", async () => {
|
|
255
|
+
const db = createDb();
|
|
256
|
+
db.prepare(`
|
|
257
|
+
INSERT INTO external_sessions (session_id, aios_user_id, agent_id, created_at, updated_at)
|
|
258
|
+
VALUES (?, ?, ?, ?, ?)
|
|
259
|
+
`).run("s-1", 1, 1, "2026-05-26T00:00:00.000Z", "2026-05-26T00:00:00.000Z");
|
|
260
|
+
|
|
261
|
+
let createCount = 0;
|
|
262
|
+
const service = createService(db, {
|
|
263
|
+
hzgProviderClient: {
|
|
264
|
+
async createSessionCookie(baseUrl, sessionId) {
|
|
265
|
+
createCount += 1;
|
|
266
|
+
expect(baseUrl).toBe("https://example.com:443/crm-app");
|
|
267
|
+
expect(sessionId).toBe("s-1");
|
|
268
|
+
return "ForguncyServer=test-cookie";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
const first = await service.getCookie("s-1", "hzg");
|
|
274
|
+
const second = await service.getCookie("s-1", "hzg");
|
|
275
|
+
|
|
276
|
+
expect(first).toEqual({
|
|
277
|
+
provider: "hzg",
|
|
278
|
+
cookie: "ForguncyServer=test-cookie"
|
|
279
|
+
});
|
|
280
|
+
expect(second).toEqual(first);
|
|
281
|
+
expect(createCount).toBe(1);
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
it("loads external context from session and agent records", async () => {
|
|
285
|
+
const db = createDb();
|
|
286
|
+
db.prepare(`
|
|
287
|
+
INSERT INTO external_sessions (session_id, aios_user_id, agent_id, created_at, updated_at)
|
|
288
|
+
VALUES (?, ?, ?, ?, ?)
|
|
289
|
+
`).run("s-context", 1, 1, "2026-05-26T00:00:00.000Z", "2026-05-26T00:00:00.000Z");
|
|
290
|
+
|
|
291
|
+
const service = createService(db);
|
|
292
|
+
const context = service.getContext("s-context");
|
|
293
|
+
|
|
294
|
+
expect(context).toEqual({
|
|
295
|
+
inboundTopic: "aios/agent-a/inbound",
|
|
296
|
+
outboundTopic: "aios/agent-a/outbound",
|
|
297
|
+
agentName: "Agent Alpha",
|
|
298
|
+
agentId: "agent-a",
|
|
299
|
+
userName: "zhangsan"
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("throws not found when external context session does not exist", () => {
|
|
304
|
+
const db = createDb();
|
|
305
|
+
const service = createService(db);
|
|
306
|
+
|
|
307
|
+
expect(() => service.getContext("missing-session")).toThrowError(/会话不存在/);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("records external invocation using session-bound agent and resolved business system", async () => {
|
|
311
|
+
const db = createDb();
|
|
312
|
+
db.prepare(`
|
|
313
|
+
INSERT INTO external_sessions (session_id, aios_user_id, agent_id, created_at, updated_at)
|
|
314
|
+
VALUES (?, ?, ?, ?, ?)
|
|
315
|
+
`).run("s-2", 1, 1, "2026-05-26T00:00:00.000Z", "2026-05-26T00:00:00.000Z");
|
|
316
|
+
|
|
317
|
+
const recorded = [];
|
|
318
|
+
const service = createService(db, {
|
|
319
|
+
systemService: {
|
|
320
|
+
findLatestActiveSystemByProvider() {
|
|
321
|
+
return null;
|
|
322
|
+
},
|
|
323
|
+
findSystemForInvocation(provider, applicationName) {
|
|
324
|
+
expect(provider).toBe("hzg");
|
|
325
|
+
expect(applicationName).toBe("crm-app");
|
|
326
|
+
return {
|
|
327
|
+
provider: "hzg",
|
|
328
|
+
application_name: "crm-app",
|
|
329
|
+
base_url: "https://example.com:443/crm-app"
|
|
330
|
+
};
|
|
331
|
+
},
|
|
332
|
+
async recordInvocation(log) {
|
|
333
|
+
recorded.push(log);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
await service.recordInvocation({
|
|
339
|
+
sessionId: "s-2",
|
|
340
|
+
provider: "hzg",
|
|
341
|
+
applicationName: "crm-app",
|
|
342
|
+
commandName: "GetList",
|
|
343
|
+
paramaters: "{\"page\":1}",
|
|
344
|
+
isOK: false,
|
|
345
|
+
errorMessage: "boom",
|
|
346
|
+
response: "{\"ok\":false}",
|
|
347
|
+
durationInMS: 123
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
expect(recorded.length).toBe(1);
|
|
351
|
+
expect(recorded[0]).toEqual(jasmine.objectContaining({
|
|
352
|
+
agent_slug: "agent-a",
|
|
353
|
+
session_id: "s-2",
|
|
354
|
+
provider: "hzg",
|
|
355
|
+
application_name: "crm-app",
|
|
356
|
+
command_name: "GetList",
|
|
357
|
+
response_time_ms: 123,
|
|
358
|
+
success: false,
|
|
359
|
+
error_message: "boom"
|
|
360
|
+
}));
|
|
361
|
+
expect(recorded[0].request_payload).toEqual({ page: 1 });
|
|
362
|
+
expect(recorded[0].response_payload).toEqual({ ok: false });
|
|
363
|
+
});
|
|
364
|
+
|
|
365
|
+
it("rejects oversized caller cookies before persisting sessions", () => {
|
|
366
|
+
const db = createDb();
|
|
367
|
+
const service = createService(db);
|
|
368
|
+
|
|
369
|
+
expect(() => service.getOrCreateSession({
|
|
370
|
+
userName: "zhangsan",
|
|
371
|
+
agentId: "agent-a",
|
|
372
|
+
cookie: "a".repeat(16 * 1024 + 1)
|
|
373
|
+
})).toThrowMatching((error) => (
|
|
374
|
+
error.status === 400
|
|
375
|
+
&& error.code === "bad_request"
|
|
376
|
+
&& error.message.includes("cookie 长度")
|
|
377
|
+
));
|
|
378
|
+
|
|
379
|
+
expect(db.prepare("SELECT COUNT(*) AS count FROM external_sessions").get().count).toBe(0);
|
|
380
|
+
});
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { HzgProviderClient } from "../src/infra/providers/hzg-provider-client.js";
|
|
2
|
+
|
|
3
|
+
it("creates session cookie from the ServerCommand endpoint response header", async () => {
|
|
4
|
+
const fetchCalls = [];
|
|
5
|
+
const client = new HzgProviderClient({
|
|
6
|
+
fetchImpl: async (url, options) => {
|
|
7
|
+
fetchCalls.push({ url, options });
|
|
8
|
+
return {
|
|
9
|
+
headers: {
|
|
10
|
+
get: (name) => (name === "set-cookie" ? "ForguncyServer=session-cookie" : null)
|
|
11
|
+
},
|
|
12
|
+
text: async () => ""
|
|
13
|
+
};
|
|
14
|
+
}
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const result = await client.createSessionCookie(
|
|
18
|
+
"https://example.com:443/demo-app",
|
|
19
|
+
"thread-1"
|
|
20
|
+
);
|
|
21
|
+
|
|
22
|
+
expect(result).toBe("ForguncyServer=session-cookie");
|
|
23
|
+
expect(fetchCalls).toEqual([
|
|
24
|
+
jasmine.objectContaining({
|
|
25
|
+
url: "https://example.com:443/demo-app/ServerCommand/",
|
|
26
|
+
options: jasmine.objectContaining({
|
|
27
|
+
method: "POST",
|
|
28
|
+
body: JSON.stringify({ ManageThreadId: "thread-1" })
|
|
29
|
+
})
|
|
30
|
+
})
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("falls back to a cookie field in the response body when header is missing", async () => {
|
|
35
|
+
const client = new HzgProviderClient({
|
|
36
|
+
fetchImpl: async () => ({
|
|
37
|
+
headers: {
|
|
38
|
+
get: () => null
|
|
39
|
+
},
|
|
40
|
+
text: async () => JSON.stringify({ cookie: "ForguncyServer=session-cookie" })
|
|
41
|
+
})
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const result = await client.createSessionCookie(
|
|
45
|
+
"https://example.com:443/demo-app",
|
|
46
|
+
"thread-1"
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
expect(result).toBe("ForguncyServer=session-cookie");
|
|
50
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { createInternalAuthMiddleware } from "../src/api/middleware/internal.js";
|
|
2
|
+
|
|
3
|
+
function runMiddleware(middleware, { headers = {}, remoteAddress = "" } = {}) {
|
|
4
|
+
return new Promise((resolve) => {
|
|
5
|
+
middleware({
|
|
6
|
+
headers,
|
|
7
|
+
socket: {
|
|
8
|
+
remoteAddress
|
|
9
|
+
}
|
|
10
|
+
}, {}, (error) => resolve(error ?? null));
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
it("accepts matching bearer token", async () => {
|
|
15
|
+
const middleware = createInternalAuthMiddleware({
|
|
16
|
+
hasAccessToken(token) {
|
|
17
|
+
return token === "sk-abcdef";
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const error = await runMiddleware(middleware, {
|
|
22
|
+
headers: {
|
|
23
|
+
authorization: "Bearer sk-abcdef"
|
|
24
|
+
}
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
expect(error).toBeNull();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("rejects missing or mismatched bearer token", async () => {
|
|
31
|
+
const middleware = createInternalAuthMiddleware({
|
|
32
|
+
hasAccessToken(token) {
|
|
33
|
+
return token === "sk-abcdef";
|
|
34
|
+
}
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const missingError = await runMiddleware(middleware);
|
|
38
|
+
expect(missingError.status).toBe(401);
|
|
39
|
+
expect(missingError.message).toBe("缺少 Bearer Token");
|
|
40
|
+
|
|
41
|
+
const invalidError = await runMiddleware(middleware, {
|
|
42
|
+
headers: {
|
|
43
|
+
authorization: "Bearer sk-other"
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
expect(invalidError.status).toBe(403);
|
|
47
|
+
expect(invalidError.message).toMatch(/invalid bearer token/i);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it("bypasses token checks for localhost calls", async () => {
|
|
51
|
+
const middleware = createInternalAuthMiddleware({
|
|
52
|
+
hasAccessToken() {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
const ipv4Error = await runMiddleware(middleware, {
|
|
58
|
+
remoteAddress: "127.0.0.1"
|
|
59
|
+
});
|
|
60
|
+
expect(ipv4Error).toBeNull();
|
|
61
|
+
|
|
62
|
+
const ipv6Error = await runMiddleware(middleware, {
|
|
63
|
+
remoteAddress: "::1"
|
|
64
|
+
});
|
|
65
|
+
expect(ipv6Error).toBeNull();
|
|
66
|
+
});
|