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,498 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import {
3
+ Button,
4
+ Card,
5
+ Col,
6
+ Descriptions,
7
+ Form,
8
+ Input,
9
+ InputNumber,
10
+ Modal,
11
+ Row,
12
+ Select,
13
+ Space,
14
+ Spin,
15
+ Table,
16
+ Tabs,
17
+ Tag,
18
+ Upload,
19
+ message
20
+ } from "antd";
21
+
22
+ import { api } from "../app/api-client.js";
23
+ import { CardTitleWithReload } from "../components/CardTitleWithReload.jsx";
24
+ import { DeleteActionButton } from "../components/DeleteActionButton.jsx";
25
+ import { formatDateTime } from "../utils/format.js";
26
+
27
+ const applicationNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
28
+
29
+ const jsonBlockStyle = {
30
+ margin: 0,
31
+ padding: 12,
32
+ borderRadius: 8,
33
+ background: "#fafafa",
34
+ border: "1px solid #f0f0f0",
35
+ overflowX: "auto",
36
+ whiteSpace: "pre-wrap",
37
+ wordBreak: "break-word",
38
+ fontSize: 12,
39
+ lineHeight: 1.6
40
+ };
41
+
42
+ function getSystemStatusLabel(status) {
43
+ return status === "active" ? "启用" : "停用";
44
+ }
45
+
46
+ function getInitialValues() {
47
+ return {
48
+ provider: "hzg",
49
+ scheme: "https",
50
+ port: 443,
51
+ status: "active"
52
+ };
53
+ }
54
+
55
+ function formatJson(value) {
56
+ if (value === undefined || value === null) {
57
+ return "-";
58
+ }
59
+
60
+ try {
61
+ return JSON.stringify(value, null, 2);
62
+ } catch {
63
+ return String(value);
64
+ }
65
+ }
66
+
67
+ export function SystemsPage() {
68
+ const [systems, setSystems] = useState([]);
69
+ const [stats, setStats] = useState([]);
70
+ const [loading, setLoading] = useState(false);
71
+ const [editing, setEditing] = useState(null);
72
+ const [open, setOpen] = useState(false);
73
+ const [testResult, setTestResult] = useState(null);
74
+ const [testResultOpen, setTestResultOpen] = useState(false);
75
+ const [submitLoading, setSubmitLoading] = useState(false);
76
+ const [submitMaskOpen, setSubmitMaskOpen] = useState(false);
77
+ const [submitMaskText, setSubmitMaskText] = useState("正在处理,请稍候...");
78
+ const [form] = Form.useForm();
79
+ const [messageApi, contextHolder] = message.useMessage();
80
+
81
+ const load = async () => {
82
+ setLoading(true);
83
+ try {
84
+ const [systemRows, statRows] = await Promise.all([
85
+ api.get("/api/systems"),
86
+ api.get("/api/systems/stats")
87
+ ]);
88
+ setSystems(systemRows);
89
+ setStats(statRows);
90
+ } finally {
91
+ setLoading(false);
92
+ }
93
+ };
94
+
95
+ useEffect(() => {
96
+ void load();
97
+ }, []);
98
+
99
+ const handleSubmit = async () => {
100
+ try {
101
+ const values = await form.validateFields();
102
+ const formData = new FormData();
103
+
104
+ Object.entries(values).forEach(([key, value]) => {
105
+ if (key === "ontology" || value === undefined) {
106
+ return;
107
+ }
108
+ formData.set(key, String(value));
109
+ });
110
+
111
+ if (values.ontology?.[0]?.originFileObj) {
112
+ formData.set("ontology", values.ontology[0].originFileObj);
113
+ }
114
+
115
+ setSubmitLoading(true);
116
+ setSubmitMaskText(editing ? "正在保存业务系统..." : "正在创建业务系统,请稍候...");
117
+ setSubmitMaskOpen(true);
118
+
119
+ if (editing) {
120
+ await api.multipart(`/api/systems/${editing.id}`, formData, "PUT");
121
+ messageApi.success(`业务系统 ${editing.application_name} 已更新`);
122
+ } else {
123
+ await api.multipart("/api/systems", formData);
124
+ messageApi.success(`业务系统 ${values.application_name} 创建成功`);
125
+ }
126
+
127
+ setOpen(false);
128
+ setEditing(null);
129
+ form.resetFields();
130
+ await load();
131
+ } catch (error) {
132
+ if (error?.errorFields) {
133
+ return;
134
+ }
135
+ messageApi.error(error.message || "保存业务系统失败");
136
+ } finally {
137
+ setSubmitLoading(false);
138
+ setSubmitMaskOpen(false);
139
+ }
140
+ };
141
+
142
+ const handleDeleteSystem = async (row) => {
143
+ try {
144
+ setSubmitLoading(true);
145
+ setSubmitMaskText(`正在删除业务系统 ${row.application_name},请稍候...`);
146
+ setSubmitMaskOpen(true);
147
+ await api.delete(`/api/systems/${row.id}`);
148
+ await load();
149
+ messageApi.success(`业务系统 ${row.application_name} 已删除`);
150
+ } catch (error) {
151
+ messageApi.error(error.message || `删除业务系统 ${row.application_name} 失败`);
152
+ } finally {
153
+ setSubmitLoading(false);
154
+ setSubmitMaskOpen(false);
155
+ }
156
+ };
157
+
158
+ const handleConnectivityTest = async (row) => {
159
+ try {
160
+ setSubmitLoading(true);
161
+ setSubmitMaskText(`正在测试业务系统 ${row.application_name} 连通性,请稍候...`);
162
+ setSubmitMaskOpen(true);
163
+ const result = await api.post(`/api/systems/${row.id}/test`, {});
164
+ setTestResult({
165
+ applicationName: row.application_name,
166
+ ok: Boolean(result?.ok),
167
+ data: result,
168
+ errorMessage: ""
169
+ });
170
+ setTestResultOpen(true);
171
+ } catch (error) {
172
+ setTestResult({
173
+ applicationName: row.application_name,
174
+ ok: false,
175
+ data: null,
176
+ errorMessage: error.message || "连通性测试失败"
177
+ });
178
+ setTestResultOpen(true);
179
+ } finally {
180
+ setSubmitLoading(false);
181
+ setSubmitMaskOpen(false);
182
+ }
183
+ };
184
+
185
+ return (
186
+ <>
187
+ {contextHolder}
188
+ <Modal
189
+ open={submitMaskOpen}
190
+ footer={null}
191
+ closable={false}
192
+ maskClosable={false}
193
+ centered
194
+ width={320}
195
+ >
196
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "16px 0" }}>
197
+ <Spin size="large" />
198
+ <div>{submitMaskText}</div>
199
+ </div>
200
+ </Modal>
201
+ <Tabs
202
+ defaultActiveKey="catalog"
203
+ items={[
204
+ {
205
+ key: "catalog",
206
+ label: "系统接入",
207
+ children: (
208
+ <Card
209
+ className="page-card"
210
+ title={<CardTitleWithReload title="业务系统" loading={loading} onReload={() => load()} />}
211
+ extra={(
212
+ <Button
213
+ type="primary"
214
+ onClick={() => {
215
+ setEditing(null);
216
+ form.resetFields();
217
+ form.setFieldsValue(getInitialValues());
218
+ setOpen(true);
219
+ }}
220
+ >
221
+ 新增业务系统
222
+ </Button>
223
+ )}
224
+ >
225
+ <Table
226
+ rowKey="id"
227
+ loading={loading}
228
+ dataSource={systems}
229
+ pagination={{
230
+ pageSize: 50,
231
+ showSizeChanger: false
232
+ }}
233
+ columns={[
234
+ { title: "提供方", dataIndex: "provider" },
235
+ { title: "应用名", dataIndex: "application_name" },
236
+ {
237
+ title: "类型",
238
+ render: (_, row) => (row.is_builtin ? <Tag color="blue">内置</Tag> : <Tag>自定义</Tag>)
239
+ },
240
+ {
241
+ title: "状态",
242
+ render: (_, row) => (
243
+ <Tag color={row.status === "active" ? "green" : "default"}>
244
+ {getSystemStatusLabel(row.status)}
245
+ </Tag>
246
+ )
247
+ },
248
+ {
249
+ title: "操作",
250
+ render: (_, row) => (
251
+ <Space wrap>
252
+ <Button
253
+ size="small"
254
+ onClick={() => {
255
+ setEditing(row);
256
+ form.setFieldsValue({
257
+ provider: row.provider,
258
+ application_name: row.application_name,
259
+ description: row.description,
260
+ scheme: row.scheme,
261
+ host: row.host,
262
+ port: row.port,
263
+ status: row.status
264
+ });
265
+ setOpen(true);
266
+ }}
267
+ >
268
+ 编辑
269
+ </Button>
270
+ <Button
271
+ size="small"
272
+ onClick={async () => {
273
+ await handleConnectivityTest(row);
274
+ }}
275
+ >
276
+ 连通性测试
277
+ </Button>
278
+ <Button
279
+ size="small"
280
+ onClick={async () => {
281
+ await api.post(`/api/systems/${row.id}/status`, {
282
+ status: row.status === "active" ? "disabled" : "active"
283
+ });
284
+ await load();
285
+ }}
286
+ >
287
+ {row.status === "active" ? "停用" : "启用"}
288
+ </Button>
289
+ <DeleteActionButton
290
+ hidden={row.is_builtin}
291
+ title={`确认删除业务系统 ${row.application_name} 吗?`}
292
+ description="删除后该业务系统及其管理记录会被移除。"
293
+ onConfirm={async () => {
294
+ await handleDeleteSystem(row);
295
+ }}
296
+ />
297
+ </Space>
298
+ )
299
+ }
300
+ ]}
301
+ />
302
+ </Card>
303
+ )
304
+ },
305
+ {
306
+ key: "stats",
307
+ label: "调用统计",
308
+ children: (
309
+ <Card
310
+ className="page-card"
311
+ title={<CardTitleWithReload title="调用统计" loading={loading} onReload={() => load()} />}
312
+ >
313
+ <Table
314
+ rowKey={(row) => `${row.application_name}-${row.command_name}`}
315
+ loading={loading}
316
+ dataSource={stats}
317
+ pagination={{
318
+ pageSize: 50,
319
+ showSizeChanger: false
320
+ }}
321
+ columns={[
322
+ { title: "提供方", dataIndex: "provider" },
323
+ { title: "应用名", dataIndex: "application_name" },
324
+ { title: "指令", dataIndex: "command_name" },
325
+ { title: "次数", dataIndex: "call_count" },
326
+ { title: "成功率", dataIndex: "success_rate", render: (value) => `${value}%` },
327
+ { title: "平均耗时", dataIndex: "avg_response_ms", render: (value) => `${value} ms` }
328
+ ]}
329
+ />
330
+ </Card>
331
+ )
332
+ }
333
+ ]}
334
+ />
335
+ <Modal
336
+ open={testResultOpen}
337
+ title="连通性测试结果"
338
+ width={640}
339
+ footer={(
340
+ <Button
341
+ type="primary"
342
+ onClick={() => {
343
+ setTestResultOpen(false);
344
+ setTestResult(null);
345
+ }}
346
+ >
347
+ 确定
348
+ </Button>
349
+ )}
350
+ onCancel={() => {
351
+ setTestResultOpen(false);
352
+ setTestResult(null);
353
+ }}
354
+ >
355
+ <Space direction="vertical" size={16} style={{ width: "100%" }}>
356
+ <Descriptions
357
+ bordered
358
+ size="small"
359
+ column={1}
360
+ items={[
361
+ {
362
+ key: "application_name",
363
+ label: "应用名",
364
+ children: testResult?.applicationName || "-"
365
+ },
366
+ {
367
+ key: "result",
368
+ label: "测试结果",
369
+ children: <Tag color={testResult?.ok ? "green" : "red"}>{testResult?.ok ? "成功" : "失败"}</Tag>
370
+ },
371
+ {
372
+ key: "base_url",
373
+ label: "地址",
374
+ children: testResult?.data?.baseUrl || "-"
375
+ },
376
+ {
377
+ key: "status",
378
+ label: "HTTP 状态",
379
+ children: testResult?.data?.service?.status ?? "-"
380
+ },
381
+ {
382
+ key: "tested_at",
383
+ label: "测试时间",
384
+ children: testResult?.data?.tested_at ? formatDateTime(testResult.data.tested_at) : "-"
385
+ },
386
+ {
387
+ key: "error_message",
388
+ label: "错误信息",
389
+ children: testResult?.errorMessage || "-"
390
+ }
391
+ ]}
392
+ />
393
+ <Card className="page-card" title="返回内容">
394
+ <pre style={jsonBlockStyle}>{formatJson(testResult?.data || testResult?.errorMessage || "-")}</pre>
395
+ </Card>
396
+ </Space>
397
+ </Modal>
398
+ <Modal
399
+ open={open}
400
+ title={editing ? "编辑业务系统" : "新增业务系统"}
401
+ width={720}
402
+ okText="确认"
403
+ cancelText="取消"
404
+ confirmLoading={submitLoading}
405
+ onCancel={() => {
406
+ setOpen(false);
407
+ setEditing(null);
408
+ }}
409
+ onOk={() => {
410
+ void handleSubmit();
411
+ }}
412
+ >
413
+ <Form form={form} layout="vertical" initialValues={getInitialValues()}>
414
+ <Row gutter={12}>
415
+ <Col span={12}>
416
+ <Form.Item
417
+ name="provider"
418
+ label="提供方"
419
+ rules={[{ required: true, message: "请选择提供方" }]}
420
+ >
421
+ <Select options={[{ value: "hzg", label: "hzg" }, { value: "phx", label: "phx" }]} />
422
+ </Form.Item>
423
+ </Col>
424
+ <Col span={12}>
425
+ <Form.Item
426
+ name="application_name"
427
+ label="应用名"
428
+ rules={[
429
+ { required: true, message: "请输入应用名" },
430
+ {
431
+ pattern: applicationNamePattern,
432
+ message: "应用名需符合 Ubuntu 目录名规则:仅允许小写字母、数字和中划线"
433
+ }
434
+ ]}
435
+ >
436
+ <Input placeholder="例如 demo-app" disabled={Boolean(editing)} />
437
+ </Form.Item>
438
+ </Col>
439
+ <Col span={8}>
440
+ <Form.Item
441
+ name="scheme"
442
+ label="协议"
443
+ rules={[{ required: true, message: "请选择协议" }]}
444
+ >
445
+ <Select options={[{ value: "http", label: "http" }, { value: "https", label: "https" }]} />
446
+ </Form.Item>
447
+ </Col>
448
+ <Col span={10}>
449
+ <Form.Item
450
+ name="host"
451
+ label="主机名"
452
+ rules={[{ required: true, message: "请输入主机名" }]}
453
+ >
454
+ <Input placeholder="例如 example.com" />
455
+ </Form.Item>
456
+ </Col>
457
+ <Col span={6}>
458
+ <Form.Item
459
+ name="port"
460
+ label="端口号"
461
+ rules={[{ required: true, message: "请输入端口号" }]}
462
+ >
463
+ <InputNumber style={{ width: "100%" }} min={1} max={65535} precision={0} />
464
+ </Form.Item>
465
+ </Col>
466
+ <Col span={12}>
467
+ <Form.Item
468
+ name="status"
469
+ label="状态"
470
+ rules={[{ required: true, message: "请选择状态" }]}
471
+ >
472
+ <Select
473
+ options={[
474
+ { value: "active", label: "启用" },
475
+ { value: "disabled", label: "停用" }
476
+ ]}
477
+ />
478
+ </Form.Item>
479
+ </Col>
480
+ </Row>
481
+ <Form.Item name="description" label="描述">
482
+ <Input.TextArea rows={3} />
483
+ </Form.Item>
484
+ <Form.Item
485
+ name="ontology"
486
+ label="本体压缩包"
487
+ valuePropName="fileList"
488
+ getValueFromEvent={(event) => event?.fileList || []}
489
+ >
490
+ <Upload beforeUpload={() => false} maxCount={1}>
491
+ <Button>选择文件</Button>
492
+ </Upload>
493
+ </Form.Item>
494
+ </Form>
495
+ </Modal>
496
+ </>
497
+ );
498
+ }
@@ -0,0 +1,207 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Button, Card, Form, Input, Modal, Space, Spin, Table, Tag, Upload, message } from "antd";
3
+
4
+ import { api } from "../app/api-client.js";
5
+ import { CardTitleWithReload } from "../components/CardTitleWithReload.jsx";
6
+ import { DeleteActionButton } from "../components/DeleteActionButton.jsx";
7
+ import { formatDateTime } from "../utils/format.js";
8
+
9
+ const templateNamePattern = /^[a-z0-9]+(?:-[a-z0-9]+)*$/;
10
+
11
+ function getTemplateRemoteStatusLabel(status) {
12
+ switch (status) {
13
+ case "ready":
14
+ return "已就绪";
15
+ case "installed":
16
+ return "已安装";
17
+ case "cataloged":
18
+ return "已归档";
19
+ default:
20
+ return status || "-";
21
+ }
22
+ }
23
+
24
+ export function TemplatesPage() {
25
+ const [items, setItems] = useState([]);
26
+ const [loading, setLoading] = useState(false);
27
+ const [open, setOpen] = useState(false);
28
+ const [submitLoading, setSubmitLoading] = useState(false);
29
+ const [submitMaskOpen, setSubmitMaskOpen] = useState(false);
30
+ const [submitMaskText, setSubmitMaskText] = useState("正在处理中,请稍候...");
31
+ const [form] = Form.useForm();
32
+ const [messageApi, contextHolder] = message.useMessage();
33
+
34
+ const load = async () => {
35
+ setLoading(true);
36
+ try {
37
+ const templates = await api.get("/api/templates");
38
+ setItems(templates);
39
+ } finally {
40
+ setLoading(false);
41
+ }
42
+ };
43
+
44
+ useEffect(() => {
45
+ void load();
46
+ }, []);
47
+
48
+ const handleSubmit = async () => {
49
+ try {
50
+ const values = await form.validateFields();
51
+ const artifact = values.artifact?.[0]?.originFileObj;
52
+ if (!artifact) {
53
+ messageApi.error("请上传模板 zip 文件");
54
+ return;
55
+ }
56
+
57
+ const formData = new FormData();
58
+ formData.set("template_name", String(values.template_name || "").trim());
59
+ formData.set("description", String(values.description || ""));
60
+ formData.set("artifact", artifact);
61
+
62
+ setSubmitLoading(true);
63
+ setSubmitMaskText("正在上传并安装全局模板,请稍候...");
64
+ setSubmitMaskOpen(true);
65
+
66
+ await api.multipart("/api/templates", formData);
67
+ setOpen(false);
68
+ form.resetFields();
69
+ await load();
70
+ messageApi.success(`模板 ${values.template_name} 处理完成`);
71
+ } catch (error) {
72
+ if (error?.errorFields) {
73
+ return;
74
+ }
75
+ messageApi.error(error.message || "模板处理失败");
76
+ } finally {
77
+ setSubmitLoading(false);
78
+ setSubmitMaskOpen(false);
79
+ }
80
+ };
81
+
82
+ const handleDelete = async (row) => {
83
+ try {
84
+ setSubmitLoading(true);
85
+ setSubmitMaskText(`正在删除模板 ${row.template_name},请稍候...`);
86
+ setSubmitMaskOpen(true);
87
+ await api.delete(`/api/templates/${row.template_name}`);
88
+ await load();
89
+ messageApi.success(`模板 ${row.template_name} 已删除`);
90
+ } catch (error) {
91
+ messageApi.error(error.message || `删除模板 ${row.template_name} 失败`);
92
+ } finally {
93
+ setSubmitLoading(false);
94
+ setSubmitMaskOpen(false);
95
+ }
96
+ };
97
+
98
+ return (
99
+ <>
100
+ {contextHolder}
101
+ <Modal
102
+ open={submitMaskOpen}
103
+ footer={null}
104
+ closable={false}
105
+ maskClosable={false}
106
+ centered
107
+ width={320}
108
+ >
109
+ <div style={{ display: "flex", alignItems: "center", justifyContent: "center", gap: 12, padding: "16px 0" }}>
110
+ <Spin size="large" />
111
+ <div>{submitMaskText}</div>
112
+ </div>
113
+ </Modal>
114
+
115
+ <Card
116
+ className="page-card"
117
+ title={<CardTitleWithReload title="数字员工模板" loading={loading} onReload={() => load()} />}
118
+ extra={<Button type="primary" onClick={() => setOpen(true)}>上传模板</Button>}
119
+ >
120
+ <Table
121
+ rowKey="template_name"
122
+ loading={loading}
123
+ dataSource={items}
124
+ pagination={{
125
+ pageSize: 50,
126
+ showSizeChanger: false
127
+ }}
128
+ columns={[
129
+ { title: "模板名称", dataIndex: "template_name" },
130
+ {
131
+ title: "类型",
132
+ render: (_, row) => (row.is_builtin ? <Tag color="blue">内置</Tag> : <Tag>自定义</Tag>)
133
+ },
134
+ { title: "描述", dataIndex: "description", render: (value) => value || "-" },
135
+ {
136
+ title: "使用中的数字员工",
137
+ dataIndex: "agent_slugs",
138
+ render: (value = []) => (
139
+ <Space wrap>
140
+ {value.length > 0 ? value.map((item) => <Tag key={item}>{item}</Tag>) : "-"}
141
+ </Space>
142
+ )
143
+ },
144
+ { title: "远端状态", dataIndex: "remote_status", render: getTemplateRemoteStatusLabel },
145
+ { title: "更新时间", dataIndex: "updated_at", render: formatDateTime },
146
+ {
147
+ title: "操作",
148
+ render: (_, row) => (
149
+ <DeleteActionButton
150
+ hidden={row.is_builtin}
151
+ title={`确认删除模板 ${row.template_name} 吗?`}
152
+ description="删除后会移除模板管理记录;如果模板仍被数字员工使用,系统会拒绝删除。"
153
+ onConfirm={async () => {
154
+ await handleDelete(row);
155
+ }}
156
+ />
157
+ )
158
+ }
159
+ ]}
160
+ />
161
+ </Card>
162
+
163
+ <Modal
164
+ open={open}
165
+ title="上传模板"
166
+ okText="确认"
167
+ cancelText="取消"
168
+ confirmLoading={submitLoading}
169
+ onCancel={() => setOpen(false)}
170
+ onOk={async () => {
171
+ await handleSubmit();
172
+ }}
173
+ >
174
+ <Form form={form} layout="vertical">
175
+ <Form.Item
176
+ name="template_name"
177
+ label="模板名称"
178
+ rules={[
179
+ { required: true, message: "请输入模板名称" },
180
+ {
181
+ pattern: templateNamePattern,
182
+ message: "模板名称需符合 Ubuntu 目录名规则:仅允许小写字母、数字和中划线"
183
+ }
184
+ ]}
185
+ >
186
+ <Input />
187
+ </Form.Item>
188
+ <Form.Item name="description" label="描述">
189
+ <Input.TextArea rows={3} />
190
+ </Form.Item>
191
+ <Form.Item
192
+ name="artifact"
193
+ label="模板 zip"
194
+ rules={[{ required: true, message: "请上传模板 zip 文件" }]}
195
+ valuePropName="fileList"
196
+ getValueFromEvent={(event) => event?.fileList || []}
197
+ extra="zip 第一层必须包含 AGENTS.md。"
198
+ >
199
+ <Upload beforeUpload={() => false} maxCount={1}>
200
+ <Button>选择文件</Button>
201
+ </Upload>
202
+ </Form.Item>
203
+ </Form>
204
+ </Modal>
205
+ </>
206
+ );
207
+ }