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,151 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Button, Card, Form, Input, Modal, Space, Table, Tag, 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
+
8
+ export function AiosUsersPage() {
9
+ const [items, setItems] = useState([]);
10
+ const [loading, setLoading] = useState(false);
11
+ const [page, setPage] = useState(1);
12
+ const [pageSize] = useState(50);
13
+ const [total, setTotal] = useState(0);
14
+ const [importForm] = Form.useForm();
15
+ const [importing, setImporting] = useState(false);
16
+ const [drawerOpen, setDrawerOpen] = useState(false);
17
+ const [messageApi, contextHolder] = message.useMessage();
18
+
19
+ const load = async (nextPage = page) => {
20
+ setLoading(true);
21
+ try {
22
+ const result = await api.get(`/api/aios-users?page=${nextPage}&limit=${pageSize}`);
23
+ setItems(result.items || []);
24
+ setTotal(result.total || 0);
25
+ setPage(result.page || nextPage);
26
+ } finally {
27
+ setLoading(false);
28
+ }
29
+ };
30
+
31
+ useEffect(() => {
32
+ void load(1);
33
+ }, []);
34
+
35
+ return (
36
+ <>
37
+ {contextHolder}
38
+ <Card
39
+ className="page-card"
40
+ title={<CardTitleWithReload title="AIOS 用户" loading={loading} onReload={() => load()} />}
41
+ extra={(
42
+ <Button
43
+ type="primary"
44
+ onClick={() => {
45
+ setDrawerOpen(true);
46
+ }}
47
+ >
48
+ 新增用户
49
+ </Button>
50
+ )}
51
+ >
52
+ <Table
53
+ rowKey="id"
54
+ loading={loading}
55
+ pagination={{
56
+ current: page,
57
+ pageSize,
58
+ total,
59
+ showSizeChanger: false,
60
+ onChange: (nextPage) => {
61
+ void load(nextPage);
62
+ }
63
+ }}
64
+ dataSource={items}
65
+ columns={[
66
+ { title: "用户名", dataIndex: "username" },
67
+ {
68
+ title: "已分配的数字员工",
69
+ dataIndex: "assigned_agent_items",
70
+ render: (value = []) => value.length > 0 ? (
71
+ <Space wrap>
72
+ {value.map((item) => (
73
+ <Tag key={item.id || item.slug}>{item.agent_name || item.slug}</Tag>
74
+ ))}
75
+ </Space>
76
+ ) : "-"
77
+ },
78
+ {
79
+ title: "操作",
80
+ render: (_, row) => (
81
+ <DeleteActionButton
82
+ hidden={Number(row.assigned_agents || 0) > 0}
83
+ title={`确认删除用户名 ${row.username} 吗?`}
84
+ description="删除后,该 AIOS 用户会被移除。若仍被数字员工引用,则不允许删除。"
85
+ onConfirm={async () => {
86
+ try {
87
+ await api.delete(`/api/aios-users/${row.id}`);
88
+ messageApi.success(`已删除用户名 ${row.username}`);
89
+ const nextPage = items.length === 1 && page > 1 ? page - 1 : page;
90
+ await load(nextPage);
91
+ } catch (error) {
92
+ messageApi.error(error.message || "删除AIOS用户失败");
93
+ }
94
+ }}
95
+ />
96
+ )
97
+ }
98
+ ]}
99
+ />
100
+ </Card>
101
+ <Modal
102
+ open={drawerOpen}
103
+ title="添加AIOS用户"
104
+ width={520}
105
+ okText="完成"
106
+ cancelText="取消"
107
+ confirmLoading={importing}
108
+ onCancel={() => {
109
+ if (importing) {
110
+ return;
111
+ }
112
+ setDrawerOpen(false);
113
+ importForm.resetFields();
114
+ }}
115
+ onOk={() => {
116
+ void importForm.submit();
117
+ }}
118
+ >
119
+ <Form
120
+ form={importForm}
121
+ layout="vertical"
122
+ onFinish={async (values) => {
123
+ setImporting(true);
124
+ try {
125
+ const result = await api.post("/api/aios-users/import", {
126
+ content: values.content
127
+ });
128
+ importForm.resetFields();
129
+ setDrawerOpen(false);
130
+ messageApi.success(`导入完成,共 ${result.total} 条,新增 ${result.created} 条`);
131
+ await load(1);
132
+ } catch (error) {
133
+ messageApi.error(error.message || "添加AIOS用户失败");
134
+ } finally {
135
+ setImporting(false);
136
+ }
137
+ }}
138
+ >
139
+ <Form.Item
140
+ name="content"
141
+ label="用户列表"
142
+ extra="每行一个用户名,空行会自动忽略。"
143
+ rules={[{ required: true, message: "请输入待添加的用户名列表" }]}
144
+ >
145
+ <Input.TextArea rows={10} placeholder={"zhangsan\nlisi\nwangwu"} />
146
+ </Form.Item>
147
+ </Form>
148
+ </Modal>
149
+ </>
150
+ );
151
+ }
@@ -0,0 +1,72 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Card, Space, Spin, Table, Tag } from "antd";
3
+
4
+ import { api } from "../app/api-client.js";
5
+ import { formatDateTime, formatTokenCount } from "../utils/format.js";
6
+
7
+ const metricLabels = {
8
+ agents: "数字员工数",
9
+ templates: "模板数",
10
+ skills: "技能数",
11
+ systems: "业务系统数",
12
+ today_tokens: "今日 Token 总用量"
13
+ };
14
+
15
+ function formatMetricValue(key, value) {
16
+ if (value === null || value === undefined) {
17
+ return "-";
18
+ }
19
+
20
+ if (key === "today_tokens") {
21
+ return formatTokenCount(value);
22
+ }
23
+
24
+ return value;
25
+ }
26
+
27
+ export function DashboardPage() {
28
+ const [data, setData] = useState(null);
29
+
30
+ useEffect(() => {
31
+ api.get("/api/dashboard").then(setData);
32
+ }, []);
33
+
34
+ if (!data) {
35
+ return <Spin />;
36
+ }
37
+
38
+ return (
39
+ <Space direction="vertical" size="large" style={{ width: "100%" }}>
40
+ <div className="metric-grid">
41
+ {Object.entries(data.stats).map(([key, value]) => (
42
+ <div key={key} className="metric-card">
43
+ <div className="metric-label">{metricLabels[key] || key}</div>
44
+ <div className="metric-value">{formatMetricValue(key, value)}</div>
45
+ </div>
46
+ ))}
47
+ </div>
48
+ <Card className="page-card" title="最近业务系统调用">
49
+ <Table
50
+ rowKey="trace_id"
51
+ pagination={{
52
+ pageSize: 50,
53
+ showSizeChanger: false
54
+ }}
55
+ dataSource={data.recentInvocations}
56
+ columns={[
57
+ { title: "时间", dataIndex: "created_at", render: formatDateTime },
58
+ { title: "应用名", dataIndex: "application_name" },
59
+ { title: "数字员工", dataIndex: "agent_slug", render: (value) => value || "-" },
60
+ { title: "接口", render: (_, row) => `${row.provider}/${row.application_name}/${row.command_name}` },
61
+ { title: "耗时", dataIndex: "response_time_ms", render: (value) => `${value} ms` },
62
+ {
63
+ title: "结果",
64
+ dataIndex: "success",
65
+ render: (value) => (value ? <Tag color="green">成功</Tag> : <Tag color="red">失败</Tag>)
66
+ }
67
+ ]}
68
+ />
69
+ </Card>
70
+ </Space>
71
+ );
72
+ }
@@ -0,0 +1,41 @@
1
+ import React from "react";
2
+ import { Button, Card, Form, Input, Space, Typography } from "antd";
3
+
4
+ const { Paragraph, Title } = Typography;
5
+
6
+ export function LoginPage({ onLogin }) {
7
+ const [form] = Form.useForm();
8
+
9
+ return (
10
+ <div className="login-shell">
11
+ <Card className="login-card">
12
+ <Space direction="vertical" size="large" style={{ width: "100%" }}>
13
+ <div>
14
+ <Title level={3} style={{ marginBottom: 8 }}>
15
+ AIOS 管理控制台
16
+ </Title>
17
+ <Paragraph type="secondary" style={{ marginBottom: 0 }}>
18
+ 默认管理员账号为 `aios`,初始密码为 `123456`。首次登录后系统会要求修改密码。
19
+ </Paragraph>
20
+ </div>
21
+ <Form
22
+ form={form}
23
+ layout="vertical"
24
+ initialValues={{ username: "aios", password: "123456" }}
25
+ onFinish={onLogin}
26
+ >
27
+ <Form.Item name="username" label="用户名" rules={[{ required: true, message: "请输入用户名" }]}>
28
+ <Input />
29
+ </Form.Item>
30
+ <Form.Item name="password" label="密码" rules={[{ required: true, message: "请输入密码" }]}>
31
+ <Input.Password />
32
+ </Form.Item>
33
+ <Button type="primary" htmlType="submit" block>
34
+ 登录
35
+ </Button>
36
+ </Form>
37
+ </Space>
38
+ </Card>
39
+ </div>
40
+ );
41
+ }
@@ -0,0 +1,431 @@
1
+ import React, { useEffect, useState } from "react";
2
+ import { Button, Card, Descriptions, Form, Input, Modal, Space, Spin, Table, Tag, Tabs, Typography } from "antd";
3
+
4
+ import { DeleteActionButton } from "../components/DeleteActionButton.jsx";
5
+
6
+ const { Text } = Typography;
7
+
8
+ function renderTaskStatus(taskStatus) {
9
+ switch (taskStatus?.status) {
10
+ case "success":
11
+ return <Tag color="green">成功</Tag>;
12
+ case "running":
13
+ return <Tag color="blue">进行中</Tag>;
14
+ case "failed":
15
+ return <Tag color="red">失败</Tag>;
16
+ case "skipped":
17
+ return <Tag>跳过</Tag>;
18
+ default:
19
+ return <Tag>空闲</Tag>;
20
+ }
21
+ }
22
+
23
+ function renderSummary(summary, fields) {
24
+ if (!summary || Object.keys(summary).length === 0) {
25
+ return "-";
26
+ }
27
+
28
+ const items = fields
29
+ .filter(({ key }) => summary[key] !== undefined && summary[key] !== null)
30
+ .map(({ key, label }) => `${label} ${summary[key]}`);
31
+
32
+ return items.length > 0 ? items.join(",") : "-";
33
+ }
34
+
35
+ function normalizeOutputText(value) {
36
+ return String(value ?? "")
37
+ .replace(/\r\n/g, "\n")
38
+ .replace(/\\r\\n/g, "\n")
39
+ .replace(/\\n/g, "\n");
40
+ }
41
+
42
+ function stringifyResult(value) {
43
+ if (value === undefined || value === null) {
44
+ return "";
45
+ }
46
+
47
+ if (typeof value === "object" && value !== null && "output" in value && value.output !== undefined && value.output !== null) {
48
+ return normalizeOutputText(value.output);
49
+ }
50
+
51
+ if (typeof value === "string") {
52
+ return normalizeOutputText(value);
53
+ }
54
+
55
+ try {
56
+ return JSON.stringify(value, null, 2);
57
+ } catch {
58
+ return String(value);
59
+ }
60
+ }
61
+
62
+ function SyncStatusCard({ title, taskStatus, buttonLabel, loading, onTrigger, summaryFields }) {
63
+ return (
64
+ <Card
65
+ title={title}
66
+ extra={(
67
+ <Button onClick={onTrigger} loading={loading || taskStatus?.status === "running"}>
68
+ {buttonLabel}
69
+ </Button>
70
+ )}
71
+ >
72
+ <Descriptions bordered column={1} size="small" className="sync-status-descriptions">
73
+ <Descriptions.Item label="当前状态">
74
+ <Space wrap>
75
+ {renderTaskStatus(taskStatus)}
76
+ <Text type="secondary">
77
+ {taskStatus?.trigger_source ? `触发来源:${taskStatus.trigger_source}` : "尚未触发"}
78
+ </Text>
79
+ </Space>
80
+ </Descriptions.Item>
81
+ <Descriptions.Item label="开始时间">
82
+ {taskStatus?.started_at || "-"}
83
+ </Descriptions.Item>
84
+ <Descriptions.Item label="结束时间">
85
+ {taskStatus?.finished_at || "-"}
86
+ </Descriptions.Item>
87
+ <Descriptions.Item label="上次成功时间">
88
+ {taskStatus?.last_success_at || "-"}
89
+ </Descriptions.Item>
90
+ <Descriptions.Item label="同步摘要">
91
+ {renderSummary(taskStatus?.summary, summaryFields)}
92
+ </Descriptions.Item>
93
+ <Descriptions.Item label="最近错误">
94
+ {taskStatus?.error_message || "-"}
95
+ </Descriptions.Item>
96
+ </Descriptions>
97
+ </Card>
98
+ );
99
+ }
100
+
101
+ export function SettingsPage({
102
+ settings,
103
+ accessTokens,
104
+ agentSyncStatus,
105
+ skillSyncStatus,
106
+ templateSyncStatus,
107
+ systemSyncStatus,
108
+ usageRefreshStatus,
109
+ onSave,
110
+ onCreateAccessToken,
111
+ onDeleteAccessToken,
112
+ onAgentSync,
113
+ onSkillSync,
114
+ onTemplateSync,
115
+ onSystemSync,
116
+ onUsageRefresh,
117
+ onServerStatus,
118
+ onServerRestart,
119
+ onServerDiagnostics
120
+ }) {
121
+ const [form] = Form.useForm();
122
+ const [agentSyncing, setAgentSyncing] = useState(false);
123
+ const [skillSyncing, setSkillSyncing] = useState(false);
124
+ const [templateSyncing, setTemplateSyncing] = useState(false);
125
+ const [systemSyncing, setSystemSyncing] = useState(false);
126
+ const [usageRefreshing, setUsageRefreshing] = useState(false);
127
+ const [creatingToken, setCreatingToken] = useState(false);
128
+ const [deletingToken, setDeletingToken] = useState("");
129
+ const [serverCommandLoading, setServerCommandLoading] = useState(false);
130
+ const [serverOutput, setServerOutput] = useState("");
131
+
132
+ useEffect(() => {
133
+ form.setFieldsValue(settings);
134
+ }, [form, settings]);
135
+
136
+ return (
137
+ <>
138
+ <Tabs
139
+ defaultActiveKey="general"
140
+ items={[
141
+ {
142
+ key: "general",
143
+ label: "基础设置",
144
+ children: (
145
+ <Card
146
+ className="page-card"
147
+ title="基础设置"
148
+ extra={(
149
+ <Button type="primary" onClick={() => form.submit()}>
150
+ 保存
151
+ </Button>
152
+ )}
153
+ >
154
+ <Form form={form} layout="vertical" onFinish={onSave}>
155
+ <Form.Item
156
+ name="portal_name"
157
+ label="门户名称"
158
+ rules={[{ required: true, message: "请输入门户名称" }]}
159
+ >
160
+ <Input />
161
+ </Form.Item>
162
+ <Form.Item
163
+ name="brand_subtitle"
164
+ label="副标题"
165
+ rules={[{ required: true, message: "请输入副标题" }]}
166
+ >
167
+ <Input />
168
+ </Form.Item>
169
+ <Form.Item
170
+ name="theme_color"
171
+ label="主题色"
172
+ rules={[{ required: true, message: "请输入主题色" }]}
173
+ >
174
+ <Input />
175
+ </Form.Item>
176
+ </Form>
177
+ <Descriptions bordered column={1} size="small">
178
+ <Descriptions.Item label="管理员密码重置">
179
+ <Text code>npm run reset-password -- aios NewPassword123!</Text>
180
+ </Descriptions.Item>
181
+ <Descriptions.Item label="AIOS内核重置">
182
+ <Text code>npm run reset-kernel -- --confirm-reset --dry-run</Text>
183
+ </Descriptions.Item>
184
+ </Descriptions>
185
+ </Card>
186
+ )
187
+ },
188
+ {
189
+ key: "accessTokens",
190
+ label: "访问凭证",
191
+ children: (
192
+ <Card
193
+ className="page-card"
194
+ title="访问凭证"
195
+ extra={(
196
+ <Button
197
+ type="primary"
198
+ loading={creatingToken}
199
+ onClick={async () => {
200
+ setCreatingToken(true);
201
+ try {
202
+ await onCreateAccessToken();
203
+ } finally {
204
+ setCreatingToken(false);
205
+ }
206
+ }}
207
+ >
208
+ 新增随机 Token
209
+ </Button>
210
+ )}
211
+ >
212
+ <Table
213
+ rowKey="token"
214
+ dataSource={accessTokens}
215
+ pagination={false}
216
+ columns={[
217
+ {
218
+ title: "Token",
219
+ dataIndex: "token",
220
+ render: (value) => <Text code>{value}</Text>
221
+ },
222
+ {
223
+ title: "创建时间",
224
+ dataIndex: "created_at",
225
+ render: (value) => value || "-"
226
+ },
227
+ {
228
+ title: "操作",
229
+ render: (_, row) => (
230
+ <DeleteActionButton
231
+ title="确认删除这个 Access Token 吗?"
232
+ description="删除后依赖该 Token 的调用会立即失效。"
233
+ onConfirm={async () => {
234
+ setDeletingToken(row.token);
235
+ try {
236
+ await onDeleteAccessToken(row.token);
237
+ } finally {
238
+ setDeletingToken("");
239
+ }
240
+ }}
241
+ >
242
+ {deletingToken === row.token ? "删除中..." : "删除"}
243
+ </DeleteActionButton>
244
+ )
245
+ }
246
+ ]}
247
+ />
248
+ </Card>
249
+ )
250
+ },
251
+ {
252
+ key: "syncStatus",
253
+ label: "定时任务",
254
+ children: (
255
+ <Space direction="vertical" size={16} style={{ width: "100%" }}>
256
+ <Card className="page-card" title="定时任务">
257
+ <Space direction="vertical" size={16} style={{ width: "100%" }}>
258
+ <SyncStatusCard
259
+ title="同步员工用量"
260
+ taskStatus={usageRefreshStatus}
261
+ buttonLabel="立即同步"
262
+ loading={usageRefreshing}
263
+ onTrigger={async () => {
264
+ setUsageRefreshing(true);
265
+ try {
266
+ await onUsageRefresh();
267
+ } finally {
268
+ setUsageRefreshing(false);
269
+ }
270
+ }}
271
+ summaryFields={[
272
+ { key: "refreshed_agents", label: "刷新数字员工" },
273
+ { key: "captured_at", label: "采集时间" }
274
+ ]}
275
+ />
276
+ <SyncStatusCard
277
+ title="同步数字员工"
278
+ taskStatus={agentSyncStatus}
279
+ buttonLabel="立即同步"
280
+ loading={agentSyncing}
281
+ onTrigger={async () => {
282
+ setAgentSyncing(true);
283
+ try {
284
+ await onAgentSync();
285
+ } finally {
286
+ setAgentSyncing(false);
287
+ }
288
+ }}
289
+ summaryFields={[
290
+ { key: "agents", label: "数字员工" }
291
+ ]}
292
+ />
293
+ <SyncStatusCard
294
+ title="同步技能"
295
+ taskStatus={skillSyncStatus}
296
+ buttonLabel="立即同步"
297
+ loading={skillSyncing}
298
+ onTrigger={async () => {
299
+ setSkillSyncing(true);
300
+ try {
301
+ await onSkillSync();
302
+ } finally {
303
+ setSkillSyncing(false);
304
+ }
305
+ }}
306
+ summaryFields={[
307
+ { key: "skills", label: "技能" }
308
+ ]}
309
+ />
310
+ <SyncStatusCard
311
+ title="同步模板"
312
+ taskStatus={templateSyncStatus}
313
+ buttonLabel="立即同步"
314
+ loading={templateSyncing}
315
+ onTrigger={async () => {
316
+ setTemplateSyncing(true);
317
+ try {
318
+ await onTemplateSync();
319
+ } finally {
320
+ setTemplateSyncing(false);
321
+ }
322
+ }}
323
+ summaryFields={[
324
+ { key: "templates", label: "模板" }
325
+ ]}
326
+ />
327
+ <SyncStatusCard
328
+ title="同步业务系统"
329
+ taskStatus={systemSyncStatus}
330
+ buttonLabel="立即同步"
331
+ loading={systemSyncing}
332
+ onTrigger={async () => {
333
+ setSystemSyncing(true);
334
+ try {
335
+ await onSystemSync();
336
+ } finally {
337
+ setSystemSyncing(false);
338
+ }
339
+ }}
340
+ summaryFields={[
341
+ { key: "systems", label: "业务系统" }
342
+ ]}
343
+ />
344
+ </Space>
345
+ </Card>
346
+ </Space>
347
+ )
348
+ },
349
+ {
350
+ key: "serverStatus",
351
+ label: "服务器状态",
352
+ children: (
353
+ <Card
354
+ className="page-card"
355
+ title="服务器状态"
356
+ extra={(
357
+ <Space wrap>
358
+ <Button
359
+ onClick={async () => {
360
+ setServerCommandLoading(true);
361
+ try {
362
+ const result = await onServerStatus();
363
+ setServerOutput(stringifyResult(result));
364
+ } finally {
365
+ setServerCommandLoading(false);
366
+ }
367
+ }}
368
+ >
369
+ 查询状态
370
+ </Button>
371
+ <Button
372
+ onClick={async () => {
373
+ setServerCommandLoading(true);
374
+ try {
375
+ const result = await onServerDiagnostics();
376
+ setServerOutput(stringifyResult(result));
377
+ } finally {
378
+ setServerCommandLoading(false);
379
+ }
380
+ }}
381
+ >
382
+ 执行诊断
383
+ </Button>
384
+ <Button
385
+ danger
386
+ onClick={() => {
387
+ Modal.confirm({
388
+ title: "确认重启服务吗?",
389
+ content: "重启期间可能短时无法访问,请确认后继续。",
390
+ okText: "确认重启",
391
+ cancelText: "取消",
392
+ okButtonProps: { danger: true },
393
+ onOk: async () => {
394
+ setServerCommandLoading(true);
395
+ try {
396
+ await onServerRestart();
397
+ } finally {
398
+ setServerCommandLoading(false);
399
+ }
400
+ }
401
+ });
402
+ }}
403
+ >
404
+ 重启服务
405
+ </Button>
406
+ </Space>
407
+ )}
408
+ >
409
+ <div className="server-command-output">
410
+ <Input.TextArea
411
+ readOnly
412
+ autoSize={{ minRows: 12 }}
413
+ value={serverOutput}
414
+ placeholder="执行结果会显示在这里"
415
+ className="server-command-output-textarea"
416
+ />
417
+ </div>
418
+ </Card>
419
+ )
420
+ }
421
+ ]}
422
+ />
423
+ <Modal open={serverCommandLoading} footer={null} closable={false} centered maskClosable={false}>
424
+ <div className="server-command-loading">
425
+ <Spin size="large" />
426
+ <Text>命令执行中,请稍候...</Text>
427
+ </div>
428
+ </Modal>
429
+ </>
430
+ );
431
+ }