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 @@
1
+ html,body,#root{margin:0;min-height:100%;background:#f3f7f5}body{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica Neue,Arial,Noto Sans,sans-serif,"Apple Color Emoji","Segoe UI Emoji",Segoe UI Symbol,"Noto Color Emoji";color:#172b1d}.app-shell{min-height:100vh}.portal-sider .ant-layout-sider-children{display:flex;flex-direction:column}.brand-block{padding:20px 18px 12px;color:#fff;font-weight:700;line-height:1.5;min-width:0}.brand-title,.brand-subtitle{display:block;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.brand-subtitle{color:#ffffffb3!important}.portal-sider-menu{flex:0 0 auto}.portal-header{background:#fff;padding:0 20px;border-bottom:1px solid #e2ede5;display:flex;justify-content:flex-end;align-items:center;gap:16px}.portal-header-actions{flex:0 0 auto}.portal-content{padding:20px;min-width:0}.page-card{box-shadow:0 10px 30px #164a2c14}.page-card .ant-card-head{min-height:64px}.page-card .ant-card-head-wrapper{gap:16px;align-items:center}.page-card .ant-card-head-title{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.page-card .ant-card-extra{flex:0 0 auto;margin-inline-start:12px}.sync-status-descriptions .ant-descriptions-view table{table-layout:fixed}.sync-status-descriptions .ant-descriptions-item-label{width:140px}.metric-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:16px;margin-bottom:20px}.metric-card{padding:18px;border-radius:6px;border:1px solid #d9eadf;background:linear-gradient(135deg,#fff,#eef9f1)}.metric-label{font-size:13px;color:#5a6f5f}.metric-value{margin-top:10px;font-size:28px;font-weight:700}.table-toolbar{display:flex;gap:12px;justify-content:space-between;flex-wrap:wrap;margin-bottom:16px}.login-shell{min-height:100vh;display:grid;place-items:center;background:radial-gradient(circle at top right,rgba(7,193,96,.18),transparent 28%),radial-gradient(circle at bottom left,rgba(14,174,87,.12),transparent 24%),linear-gradient(180deg,#f8fffa,#edf7f0)}.login-card{width:min(460px,calc(100vw - 32px));box-shadow:0 20px 60px #14532d1f}.thread-list{display:grid;gap:10px}.thread-item{padding:12px;border:1px solid #d9eadf;border-radius:6px;background:#fff;cursor:pointer}.thread-item:hover{border-color:#07c160}.message-stack{display:grid;gap:10px}.message-card{padding:12px 14px;border-radius:6px;background:#f8fffa;border:1px solid #d9eadf}.message-card.outbound{background:#f0fff6;border-color:#b9ebc9}.server-command-output{margin-top:16px}.server-command-output .ant-input{font-family:Courier New,Courier,monospace;min-height:300px!important}.server-command-output-textarea.ant-input{background:#111;color:#fff}.server-command-output-textarea.ant-input::placeholder{color:#ffffff8c}.server-command-loading{min-height:160px;display:grid;place-items:center;gap:16px}@media(max-width:960px){.portal-header{padding:12px;height:auto;align-items:flex-start}.portal-header-actions{align-self:stretch}.portal-content{padding:12px}}
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AIOS 企业级 AI 门户</title>
7
+ <script type="module" crossorigin src="/assets/index-CV_wjCAG.js"></script>
8
+ <link rel="stylesheet" crossorigin href="/assets/index-DfMPB0eV.css">
9
+ </head>
10
+ <body>
11
+ <div id="root"></div>
12
+ </body>
13
+ </html>
package/docs/spec.md ADDED
@@ -0,0 +1,199 @@
1
+ # AIOS 管理控制台
2
+
3
+ 本项目是 AIOS 内建的运维管理应用,通常部署在局域网环境中的 Docker 内,通过 MQTT Broker 和 S3 存储服务与位于 DMZ 的 `aios-management-serivce` 通信,为企业 IT 人员提供数字员工运营监控和管理能力。
4
+
5
+ 约束如下:
6
+
7
+ - Web 端与 `aios-management-serivce` 之间只允许通过 MQTT 和 S3 交互。
8
+ - 不允许 Web 端与 `aios-management-serivce` 直连,不允许额外增加 HTTP 私有控制通道。
9
+ - 不需要兼容旧实现,可以直接按新架构和新数据模型设计。
10
+
11
+ ## 需求概要
12
+
13
+ ### 基础概念:数字员工
14
+
15
+ 数字员工是本系统的核心管理对象。人类员工与数字员工交互的方式类似即时通信:
16
+
17
+ - 人类员工给数字员工发送消息,支持文本和文件。
18
+ - 数字员工返回消息。
19
+ - 管理员可以查询聊天历史。
20
+ - 数字员工回答时需要参考上下文会话历史。
21
+
22
+ 本项目只负责数字员工的管理与监控,不负责终端使用侧产品。
23
+
24
+ 在物理实现层面,一个数字员工对应 AIOS 的一个 `agent`。
25
+
26
+ ### 认证与用户管理
27
+
28
+ - 只支持本地用户名密码登录。
29
+ - 不支持第三方用户登录,不支持第三方鉴权模式。
30
+ - 用户管理中不需要邮箱字段。
31
+ - 用户管理中不需要“来源”字段。
32
+ - 管理站登录账号当前只支持 `aios-admin` 管理员角色。
33
+ - AIOS 终端用户独立存储在 `aios_users` 目录中,用于数字员工授权,不作为管理站登录账号。
34
+ - 管理员可以创建、编辑、删除非内置管理员账号。
35
+ - 管理员可以重置非内置管理员密码。
36
+ - 用户可以修改自己的密码。
37
+ - 内置管理员用户为 `aios`,默认密码 `123456`。
38
+ - 如果检测到 `aios` 使用默认密码登录,系统必须要求其修改密码。
39
+ - `aios` 是内置账号,不允许删除。
40
+ - 需要支持在服务器上通过命令行重置 `aios` 密码。
41
+
42
+ ### 数字员工模板管理
43
+
44
+ 数字员工模板由模板名和工作区 zip 包构成,zip 顶层至少包含 `AGENTS.md`。
45
+
46
+ - 管理员可以创建、删除、查询模板。
47
+ - 管理员可以查看每个模板被哪些数字员工使用。
48
+
49
+ ### 技能管理
50
+
51
+ 技能当前作为全局技能管理,仅支持上传本地 zip 包,zip 顶层至少包含 `SKILL.md`。
52
+
53
+ - 管理员可以创建、删除、查询技能。
54
+ - 上传后通过 `aios-management-serivce` 安装为全局技能,对所有 agent 可见。
55
+ - 当前管理站不提供按单个数字员工安装或移除非全局技能的操作入口。
56
+
57
+ ### 数字员工管理
58
+
59
+ - 管理员可以创建数字员工,模板默认使用 `default`。
60
+ - 数字员工的 `slug` 和名称为必填项。
61
+ - 当前 UI 支持编辑名称、状态、人员分配和每日 Token 限额。
62
+ - 管理员可以停用或启用数字员工。
63
+ - 管理员可以设置每个数字员工的每日 Token 用量上限。
64
+ - 超过每日上限后,状态显示为“超限”,并从 CUI 可用目录中排除。
65
+ - 当前未实现周/月/永久限额、预警阈值和按小时聚合展示。
66
+
67
+ ### 数字员工审计
68
+
69
+ - 管理员可以查看数字员工状态、入站/出站主题和今日 Token 用量。
70
+ - 业务系统调用日志可按业务系统或数字员工过滤。
71
+ - 当前未实现统一聊天历史、互动文件检索和员工维度对话审计。
72
+
73
+ ### 业务系统管理
74
+
75
+ 数字员工可以操作接入 AI 门户的业务系统。
76
+
77
+ - 管理员可以录入业务系统类型,目前数据模型接受 `hzg` 和 `phx`,当前调用链路只支持 `hzg`。
78
+ - 业务系统信息包括 `application_name`、描述、Ontology 包、协议、主机、端口。
79
+ - `system_id`、Ontology 名称、服务接入点和会话服务接入点当前由 `application_name` 与协议/主机/端口派生。
80
+ - Ontology 包为 zip 文件,顶层包含 `index.md`。
81
+ - 管理员可以创建、删除、停用、启用、查询业务系统。
82
+ - 管理员可以更新业务系统 Ontology。
83
+ - 管理员可以测试业务系统连通性。
84
+ - 连通性测试当前向派生出的单一 `baseUrl` 发起 GET 请求,返回 2xx 视为成功。
85
+
86
+ ### 业务系统审计
87
+
88
+ - 管理员可以查看业务系统调用统计
89
+ - 统计维度包括每个接口 `provider / applicationName / commandName` 的调用次数、成功率、平均响应时间
90
+ - 统计口径支持全部数字员工和指定数字员工
91
+ - 管理员可以查看调用日志,包括接口、输入参数、会话、响应时间、成功状态、返回内容、错误信息
92
+
93
+ ## 对 Proxy 提供的接口
94
+
95
+ 为 `aios-app-invoke-proxy-service` 提供服务。
96
+
97
+ ### 获取 Cookie
98
+
99
+ - 谓词:GET
100
+ - Url:/api/external/cookie
101
+ - 输入参数:sessionId, provider
102
+ - 返回结构:`{"provider":"hzg","cookie":"Foun..."}`
103
+ - 行为:读取数据库中的会话信息,返回符合要求sessionId和provider的Cookie。如果没有找到,则返回 `httpcode` 为404
104
+ - 审计日志:无
105
+
106
+ ### 获取业务系统调用信息
107
+
108
+ - 谓词:GET
109
+ - Url:`/api/internal/app-invoke/system`
110
+ - 输入参数:`provider`、`applicationName`
111
+ - 返回结构:业务系统记录,包含 `service_endpoint`、`session_endpoint`、`base_url`
112
+ - 行为:读取状态为 `active` 的业务系统信息,按 `provider` 和 `applicationName` 精确匹配。如果没有找到,则返回 `httpcode` 为 404。
113
+
114
+ ### 获取 BaseUrl
115
+
116
+ - 谓词:GET
117
+ - Url:`/api/internal/app-invoke/base-url`
118
+ - 输入参数:`applicationName`
119
+ - 返回结构:`{"provider":"hzg","baseUrl":"https://xxxx:8091/app1"}`
120
+ - 行为:读取状态为 `active` 的业务系统信息,基于 `applicationName` 拼接 `baseUrl` 并返回,地址不包含结尾的 `/`。如果没有找到,则返回 `httpcode` 为 404。
121
+
122
+ ### 记录请求日志
123
+
124
+ - 谓词:POST
125
+ - Url:/api/external/invoke/logging
126
+ - 输入参数:`{"sessionId":"xxxx","provider":"hzg","applicationName":"wms","commandName":"GetDataTableWithOffset","paramaters":"","isOK":false,"errorMessage":"xxxx","response":"","durationInMS":500}`
127
+ - 返回结构:空
128
+ - 行为:将请求信息写入数据库
129
+ - 审计日志:无
130
+
131
+ ## 对于 CUI 提供的接口
132
+
133
+ 为 `cui` 系统提供服务
134
+
135
+ ### 获取 Agent 列表
136
+
137
+ - 谓词:GET
138
+ - Url:/api/external/agents
139
+ - 输入参数:userName
140
+ - 返回结构:`{"items":[{"agent_id":"dev-tester","agent_name":"dev-tester","status":"normal","inbound_topic":"aios-dev1/agent/dev-tester/inbound","outbound_topic":"aios-dev1/agent/dev-tester/outbound","users":["ai","bi"]}]}}`
141
+ - 行为:从数据库中读取数字员工列表,返回指定用户有权限使用的全部数字员工列表,包含 `normal`、`disabled`、`overlimit` 三种状态
142
+ - 审计日志:无
143
+
144
+ ### 获取 Session
145
+
146
+ - 谓词:GET
147
+ - Url:/api/external/session
148
+ - 输入参数:userName, agentId, cookie
149
+ - 返回结构:`{"sessionId":"s-xxxxx","inboundTopic":"xxxxx","outboundTopic":"xxxx"}`
150
+ - 行为:先校验数字员工存在且状态为 `normal`;如果状态不是 `normal`,返回 HTTP 500,`message` 中提示停用、超限或其它非 normal 状态原因。校验通过后读取数据库中的会话信息,如果存在该用户操作该数字员工的会话,就返回会话ID,否则生成一个随机的会话ID,在数据库中插入会话信息后,将该会话ID返回
151
+ - 审计日志:新插入会话信息时,需要记录审计日志
152
+
153
+ ### 获取上下文
154
+
155
+ - 谓词:GET
156
+ - Url:/api/external/context
157
+ - 输入参数:sessionId
158
+ - 返回结构:`{"inboundTopic":"","outboundTopic":"","agentName":"","agentId":"","userName":""}`
159
+ - 行为:读取数据库中的会话信息和数字员工信息,组装出数据结构返回,如果没有找到,则返回 `httpcode` 为404
160
+
161
+ ## 定时任务
162
+
163
+ ### 刷新 Agent / Skill / Template 列表
164
+
165
+ - 触发时机:应用启动时
166
+ - 行为:发送指令给 [aios-management-serivce](../../../kernal/aios-management-serivce/README.md) 从 OpenClaw 获取对应的信息,然后保存到数据库,全额替换数据库中既存数据
167
+ - 审计日志:本操作需要记录审计日志
168
+
169
+ ### 获取 Agent 的 Token 用量
170
+
171
+ - 触发时机:每10分钟
172
+ - 行为:发送指令给 [aios-management-serivce](../../../kernal/aios-management-serivce/README.md) 从 OpenClaw 获取每个 Agent 的最新 Token 用量,保存到 `agents.usage_snapshot_json`,并据此刷新每日限额状态。
173
+ - 审计日志:本操作需要记录审计日志
174
+
175
+ ## 技术方案
176
+
177
+ ### 技术栈
178
+
179
+ - Node.js
180
+ - Express
181
+ - SQLite
182
+ - React
183
+ - Ant Design
184
+
185
+ ### Agent 执行层接口
186
+
187
+ 通过 MQTT 与 [aios-management-serivce](../../../kernal/aios-management-serivce/README.md) 和 [aios-mqtt-channel](../../../kernal/openclaw-plugins/aios-mqtt-channel/README.md) 交互。
188
+
189
+ 文件传输通过 S3 兼容接口完成,用于模板、技能、Ontology 等制品交换。
190
+
191
+ ### 鉴权
192
+
193
+ 管理站界面采用本地用户名密码登录,登录成功后由前端携带会话 Token 访问管理 API。面向服务的 `/api/internal/*` 和 `/api/external/*` 接口采用 Bearer Token 认证,Token 存储在 SQLite 并通过设置页管理。localhost 调用 API 时跳过认证检查。
194
+
195
+ ### 设计风格
196
+
197
+ - 基于 Ant Design
198
+ - 品牌中性化设计
199
+ - 主题色默认采用微信绿系
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="zh-CN">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>AIOS 企业级 AI 门户</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/main.jsx"></script>
11
+ </body>
12
+ </html>
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "aios-management-web",
3
+ "version": "0.1.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "vite",
7
+ "start": "node server/index.js",
8
+ "server": "node server/index.js",
9
+ "build": "vite build",
10
+ "preview": "vite preview",
11
+ "reset-kernel": "node scripts/reset-kernel.js",
12
+ "reset-password": "node scripts/reset-password.js",
13
+ "test": "node test/run-jasmine.js",
14
+ "test:unit": "node test/run-jasmine.js",
15
+ "test:e2e-browser": "node test/run-browser-e2e.js",
16
+ "test:fast": "node test/integration/fast.js",
17
+ "test:full": "node test/integration/full.js"
18
+ },
19
+ "dependencies": {
20
+ "@ant-design/icons": "^6.0.0",
21
+ "@aws-sdk/client-s3": "^3.907.0",
22
+ "adm-zip": "^0.5.16",
23
+ "antd": "^5.27.0",
24
+ "dayjs": "^1.11.13",
25
+ "express": "^5.1.0",
26
+ "mqtt": "^5.15.1",
27
+ "multer": "^2.0.2",
28
+ "react": "^19.1.0",
29
+ "react-dom": "^19.1.0"
30
+ },
31
+ "devDependencies": {
32
+ "@vitejs/plugin-react": "^5.0.1",
33
+ "jasmine": "^5.13.0",
34
+ "playwright-core": "^1.60.0",
35
+ "vite": "^7.0.0"
36
+ }
37
+ }
@@ -0,0 +1,59 @@
1
+ import { pathToFileURL } from "node:url";
2
+
3
+ import { db } from "../server/src/db/index.js";
4
+ import { loadEnv } from "../server/src/config/env.js";
5
+ import { ManagementRpcClient } from "../server/src/infra/mqtt/management-rpc-client.js";
6
+ import { KernelResetService } from "../server/src/services/kernel-reset-service.js";
7
+
8
+ export function parseResetKernelArgs(argv) {
9
+ const args = Array.isArray(argv) ? argv : [];
10
+ return {
11
+ dryRun: args.includes("--dry-run"),
12
+ confirmed: args.includes("--confirm-reset")
13
+ };
14
+ }
15
+
16
+ function printPlan(result) {
17
+ console.log(`Agents: ${result.deletedAgents.length}`);
18
+ console.log(`Ontologies: ${result.deletedOntologies.length}`);
19
+ console.log(`Templates: ${result.deletedTemplates.length}`);
20
+ }
21
+
22
+ export async function runResetKernelCli(argv = process.argv.slice(2)) {
23
+ const options = parseResetKernelArgs(argv);
24
+ if (!options.confirmed) {
25
+ console.error("Refusing to reset kernel without --confirm-reset.");
26
+ console.error("Use --dry-run to preview what would be deleted.");
27
+ process.exitCode = 1;
28
+ return;
29
+ }
30
+
31
+ const env = loadEnv();
32
+ const rpcClient = new ManagementRpcClient({ env, db });
33
+ const service = new KernelResetService({ rpcClient });
34
+
35
+ try {
36
+ await rpcClient.start();
37
+ const result = await service.resetKernel({ dryRun: options.dryRun });
38
+
39
+ if (result.dryRun) {
40
+ console.log("AIOS kernel reset dry run.");
41
+ printPlan(result);
42
+ return;
43
+ }
44
+
45
+ console.log("AIOS kernel reset completed.");
46
+ printPlan(result);
47
+ } catch (error) {
48
+ console.error(error?.message || String(error));
49
+ process.exitCode = 1;
50
+ } finally {
51
+ await rpcClient.stop().catch(() => {});
52
+ }
53
+ }
54
+
55
+ const isDirectRun = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
56
+
57
+ if (isDirectRun) {
58
+ await runResetKernelCli();
59
+ }
@@ -0,0 +1,22 @@
1
+ import { db } from "../server/src/db/index.js";
2
+ import { hashPassword } from "../server/src/utils/security.js";
3
+
4
+ const [, , username, password] = process.argv;
5
+
6
+ if (!username || !password) {
7
+ console.error("Usage: npm run reset-password -- <username> <newPassword>");
8
+ process.exit(1);
9
+ }
10
+
11
+ const result = db.prepare(`
12
+ UPDATE users
13
+ SET password_hash = ?, must_change_password = 1, updated_at = ?
14
+ WHERE username = ? AND role = 'admin'
15
+ `).run(hashPassword(password), new Date().toISOString(), username);
16
+
17
+ if (!result.changes) {
18
+ console.error(`Admin user not found: ${username}`);
19
+ process.exit(1);
20
+ }
21
+
22
+ console.log(`Password reset completed for admin user ${username}`);
@@ -0,0 +1,57 @@
1
+ import { EventEmitter } from "node:events";
2
+
3
+ export function createFakeDb() {
4
+ const statements = [];
5
+
6
+ return {
7
+ statements,
8
+ prepare(sql) {
9
+ return {
10
+ run: (...args) => {
11
+ statements.push({ sql, args });
12
+ return { lastInsertRowid: 1 };
13
+ },
14
+ get: () => null,
15
+ all: () => []
16
+ };
17
+ }
18
+ };
19
+ }
20
+
21
+ export function createFakeMqttFactory() {
22
+ const clients = [];
23
+
24
+ return {
25
+ clients,
26
+ connect() {
27
+ const client = new EventEmitter();
28
+ client.connected = false;
29
+ client.subscriptions = [];
30
+ client.publishes = [];
31
+ client.ended = false;
32
+
33
+ client.subscribe = (topics, options, callback) => {
34
+ client.subscriptions.push({ topics, options });
35
+ callback?.(null);
36
+ };
37
+
38
+ client.publish = (topic, payload, options, callback) => {
39
+ client.publishes.push({ topic, payload, options });
40
+ callback?.(null);
41
+ };
42
+
43
+ client.end = (_force, _options, callback) => {
44
+ client.ended = true;
45
+ callback?.();
46
+ };
47
+
48
+ clients.push(client);
49
+ queueMicrotask(() => {
50
+ client.connected = true;
51
+ client.emit("connect");
52
+ });
53
+
54
+ return client;
55
+ }
56
+ };
57
+ }
@@ -0,0 +1,21 @@
1
+ import { createServerApp } from "./src/app.js";
2
+ import { startBackgroundServices } from "./src/background/index.js";
3
+
4
+ process.on("unhandledRejection", (error) => {
5
+ console.error("Unhandled promise rejection", error);
6
+ });
7
+
8
+ const { app, env, services } = createServerApp();
9
+ const background = startBackgroundServices({ env, services });
10
+
11
+ const server = app.listen(env.port, () => {
12
+ console.log(`AIOS management console listening on http://localhost:${env.port}`);
13
+ });
14
+
15
+ for (const signal of ["SIGINT", "SIGTERM"]) {
16
+ process.on(signal, async () => {
17
+ server.close();
18
+ await background.stop();
19
+ process.exit(0);
20
+ });
21
+ }
@@ -0,0 +1,29 @@
1
+ import { unauthorized } from "../../utils/errors.js";
2
+ import { isLoopbackAddress } from "./internal.js";
3
+
4
+ export function createAuthMiddleware(authService) {
5
+ return (req, _res, next) => {
6
+ const remoteAddress = String(req.socket?.remoteAddress || req.ip || "").trim();
7
+ if (isLoopbackAddress(remoteAddress)) {
8
+ req.authToken = "localhost";
9
+ req.currentUser = authService.getLocalApiUser();
10
+ next();
11
+ return;
12
+ }
13
+
14
+ const authHeader = req.headers.authorization || "";
15
+ const token = authHeader.startsWith("Bearer ") ? authHeader.slice("Bearer ".length) : "";
16
+ if (!token) {
17
+ next(unauthorized());
18
+ return;
19
+ }
20
+
21
+ try {
22
+ req.authToken = token;
23
+ req.currentUser = authService.getSessionUser(token);
24
+ next();
25
+ } catch (error) {
26
+ next(error);
27
+ }
28
+ };
29
+ }
@@ -0,0 +1,44 @@
1
+ import { forbidden, unauthorized } from "../../utils/errors.js";
2
+
3
+ export function isLoopbackAddress(address = "") {
4
+ const normalized = String(address || "").trim();
5
+ return normalized === "127.0.0.1"
6
+ || normalized === "::1"
7
+ || normalized === "::ffff:127.0.0.1";
8
+ }
9
+
10
+ function getBearerToken(req) {
11
+ const authHeader = String(req.headers.authorization || "").trim();
12
+ if (!authHeader.startsWith("Bearer ")) {
13
+ return "";
14
+ }
15
+
16
+ return authHeader.slice("Bearer ".length).trim();
17
+ }
18
+
19
+ export function createServiceApiAuthMiddleware(portalService) {
20
+ return (req, _res, next) => {
21
+ const remoteAddress = String(req.socket?.remoteAddress || req.ip || "").trim();
22
+ if (isLoopbackAddress(remoteAddress)) {
23
+ next();
24
+ return;
25
+ }
26
+
27
+ const presentedToken = getBearerToken(req);
28
+ if (!presentedToken) {
29
+ next(unauthorized("缺少 Bearer Token"));
30
+ return;
31
+ }
32
+
33
+ if (!portalService?.hasAccessToken(presentedToken)) {
34
+ next(forbidden("Invalid bearer token"));
35
+ return;
36
+ }
37
+
38
+ next();
39
+ };
40
+ }
41
+
42
+ export function createInternalAuthMiddleware(portalService) {
43
+ return createServiceApiAuthMiddleware(portalService);
44
+ }