aios-management-web 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/.env.json +21 -0
  2. package/README.md +257 -0
  3. package/data/management-console.db +0 -0
  4. package/data/management-console.db-shm +0 -0
  5. package/data/management-console.db-wal +0 -0
  6. package/dist/assets/index-CV_wjCAG.js +464 -0
  7. package/dist/assets/index-DfMPB0eV.css +1 -0
  8. package/dist/index.html +13 -0
  9. package/docs/spec.md +199 -0
  10. package/index.html +12 -0
  11. package/package.json +37 -0
  12. package/scripts/reset-kernel.js +59 -0
  13. package/scripts/reset-password.js +22 -0
  14. package/server/fakes.js +57 -0
  15. package/server/index.js +21 -0
  16. package/server/src/api/middleware/auth.js +29 -0
  17. package/server/src/api/middleware/internal.js +44 -0
  18. package/server/src/api/routes/index.js +677 -0
  19. package/server/src/app.js +90 -0
  20. package/server/src/background/index.js +106 -0
  21. package/server/src/background/protocol.js +15 -0
  22. package/server/src/config/env.js +90 -0
  23. package/server/src/db/index.js +501 -0
  24. package/server/src/infra/mqtt/management-rpc-client.js +213 -0
  25. package/server/src/infra/providers/hzg-provider-client.js +39 -0
  26. package/server/src/infra/s3/object-storage.js +97 -0
  27. package/server/src/services/agent-quota.js +54 -0
  28. package/server/src/services/agent-service.js +696 -0
  29. package/server/src/services/agent-status-sync-service.js +132 -0
  30. package/server/src/services/audit-log-service.js +39 -0
  31. package/server/src/services/auth-service.js +153 -0
  32. package/server/src/services/catalog-sync-service.js +712 -0
  33. package/server/src/services/external-service.js +308 -0
  34. package/server/src/services/kernel-reset-service.js +86 -0
  35. package/server/src/services/portal-service.js +555 -0
  36. package/server/src/services/system-service.js +580 -0
  37. package/server/src/services/topic-ping-service.js +282 -0
  38. package/server/src/utils/errors.js +36 -0
  39. package/server/src/utils/security.js +22 -0
  40. package/server/test/agent-service-alignment.test.js +316 -0
  41. package/server/test/agent-service-create.test.js +662 -0
  42. package/server/test/agent-status-sync-service.test.js +167 -0
  43. package/server/test/agent-update-audit.test.js +63 -0
  44. package/server/test/auth-middleware.test.js +71 -0
  45. package/server/test/background-services.test.js +160 -0
  46. package/server/test/catalog-sync-service.test.js +920 -0
  47. package/server/test/db-reset-migration.test.js +123 -0
  48. package/server/test/env-config.test.js +68 -0
  49. package/server/test/external-service.test.js +380 -0
  50. package/server/test/hzg-provider-client.test.js +50 -0
  51. package/server/test/internal-auth-middleware.test.js +66 -0
  52. package/server/test/kernel-reset-service.test.js +112 -0
  53. package/server/test/management-rpc-client.test.js +105 -0
  54. package/server/test/portal-service-access-tokens.test.js +121 -0
  55. package/server/test/portal-service-alignment.test.js +318 -0
  56. package/server/test/portal-service-management-logs.test.js +114 -0
  57. package/server/test/reset-kernel-cli.test.js +23 -0
  58. package/server/test/service-api-auth-middleware.test.js +59 -0
  59. package/server/test/system-service-alignment.test.js +265 -0
  60. package/server/test/topic-ping-service.test.js +182 -0
  61. package/server/test/usage-refresh-audit-route.test.js +82 -0
  62. package/src/App.jsx +1 -0
  63. package/src/api.js +1 -0
  64. package/src/app/App.jsx +346 -0
  65. package/src/app/api-client.js +112 -0
  66. package/src/components/AppShell.jsx +117 -0
  67. package/src/components/CardTitleWithReload.jsx +20 -0
  68. package/src/components/DeleteActionButton.jsx +31 -0
  69. package/src/main.jsx +14 -0
  70. package/src/pages/AgentsPage.jsx +647 -0
  71. package/src/pages/AiosUsersPage.jsx +151 -0
  72. package/src/pages/DashboardPage.jsx +72 -0
  73. package/src/pages/LoginPage.jsx +41 -0
  74. package/src/pages/SettingsPage.jsx +431 -0
  75. package/src/pages/SkillsPage.jsx +175 -0
  76. package/src/pages/SystemLogsPage.jsx +349 -0
  77. package/src/pages/SystemsPage.jsx +498 -0
  78. package/src/pages/TemplatesPage.jsx +207 -0
  79. package/src/pages/UserManagementPage.jsx +25 -0
  80. package/src/pages/UsersPage.jsx +192 -0
  81. package/src/pages/system-logs/SystemLogsTabs.jsx +362 -0
  82. package/src/styles.css +222 -0
  83. package/src/utils/format.js +63 -0
  84. package/test/.reports/fast-2026-05-25T08-32-39-420Z.json +299 -0
  85. package/test/integration/common.js +208 -0
  86. package/test/integration/fast.js +135 -0
  87. package/test/integration/full.js +306 -0
  88. package/test/run-browser-e2e.js +212 -0
  89. package/test/run-jasmine.js +21 -0
  90. package/test/setup.js +1 -0
  91. package/vite.config.js +12 -0
@@ -0,0 +1,31 @@
1
+ import React from "react";
2
+ import { Button, Popconfirm } from "antd";
3
+
4
+ export function DeleteActionButton({
5
+ hidden = false,
6
+ title = "确认删除这条记录吗?",
7
+ description = "删除后不可恢复。",
8
+ okText = "删除",
9
+ cancelText = "取消",
10
+ onConfirm,
11
+ children = "删除"
12
+ }) {
13
+ if (hidden) {
14
+ return null;
15
+ }
16
+
17
+ return (
18
+ <Popconfirm
19
+ title={title}
20
+ description={description}
21
+ okText={okText}
22
+ cancelText={cancelText}
23
+ okButtonProps={{ danger: true }}
24
+ onConfirm={onConfirm}
25
+ >
26
+ <Button size="small" danger>
27
+ {children}
28
+ </Button>
29
+ </Popconfirm>
30
+ );
31
+ }
package/src/main.jsx ADDED
@@ -0,0 +1,14 @@
1
+ import React from "react";
2
+ import ReactDOM from "react-dom/client";
3
+ import { App as AntdApp } from "antd";
4
+
5
+ import App from "./App.jsx";
6
+ import "./styles.css";
7
+
8
+ ReactDOM.createRoot(document.getElementById("root")).render(
9
+ <React.StrictMode>
10
+ <AntdApp>
11
+ <App />
12
+ </AntdApp>
13
+ </React.StrictMode>
14
+ );
@@ -0,0 +1,647 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import {
3
+ Button,
4
+ Card,
5
+ Col,
6
+ Descriptions,
7
+ Drawer,
8
+ Form,
9
+ Input,
10
+ InputNumber,
11
+ Modal,
12
+ Progress,
13
+ Row,
14
+ Select,
15
+ Spin,
16
+ Space,
17
+ Tag,
18
+ Table,
19
+ Typography,
20
+ message
21
+ } from "antd";
22
+
23
+ import { api } from "../app/api-client.js";
24
+ import { CardTitleWithReload } from "../components/CardTitleWithReload.jsx";
25
+ import { DeleteActionButton } from "../components/DeleteActionButton.jsx";
26
+ import { formatDateTime, formatTokenCount } from "../utils/format.js";
27
+
28
+ const { Link, Text } = Typography;
29
+
30
+ const quotaFields = [
31
+ { limitKey: "daily_limit", usageKey: "daily", label: "每日限额" }
32
+ ];
33
+
34
+ const agentIdPattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
35
+ const quotaRules = [
36
+ { required: true, message: "请输入每日限额" },
37
+ {
38
+ validator: (_, value) => (
39
+ Number.isInteger(value) && value >= -1
40
+ ? Promise.resolve()
41
+ : Promise.reject(new Error("请输入 -1 或不小于 0 的整数"))
42
+ )
43
+ }
44
+ ];
45
+
46
+ function getInitialValues() {
47
+ return {
48
+ id: "",
49
+ agent_name: "",
50
+ status: "normal",
51
+ daily_limit: -1
52
+ };
53
+ }
54
+
55
+ function formatQuota(value) {
56
+ return Number(value) < 0 ? "不限" : formatTokenCount(value);
57
+ }
58
+
59
+ function buildUsageQuotaRows(agent) {
60
+ return quotaFields.map((field) => {
61
+ const current = Number(agent?.usage?.[field.usageKey] ?? 0);
62
+ const limit = Number(agent?.[field.limitKey] ?? -1);
63
+ const hasLimit = limit >= 0;
64
+ const percent = hasLimit && limit > 0
65
+ ? Math.min(100, Math.round((current / limit) * 100))
66
+ : 0;
67
+ const exceeded = hasLimit && current >= limit;
68
+ const remaining = hasLimit ? Math.max(0, limit - current) : null;
69
+
70
+ return {
71
+ key: field.usageKey,
72
+ label: field.label,
73
+ current,
74
+ limit,
75
+ hasLimit,
76
+ percent,
77
+ exceeded,
78
+ remaining
79
+ };
80
+ });
81
+ }
82
+
83
+ function normalizeUsernames(values) {
84
+ return [...new Set((values || []).map((item) => String(item || "").trim()).filter(Boolean))];
85
+ }
86
+
87
+ function buildAgentPayload(values, editing) {
88
+ const payload = {
89
+ agent_name: String(values.agent_name || "").trim(),
90
+ daily_limit: Number(values.daily_limit ?? -1)
91
+ };
92
+
93
+ if (!editing) {
94
+ payload.id = String(values.id || "").trim();
95
+ } else {
96
+ payload.status = values.status || "normal";
97
+ }
98
+
99
+ return payload;
100
+ }
101
+
102
+ function getStatusColor(status) {
103
+ if (status === "normal") {
104
+ return "green";
105
+ }
106
+ if (status === "overlimit") {
107
+ return "red";
108
+ }
109
+ return "default";
110
+ }
111
+
112
+ function getStatusLabel(status) {
113
+ switch (status) {
114
+ case "normal":
115
+ return "正常";
116
+ case "overlimit":
117
+ return "超限";
118
+ case "disabled":
119
+ return "停用";
120
+ default:
121
+ return status || "-";
122
+ }
123
+ }
124
+
125
+ function getAgentDetailItems(agent) {
126
+ if (!agent) {
127
+ return [];
128
+ }
129
+
130
+ return [
131
+ { key: "id", label: "ID", children: agent.slug || "-" },
132
+ { key: "name", label: "名称", children: agent.agent_name || "-" },
133
+ {
134
+ key: "status",
135
+ label: "状态",
136
+ children: <Tag color={getStatusColor(agent.status)}>{getStatusLabel(agent.status)}</Tag>
137
+ },
138
+ { key: "created_at", label: "创建时间", children: formatDateTime(agent.created_at) },
139
+ { key: "updated_at", label: "更新时间", children: formatDateTime(agent.updated_at) },
140
+ {
141
+ key: "inbound",
142
+ label: "入站主题",
143
+ children: agent.remote_state?.inboundTopic || "-"
144
+ },
145
+ { key: "outbound", label: "出站主题", children: agent.remote_state?.outboundTopic || "-" }
146
+ ];
147
+ }
148
+
149
+ export function AgentsPage() {
150
+ const [items, setItems] = useState([]);
151
+ const [aiosUserOptions, setAiosUserOptions] = useState([]);
152
+ const [loading, setLoading] = useState(false);
153
+ const [aiosUserLoading, setAiosUserLoading] = useState(false);
154
+ const [editing, setEditing] = useState(null);
155
+ const [open, setOpen] = useState(false);
156
+ const [permissionAgent, setPermissionAgent] = useState(null);
157
+ const [permissionOpen, setPermissionOpen] = useState(false);
158
+ const [permissionSaving, setPermissionSaving] = useState(false);
159
+ const [detailAgent, setDetailAgent] = useState(null);
160
+ const [detailOpen, setDetailOpen] = useState(false);
161
+ const [submitLoading, setSubmitLoading] = useState(false);
162
+ const [submitMaskOpen, setSubmitMaskOpen] = useState(false);
163
+ const [submitMaskText, setSubmitMaskText] = useState("正在处理,请稍候...");
164
+ const [form] = Form.useForm();
165
+ const [permissionForm] = Form.useForm();
166
+ const [messageApi, contextHolder] = message.useMessage();
167
+
168
+ const load = async () => {
169
+ setLoading(true);
170
+ try {
171
+ const [agents, aiosUsers] = await Promise.all([
172
+ api.get("/api/agents"),
173
+ api.get("/api/aios-users")
174
+ ]);
175
+ setItems(agents);
176
+ setAiosUserOptions((aiosUsers.items || []).map((item) => ({
177
+ value: item.username,
178
+ label: item.username
179
+ })));
180
+ } finally {
181
+ setLoading(false);
182
+ }
183
+ };
184
+
185
+ const refreshAgentReferences = (agents, agentId) => {
186
+ const nextAgent = agents.find((item) => Number(item.id) === Number(agentId)) || null;
187
+ if (nextAgent && detailOpen && Number(detailAgent?.id) === Number(agentId)) {
188
+ setDetailAgent(nextAgent);
189
+ }
190
+ return nextAgent;
191
+ };
192
+
193
+ const reloadAll = async (focusAgentId = null) => {
194
+ const [agents, aiosUsers] = await Promise.all([
195
+ api.get("/api/agents"),
196
+ api.get("/api/aios-users")
197
+ ]);
198
+ setItems(agents);
199
+ setAiosUserOptions((aiosUsers.items || []).map((item) => ({
200
+ value: item.username,
201
+ label: item.username
202
+ })));
203
+ if (focusAgentId !== null && focusAgentId !== undefined) {
204
+ return refreshAgentReferences(agents, focusAgentId);
205
+ }
206
+ return null;
207
+ };
208
+
209
+ useEffect(() => {
210
+ void load();
211
+ }, []);
212
+
213
+ const searchAiosUsers = async (keyword) => {
214
+ setAiosUserLoading(true);
215
+ try {
216
+ const result = await api.get(`/api/aios-users?q=${encodeURIComponent(keyword || "")}`);
217
+ setAiosUserOptions((result.items || []).map((item) => ({
218
+ value: item.username,
219
+ label: item.username
220
+ })));
221
+ } finally {
222
+ setAiosUserLoading(false);
223
+ }
224
+ };
225
+
226
+ const openPermissionDrawer = (agent) => {
227
+ setPermissionAgent(agent);
228
+ permissionForm.setFieldsValue({
229
+ permission_usernames: (agent.permissions || []).map((item) => item.username)
230
+ });
231
+ setPermissionOpen(true);
232
+ };
233
+
234
+ const openDetailDrawer = (agent) => {
235
+ setDetailAgent(agent);
236
+ setDetailOpen(true);
237
+ };
238
+
239
+ const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
240
+
241
+ const pollDeletedAgent = async (slug, timeoutMs = 90000, intervalMs = 2000) => {
242
+ const startedAt = Date.now();
243
+
244
+ while (Date.now() - startedAt < timeoutMs) {
245
+ try {
246
+ await api.get(`/api/agents/by-slug/${encodeURIComponent(slug)}`);
247
+ } catch (error) {
248
+ if (String(error?.message || "").includes("数字员工不存在")) {
249
+ return;
250
+ }
251
+ throw error;
252
+ }
253
+ await sleep(intervalMs);
254
+ }
255
+
256
+ throw new Error(`数字员工 ${slug} 删除状态确认超时,请稍后刷新列表确认结果`);
257
+ };
258
+
259
+ const handleSubmit = async () => {
260
+ try {
261
+ const values = await form.validateFields();
262
+ const payload = buildAgentPayload(values, editing);
263
+ setSubmitLoading(true);
264
+ setSubmitMaskText(editing ? "正在保存数字员工..." : "正在创建数字员工,请稍候...");
265
+ setSubmitMaskOpen(true);
266
+
267
+ if (editing) {
268
+ await api.put(`/api/agents/${editing.id}`, payload);
269
+ setOpen(false);
270
+ form.resetFields();
271
+ await load();
272
+ messageApi.success(`已更新数字员工 ${editing.slug}`);
273
+ return;
274
+ }
275
+
276
+ const createdAgent = await api.post("/api/agents", payload);
277
+ setOpen(false);
278
+ form.resetFields();
279
+ await load();
280
+ messageApi.success(`数字员工 ${createdAgent.slug} 创建成功`);
281
+ } catch (error) {
282
+ if (error?.errorFields) {
283
+ return;
284
+ }
285
+ messageApi.error(error.message || "保存数字员工失败");
286
+ } finally {
287
+ setSubmitLoading(false);
288
+ setSubmitMaskOpen(false);
289
+ }
290
+ };
291
+
292
+ const handleDeleteAgent = async (row) => {
293
+ try {
294
+ setSubmitLoading(true);
295
+ setSubmitMaskText(`正在删除数字员工 ${row.slug},请稍候...`);
296
+ setSubmitMaskOpen(true);
297
+ await api.delete(`/api/agents/${row.id}`);
298
+ await pollDeletedAgent(row.slug);
299
+ await load();
300
+ messageApi.success(`数字员工 ${row.slug} 已删除`);
301
+ } catch (error) {
302
+ messageApi.error(error.message || `删除数字员工 ${row.slug} 失败`);
303
+ } finally {
304
+ setSubmitLoading(false);
305
+ setSubmitMaskOpen(false);
306
+ }
307
+ };
308
+
309
+ return (
310
+ <>
311
+ {contextHolder}
312
+ <Modal
313
+ open={submitMaskOpen}
314
+ footer={null}
315
+ closable={false}
316
+ maskClosable={false}
317
+ centered
318
+ width={320}
319
+ >
320
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "16px 0" }}>
321
+ <Spin size="large" />
322
+ <div>{submitMaskText}</div>
323
+ </div>
324
+ </Modal>
325
+
326
+ <Card
327
+ className="page-card"
328
+ title={<CardTitleWithReload title="数字员工(Agent)" loading={loading} onReload={() => load()} />}
329
+ extra={(
330
+ <Button
331
+ type="primary"
332
+ onClick={() => {
333
+ setEditing(null);
334
+ form.setFieldsValue(getInitialValues());
335
+ setOpen(true);
336
+ }}
337
+ >
338
+ 新增数字员工
339
+ </Button>
340
+ )}
341
+ >
342
+ <Table
343
+ rowKey="id"
344
+ loading={loading}
345
+ dataSource={items}
346
+ pagination={{
347
+ pageSize: 50,
348
+ showSizeChanger: false
349
+ }}
350
+ columns={[
351
+ {
352
+ title: "ID",
353
+ dataIndex: "slug",
354
+ render: (value, row) => (
355
+ <Link
356
+ onClick={() => {
357
+ openDetailDrawer(row);
358
+ }}
359
+ >
360
+ {value}
361
+ </Link>
362
+ )
363
+ },
364
+ { title: "名称", dataIndex: "agent_name", render: (value) => value || "-" },
365
+ {
366
+ title: "每日限额",
367
+ dataIndex: "daily_limit",
368
+ render: formatQuota
369
+ },
370
+ {
371
+ title: "今日用量",
372
+ render: (_, row) => formatTokenCount(row.usage?.daily ?? 0)
373
+ },
374
+ {
375
+ title: "状态",
376
+ render: (_, row) => <Tag color={getStatusColor(row.status)}>{getStatusLabel(row.status)}</Tag>
377
+ },
378
+ {
379
+ title: "操作",
380
+ render: (_, row) => (
381
+ <Space wrap>
382
+ <Button
383
+ size="small"
384
+ onClick={() => {
385
+ setEditing(row);
386
+ form.setFieldsValue({
387
+ agent_name: row.agent_name,
388
+ status: row.status,
389
+ daily_limit: row.daily_limit
390
+ });
391
+ setOpen(true);
392
+ }}
393
+ >
394
+ 编辑
395
+ </Button>
396
+ <Button
397
+ size="small"
398
+ onClick={() => {
399
+ openPermissionDrawer(row);
400
+ }}
401
+ >
402
+ 管理用户
403
+ </Button>
404
+ <DeleteActionButton
405
+ title={`确认删除数字员工 ${row.slug} 吗?`}
406
+ description="删除后该数字员工的管理记录会被移除。"
407
+ onConfirm={async () => {
408
+ await handleDeleteAgent(row);
409
+ }}
410
+ />
411
+ </Space>
412
+ )
413
+ }
414
+ ]}
415
+ />
416
+ </Card>
417
+
418
+ <Modal
419
+ open={open}
420
+ title={editing ? "编辑数字员工" : "新增数字员工"}
421
+ width={720}
422
+ okText="完成"
423
+ cancelText="取消"
424
+ confirmLoading={submitLoading}
425
+ onCancel={() => {
426
+ setOpen(false);
427
+ setEditing(null);
428
+ form.resetFields();
429
+ }}
430
+ onOk={() => {
431
+ void handleSubmit();
432
+ }}
433
+ >
434
+ <Form form={form} layout="vertical" initialValues={getInitialValues()}>
435
+ {editing ? (
436
+ <>
437
+ <Form.Item label="ID">
438
+ <Input value={editing.slug} disabled />
439
+ </Form.Item>
440
+ </>
441
+ ) : (
442
+ <Form.Item
443
+ name="id"
444
+ label="ID"
445
+ rules={[
446
+ { required: true, message: "请输入数字员工 ID" },
447
+ {
448
+ pattern: agentIdPattern,
449
+ message: "ID 需使用 slug 格式:小写字母、数字和中划线"
450
+ }
451
+ ]}
452
+ >
453
+ <Input placeholder="例如 finance-agent" />
454
+ </Form.Item>
455
+ )}
456
+
457
+ <Form.Item
458
+ name="agent_name"
459
+ label="数字员工名称"
460
+ rules={[{ required: true, message: "请输入数字员工名称" }]}
461
+ >
462
+ <Input placeholder="例如 财务助手" />
463
+ </Form.Item>
464
+
465
+ {editing ? (
466
+ <Form.Item
467
+ name="status"
468
+ label="状态"
469
+ rules={[{ required: true, message: "请选择状态" }]}
470
+ >
471
+ <Select
472
+ options={[
473
+ { value: "normal", label: "正常" },
474
+ { value: "disabled", label: "停用" },
475
+ { value: "overlimit", label: "超限" }
476
+ ]}
477
+ />
478
+ </Form.Item>
479
+ ) : null}
480
+
481
+ <Row gutter={12}>
482
+ {quotaFields.map((field) => (
483
+ <Col span={12} key={field.limitKey}>
484
+ <Form.Item name={field.limitKey} label={`${field.label}限制`} rules={quotaRules}>
485
+ <InputNumber style={{ width: "100%" }} min={-1} precision={0} />
486
+ </Form.Item>
487
+ </Col>
488
+ ))}
489
+ </Row>
490
+ </Form>
491
+ </Modal>
492
+
493
+ <Modal
494
+ open={permissionOpen}
495
+ title={permissionAgent ? `管理 ${permissionAgent.slug} 的用户` : "管理数字员工用户"}
496
+ width={640}
497
+ okText="保存分配"
498
+ cancelText="取消"
499
+ confirmLoading={permissionSaving}
500
+ onCancel={() => {
501
+ setPermissionOpen(false);
502
+ setPermissionAgent(null);
503
+ permissionForm.resetFields();
504
+ }}
505
+ onOk={async () => {
506
+ const values = await permissionForm.validateFields();
507
+ setPermissionSaving(true);
508
+ try {
509
+ await api.put(`/api/agents/${permissionAgent.id}`, {
510
+ permission_usernames: normalizeUsernames(values.permission_usernames)
511
+ });
512
+ const nextAgent = await reloadAll(permissionAgent.id);
513
+ if (nextAgent) {
514
+ setPermissionAgent(nextAgent);
515
+ }
516
+ setPermissionOpen(false);
517
+ setPermissionAgent(null);
518
+ permissionForm.resetFields();
519
+ } finally {
520
+ setPermissionSaving(false);
521
+ }
522
+ }}
523
+ >
524
+ <Space direction="vertical" size={16} style={{ width: "100%" }}>
525
+ <Card size="small" className="page-card">
526
+ <Space direction="vertical" size={8} style={{ width: "100%" }}>
527
+ <Text strong>{permissionAgent?.slug || "-"}</Text>
528
+ <Text type="secondary">
529
+ 直接输入用户名后回车即可新增并分配,输入时会优先联想已有 AIOS 用户。
530
+ </Text>
531
+ </Space>
532
+ </Card>
533
+ <Form form={permissionForm} layout="vertical">
534
+ <Form.Item
535
+ name="permission_usernames"
536
+ label="授权用户列表"
537
+ extra="支持搜索、粘贴多个用户名并按回车确认。"
538
+ >
539
+ <Select
540
+ mode="tags"
541
+ placeholder="请输入用户名并回车,或从联想结果中选择"
542
+ options={aiosUserOptions}
543
+ loading={aiosUserLoading}
544
+ onSearch={(value) => {
545
+ void searchAiosUsers(value);
546
+ }}
547
+ onDropdownVisibleChange={(openDropdown) => {
548
+ if (openDropdown) {
549
+ void searchAiosUsers("");
550
+ }
551
+ }}
552
+ tokenSeparators={[",", " "]}
553
+ filterOption={false}
554
+ />
555
+ </Form.Item>
556
+ </Form>
557
+ </Space>
558
+ </Modal>
559
+
560
+ <Drawer
561
+ open={detailOpen}
562
+ title={detailAgent ? `${detailAgent.slug} 的数字员工详情` : "数字员工详情"}
563
+ placement="right"
564
+ width="100vw"
565
+ onClose={() => {
566
+ setDetailOpen(false);
567
+ setDetailAgent(null);
568
+ }}
569
+ >
570
+ <Space direction="vertical" size={16} style={{ width: "100%" }}>
571
+ <Card className="page-card" title="基础信息">
572
+ <Descriptions
573
+ bordered
574
+ size="small"
575
+ column={2}
576
+ items={getAgentDetailItems(detailAgent)}
577
+ />
578
+ </Card>
579
+ <Row gutter={[16, 16]}>
580
+ <Col xs={24} md={12} xl={8}>
581
+ <Card className="page-card" title="员工分配">
582
+ <Space wrap>
583
+ {(detailAgent?.permissions || []).length > 0
584
+ ? detailAgent.permissions.map((item) => (
585
+ <Tag key={item.id || item.username} color="blue">
586
+ {item.username}
587
+ </Tag>
588
+ ))
589
+ : <Text type="secondary">暂无分配用户</Text>}
590
+ </Space>
591
+ </Card>
592
+ </Col>
593
+ <Col xs={24} md={12} xl={16}>
594
+ <Card className="page-card" title="用量与限额">
595
+ <Table
596
+ rowKey="key"
597
+ size="small"
598
+ pagination={false}
599
+ dataSource={buildUsageQuotaRows(detailAgent)}
600
+ columns={[
601
+ {
602
+ title: "周期",
603
+ dataIndex: "label",
604
+ width: 96
605
+ },
606
+ {
607
+ title: "用量",
608
+ render: (_, row) => formatTokenCount(row.current)
609
+ },
610
+ {
611
+ title: "限额",
612
+ render: (_, row) => formatQuota(row.limit)
613
+ },
614
+ {
615
+ title: "占比",
616
+ render: (_, row) => (
617
+ row.hasLimit ? (
618
+ <Progress
619
+ percent={row.percent}
620
+ status={row.exceeded ? "exception" : "active"}
621
+ size="small"
622
+ />
623
+ ) : <Text type="secondary">不限额</Text>
624
+ )
625
+ },
626
+ {
627
+ title: "剩余",
628
+ render: (_, row) => (
629
+ row.hasLimit
630
+ ? (
631
+ <Text type={row.exceeded ? "danger" : undefined}>
632
+ {row.exceeded ? "已超限" : formatTokenCount(row.remaining)}
633
+ </Text>
634
+ )
635
+ : <Text type="secondary">不限额</Text>
636
+ )
637
+ }
638
+ ]}
639
+ />
640
+ </Card>
641
+ </Col>
642
+ </Row>
643
+ </Space>
644
+ </Drawer>
645
+ </>
646
+ );
647
+ }