From 38fa5824edb6868f80c63be007b113b9072bb085 Mon Sep 17 00:00:00 2001 From: NingWei Date: Mon, 6 Jul 2026 14:58:49 +0800 Subject: [PATCH 1/2] =?UTF-8?q?=E4=BC=98=E5=8C=96=E4=B8=9A=E5=8A=A1?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E9=85=8D=E7=BD=AE=E5=92=8C=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E6=96=B9=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 11 +- docker/Dockerfile | 4 +- docker/README.md | 5 +- docker/docs/environment-variables.md | 4 +- docker/sample/env.json | 1 + .../aios-call-app-service/SKILL.md | 18 +- .../aios-call-app-service/readme.md | 5 +- .../references/invoke-rules.md | 14 +- packages/aios-apps-invoke-cli/README.md | 23 +- packages/aios-apps-invoke-cli/docs/design.md | 42 +- packages/aios-apps-invoke-cli/package.json | 2 +- .../src/client/socket-client.ts | 26 +- .../aios-apps-invoke-cli/src/commands/root.ts | 179 +++++--- packages/aios-apps-invoke-cli/src/index.ts | 1 - .../src/protocol/messages.ts | 5 - .../aios-apps-invoke-cli/src/proxy/handler.ts | 20 +- .../src/proxy/hzg-provider-client.ts | 12 +- .../aios-apps-invoke-cli/src/proxy/types.ts | 9 +- .../src/proxy/web-api-client.ts | 12 +- .../test/proxy/socket-service.test.ts | 4 +- .../test/proxy/web-api-client.test.ts | 14 +- .../aios-apps-invoke-cli/test/root.test.ts | 52 +-- packages/aios-management-web/package.json | 2 +- .../server/src/api/routes/index.js | 31 +- .../aios-management-web/server/src/app.js | 2 +- .../server/src/config/env.js | 5 + .../server/src/db/index.js | 111 ++++- .../server/src/services/external-service.js | 42 +- .../server/src/services/portal-service.js | 4 +- .../services/runtime-env-config-service.js | 101 ++++- .../server/src/services/system-service.js | 426 +++++++++++++++--- .../server/test/db-reset-migration.test.js | 33 +- .../server/test/env-config.test.js | 16 + .../server/test/external-service.test.js | 19 +- .../test/runtime-env-config-service.test.js | 36 +- .../test/runtime-settings-route.test.js | 6 +- .../test/system-service-alignment.test.js | 101 ++++- .../src/pages/SettingsPage.jsx | 53 ++- .../src/pages/SystemsPage.jsx | 67 +-- .../src/pages/system-logs/SystemLogsTabs.jsx | 82 +++- .../test/integration/fast.js | 9 +- .../test/integration/full.js | 14 +- 42 files changed, 1190 insertions(+), 433 deletions(-) diff --git a/README.md b/README.md index b312093..6a7fdc4 100644 --- a/README.md +++ b/README.md @@ -51,16 +51,17 @@ http://127.0.0.1:3030 - 名称 - 上下文长度、输入上限等参数 -如果模型服务位于内网,需要在在管理控制台 `系统设置` 页面中,将该服务器的地址(如 `172.16.11.12`)添加到白名单。需要注意的是,白名单的变更需要重启容器方可生效,而不是重启服务。 +如果模型服务位于内网,需要在管理控制台 `系统设置` 页面中,将该服务器的地址(如 `172.16.11.12`)添加到白名单。需要注意的是,白名单的变更需要重启容器方可生效,而不是重启服务。 ## 步骤4:接入业务系统 -在“业务系统”菜单中添加接入 AIOS 的业务系统。活字格系统通常需要填写: +首先,在管理控制台 `系统设置` 页面中,将业务系统和对话应用所在的服务器 IP 地址(对话应用和业务系统需要部署在同一台服务器中)设置到 `对话应用/业务应用服务器`。因为修改后该配置后,系统会同步调整 IP 白名单,需重启容器后生效。 + +然后,在“业务系统”菜单中添加接入的业务系统。活字格系统通常需要填写: -- 提供方:`hzg` - 应用名 -- 协议、主机名、端口 -- 状态 +- 端口 +- 是否启用 HTTPS - Ontology zip Ontology zip 需通过 `huozige-ontology-builder` 构建,[点击阅读使用指南](https://www.npmjs.com/package/huozige-ontology-builder)。 diff --git a/docker/Dockerfile b/docker/Dockerfile index a611593..14d79ca 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -2,8 +2,8 @@ ARG OPENCLAW_VERSION=2026.6.10 ARG AEDES_VERSION=1.1.0 -ARG AIOS_APPS_INVOKE_CLI_VERSION=1.0.2 -ARG AIOS_MANAGEMENT_WEB_NPM_VERSION=1.0.15 +ARG AIOS_APPS_INVOKE_CLI_VERSION=1.0.3 +ARG AIOS_MANAGEMENT_WEB_NPM_VERSION=1.0.16 ARG AIOS_BUILD_NPM_REGISTRY=https://registry.npmjs.org FROM node:24-bookworm-slim AS runtime-base diff --git a/docker/README.md b/docker/README.md index 120a4ae..97a179b 100644 --- a/docker/README.md +++ b/docker/README.md @@ -47,7 +47,8 @@ docker start aios ```json { "AIOS_MANAGEMENT_CONSOLE_PORT": 3030, - "AIOS_OPENCLAW_GATEWAY_MAX_CONCURRENT": 16 + "AIOS_OPENCLAW_GATEWAY_MAX_CONCURRENT": 16, + "AIOS_HUOZIGE_SERVER_IP": "" } ``` @@ -119,7 +120,7 @@ aios-mqtt-channel-chat-{yyyyMMdd}.log 顶层脚本守护 MQTT、MinIO、OpenClaw supervisor 和 apps supervisor。apps supervisor 内部以 `aios-svc` 守护 management web 和 `aios-apps-invoke-cli serve`;OpenClaw supervisor 内部守护 OpenClaw gateway。 -应用调用不需要把 agent 加入局域网白名单。OpenClaw/agent 侧只执行 CLI client,并连接固定内部 socket `/var/aios/run/app-invoke.sock`;真实业务系统访问由 `aios-svc` 运行的 invoke service 完成。 +对话应用/业务应用所在的活字格服务器地址通过管理控制台“系统设置 -> 基础设置”统一配置。修改后管理控制台会同步维护 `AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST`:移除旧地址并加入新地址;白名单变更需重启容器后生效。OpenClaw/agent 侧仍只执行 CLI client,并连接固定内部 socket `/var/aios/run/app-invoke.sock`;真实业务系统访问由 `aios-svc` 运行的 invoke service 完成。 ## 验收与压测 diff --git a/docker/docs/environment-variables.md b/docker/docs/environment-variables.md index 5a881a0..bc8c76e 100644 --- a/docker/docs/environment-variables.md +++ b/docker/docs/environment-variables.md @@ -8,7 +8,8 @@ AIOS Docker 支持通过 `/var/aios/env.json` 配置少量用户参数。MQTT、 | --- | --- | --- | | `AIOS_MANAGEMENT_CONSOLE_PORT` | `3030` | management web 监听端口。建议宿主机映射整个 `3030-3050` 区间。 | | `AIOS_OPENCLAW_GATEWAY_MAX_CONCURRENT` | `16` | OpenClaw gateway 执行 lane 并发数,启动时写入 `openclaw.json` 的 `agents.defaults.maxConcurrent`。 | -| `AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST` | 空字符串 | 允许 `aios-agent` 访问的局域网地址或 CIDR,支持逗号、空格、分号或 JSON 数组字符串。业务系统调用无需配置这里,因为真实调用由 `aios-svc` 完成。 | +| `AIOS_HUOZIGE_SERVER_IP` | 空字符串 | 对话应用/业务应用所在的活字格服务器 IP 地址或主机名。通过管理控制台修改时会同步维护 `AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST`。 | +| `AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST` | 空字符串 | 允许 `aios-agent` 访问的局域网地址或 CIDR,支持逗号、空格、分号或 JSON 数组字符串。通过基础设置修改活字格服务器地址时会自动移除旧地址并加入新地址。 | | `AIOS_MANAGEMENT_WEBSITE_TIMEOUT` | `180` | management web 访问内部管理动作的超时时间,单位秒。 | | `AIOS_SYSTEM_STATUS_INITIAL_DELAY_MS` | `60000` | 管理控制台启动后首次采集系统状态的延迟,单位毫秒;覆盖 MQTT、S3、内核、插件加载和调用服务等状态。 | | `AIOS_SYSTEM_STATUS_REFRESH_INTERVAL_MS` | `60000` | 首次采集完成后系统状态缓存的刷新间隔,单位毫秒。 | @@ -20,6 +21,7 @@ AIOS Docker 支持通过 `/var/aios/env.json` 配置少量用户参数。MQTT、 { "AIOS_MANAGEMENT_CONSOLE_PORT": 3030, "AIOS_OPENCLAW_GATEWAY_MAX_CONCURRENT": 16, + "AIOS_HUOZIGE_SERVER_IP": "", "AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST": "", "AIOS_SYSTEM_STATUS_INITIAL_DELAY_MS": 60000, "AIOS_SYSTEM_STATUS_REFRESH_INTERVAL_MS": 60000, diff --git a/docker/sample/env.json b/docker/sample/env.json index 0911fad..68a3e19 100644 --- a/docker/sample/env.json +++ b/docker/sample/env.json @@ -1,5 +1,6 @@ { "AIOS_MANAGEMENT_CONSOLE_PORT": 3030, "AIOS_OPENCLAW_GATEWAY_MAX_CONCURRENT": 100, + "AIOS_HUOZIGE_SERVER_IP": "", "AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST": "127.0.0.1,::1" } diff --git a/extensions/clawhub-skills/aios-call-app-service/SKILL.md b/extensions/clawhub-skills/aios-call-app-service/SKILL.md index 87afde0..4bc3624 100644 --- a/extensions/clawhub-skills/aios-call-app-service/SKILL.md +++ b/extensions/clawhub-skills/aios-call-app-service/SKILL.md @@ -1,6 +1,6 @@ --- name: aios-call-app-service -description: 当请求依赖 AIOS、OpenClaw、Forguncy 等业务系统的实时数据、接口调用或业务操作时,优先使用本技能。先读取 AIOS_ONTOLOGY_DIR 指向的本体目录,再确认应用、命令、参数结构和枚举映射,通过 aios-apps-invoke-cli 发起调用,并以实时返回结果作为后续分析和执行依据。遇到 aios-mqtt-channel 会话时,把当前会话的 SessionId 视为唯一合法会话标识;它来自当前会话上下文中的 `sessionId`。 +description: 当请求依赖 AIOS、OpenClaw、Forguncy 等业务系统的实时数据、接口调用或业务操作时,优先使用本技能。先读取 AIOS_ONTOLOGY_DIR 指向的本体目录,再确认应用、命令、参数结构和枚举映射,通过 aios-apps-invoke-cli 发起调用,并以实时返回结果作为后续分析和执行依据。遇到 aios-mqtt-channel 会话时,把当前会话的 SessionId 视为唯一合法会话标识;它来自当前会话上下文中的 `topic_id`。 --- # 系统调用技能 @@ -38,11 +38,10 @@ description: 当请求依赖 AIOS、OpenClaw、Forguncy 等业务系统的实时 2. 根据文档,确认本次是 `servercommand` 还是 `binding`。 3. 根据文档,确认 `applicationName`。 4. 根据文档,确认 `commandName`。 -5. 根据文档,确认 `provider`。 -6. 通过会话上下文的 `topic_id` ,确认 `SessionId`。 -7. 生成 `jsonBody`,并检查确认合法性。 -8. 如果涉及到创建、修改、提交、审核、删除等动作,必须整理出包括每个参数、值和说明的表格,请用户确认后,方可执行。查询类动作需忽略这一步。 -9. 生成并执行唯一一条 CLI 命令。 +5. 通过会话上下文的 `topic_id` ,确认 `SessionId`。 +6. 生成 `jsonBody`,保存为 UTF-8 JSON 文件,并检查确认合法性。 +7. 如果涉及到创建、修改、提交、审核、删除等动作,必须整理出包括每个参数、值和说明的表格,请用户确认后,方可执行。查询类动作需忽略这一步。 +8. 生成并执行唯一一条 CLI 命令。 任一步缺失,都停止并说明缺口。 @@ -55,15 +54,15 @@ description: 当请求依赖 AIOS、OpenClaw、Forguncy 等业务系统的实时 ## 约束要求 - `AIOS_ONTOLOGY_DIR` (默认为 `/var/aios/kernel/ontology`)视为当前事实源。 -- 当前 CLI 只支持 `provider=hzg`,如果出现其他 provider,直接说明当前运行链路不支持,不要猜测替代方案。 +- 业务系统调用链路固定使用 HZG;CLI 不接受 `provider`、`-p` 或 `--provider` 参数。 - 调用 CLI 时,`-s` 传入当前会话的 `topic_id` 。 - 不能臆造,不能复用其他会话的 `topic_id` 。 - 不得使用提示词中的 `SessionId` 、 `topic_id` 、`chat_id` 、 `message_id` 或其他字段代替。 -- 不要臆造接口名、请求字段、枚举 ID 或 `provider`。 +- 不要臆造接口名、请求字段或枚举 ID。 - 不要绕过 CLI 自行编写 API 调用脚本。 - 不要手动启动 `aios-apps-invoke-cli serve` ;运行环境应已提供常驻 app invoke service。 - 只有拿到 CLI 结果后,才允许用 Python 做二次分析和计算。 -- 如果本体不完整、`provider` 未知或运行时上下文缺失,应明确说明阻塞点,不要猜测。 +- 如果本体不完整或运行时上下文缺失,应明确说明阻塞点,不要猜测。 - 禁止将调用结果存储到记忆、缓存或数据库中;禁止在后续对话中复用之前的调用结果。 ## 降歧义规则 @@ -74,6 +73,7 @@ description: 当请求依赖 AIOS、OpenClaw、Forguncy 等业务系统的实时 - `CalcBindingDataSource` - `servercommand` 的 `jsonBody` 严格按 ontology 的 `Input Arguments` 生成。 - `binding` 的 `jsonBody` 严格按 [references/invoke-rules.md](references/invoke-rules.md) 中的兼容 schema 生成。 +- 调用 `POST` 或 `binding` 时,优先使用 `--body-file `;调用 `GET servercommand` 时,优先使用 `--query-file `。 - 不要把底层 HTTP 原始 body 直接塞进 `binding` 的 `jsonBody`,除非规则文件明确允许。 - 同一个问题里如果存在多个可疑 ontology 条目,先说明候选项并停止,不要自选一个继续调用。 diff --git a/extensions/clawhub-skills/aios-call-app-service/readme.md b/extensions/clawhub-skills/aios-call-app-service/readme.md index 16bf450..6a8a71c 100644 --- a/extensions/clawhub-skills/aios-call-app-service/readme.md +++ b/extensions/clawhub-skills/aios-call-app-service/readme.md @@ -15,11 +15,12 @@ - 先读取 `AIOS_ONTOLOGY_DIR` - 再确认应用、命令、参数结构和枚举映射 - 统一通过 `aios-apps-invoke-cli` 发起调用;CLI 会把请求交给本地 app invoke socket service -- 当前 CLI 只支持 `provider=hzg` +- 业务系统调用链路固定使用 HZG;CLI 不接受 provider 参数 - 当前会话标识使用 `SessionId` - 调用 `aios-apps-invoke-cli` 时: - - `-p` 对应 `provider` - `-s` 对应 `SessionId` + - `POST` 和 `binding` 优先使用 `--body-file` + - `GET servercommand` 优先使用 `--query-file` - 不要绕过 CLI 直接访问业务系统 HTTP 接口;局域网业务系统访问由 `aios-apps-invoke-cli serve` 进程完成 - `binding` 参数优先从 `binding-*.md` 的 `CandidatesBindings`、`TableBindings`、`DataSourceBindings` 文档生成兼容 schema - `servercommand` 参数严格按 ontology 的 `Input Arguments` 生成 diff --git a/extensions/clawhub-skills/aios-call-app-service/references/invoke-rules.md b/extensions/clawhub-skills/aios-call-app-service/references/invoke-rules.md index 7a3926b..9e87c50 100644 --- a/extensions/clawhub-skills/aios-call-app-service/references/invoke-rules.md +++ b/extensions/clawhub-skills/aios-call-app-service/references/invoke-rules.md @@ -3,8 +3,9 @@ ## 命令 ```bash -aios-apps-invoke-cli servercommand [jsonBody] -p -s -aios-apps-invoke-cli binding -p -s +aios-apps-invoke-cli servercommand POST --body-file -s +aios-apps-invoke-cli servercommand GET --query-file -s +aios-apps-invoke-cli binding --body-file -s aios-apps-invoke-cli service-status aios-apps-invoke-cli status ``` @@ -13,9 +14,8 @@ aios-apps-invoke-cli status ## 必守规则 -- `-p/-s` 分别来自 `provider`、当前 `SessionId` (上下文中的 `topic_id` ) -- 当前 CLI 只支持 `provider=hzg` -- 如果 ontology、用户或上下文指向其他 provider,直接停止并说明当前运行链路不支持 +- `-s` 来自当前 `SessionId` (上下文中的 `topic_id` ) +- 业务系统调用链路固定使用 HZG;CLI 不接受 `provider`、`-p` 或 `--provider` 参数 - `servercommand` 和 `binding` 是 CLI client 命令,会通过本地 app invoke socket service 执行真实业务系统调用 - 不要手动启动 `serve` 或 `serve-mqtt`,也不要绕过 CLI 直接拼业务系统 HTTP 请求 - `service-status` 用于检查 socket service 配置;`status` 会读取完整配置视图,可能要求兼容 MQTT 环境变量存在 @@ -35,6 +35,8 @@ aios-apps-invoke-cli status - `servercommand`:`commandName` 用本体命令名,`method` 用本体 `verb`,`jsonBody` 严格按 `ServerCommand 兼容 schema` 章节处理 - `servercommand` 不新增字段,不遗漏字段,不修改字段类型 - CLI 请求体只发送当前链路实际需要的 OpenClaw 会话字段:`SessionId` +- `POST servercommand` 和 `binding` 优先把 `jsonBody` 写入 UTF-8 JSON 文件,并用 `--body-file ` 传入 +- `GET servercommand` 优先把查询参数 JSON 写入 UTF-8 JSON 文件,并用 `--query-file ` 传入;该 JSON 必须是字符串值对象 - `binding`:优先根据 ontology 中的 `*-bindings.md` 文档生成兼容 schema,不要手写或反推底层 HTTP 原始 body - `DataSourceBindings` 一律用 `binding` 命令,`commandName` 固定为 `CalcBindingDataSource` - 如果 `binding` 和 `servercommand` 都能勉强解释当前需求,优先按 ontology 明确标注的端点类型执行;ontology 没标明时停止,不要猜 @@ -242,7 +244,7 @@ aios-apps-invoke-cli status - 如果 bindings 文档已经给出真实 `column-name`,必须保留真实列名,不要用 `guid` 顶替 - 只有在文档没有列名、但调试链路必须先跑通时,才允许临时用 `guid` 兼作 `column-name`,并且必须明确说明这是降级方案 -- 生成 CLI 命令前,先在答案里或推理里完成这 4 个定值:`applicationName`、`commandName`、`provider`、`SessionId` +- 生成 CLI 命令前,先在答案里或推理里完成这 4 个定值:`applicationName`、`commandName`、`SessionId`、`jsonFile` - 任一值不能唯一确定时,不要继续拼命令 - 需要底层 ID 时,先用本体定义的查询或绑定把展示文本解析成真实 ID,再发正式请求 - JSON 字符串统一双引号;shell 转义按当前 shell 处理 diff --git a/packages/aios-apps-invoke-cli/README.md b/packages/aios-apps-invoke-cli/README.md index df3c4d8..0a138db 100644 --- a/packages/aios-apps-invoke-cli/README.md +++ b/packages/aios-apps-invoke-cli/README.md @@ -8,7 +8,7 @@ 正常调用链路是: ```text -OpenClaw/agent -> aios-apps-invoke-cli client -> app-invoke socket -> aios-apps-invoke-cli serve -> management web / provider SDK -> 业务系统 +OpenClaw/agent -> aios-apps-invoke-cli client -> app-invoke socket -> aios-apps-invoke-cli serve -> management web / HZG SDK -> 业务系统 ``` CLI client 自身不直接访问业务系统,也不通过 MQTT 发起 app invoke 请求。AIOS Docker 容器中,OpenClaw 仍以默认 agent 身份调用 CLI;`serve` 由 apps supervisor 以 `aios-svc` 身份常驻运行,因此实际访问局域网业务系统的是 service 进程。 @@ -46,12 +46,16 @@ Client 命令不读取通讯配置环境变量。它固定连接内部 socket: Socket service 使用: - `AIOS_WEB_BASE_URL`:可选,默认 `http://127.0.0.1:3030`,用于读取业务系统配置、解析外部 cookie、记录调用日志。 +- `AIOS_APP_INVOKE_MAX_PAYLOAD_BYTES`:可选,默认 `33554432`,限制单次 socket 请求 JSON 的最大字节数。 ## 用法 ```bash -aios-apps-invoke-cli servercommand <应用名> <命令名> <请求方法> [jsonBody] -p -s -aios-apps-invoke-cli binding <应用名> <命令名> <请求方法> -p -s +aios-apps-invoke-cli servercommand <应用名> <命令名> POST [jsonBody] -s +aios-apps-invoke-cli servercommand <应用名> <命令名> POST --body-file -s +aios-apps-invoke-cli servercommand <应用名> <命令名> GET --query-file -s +aios-apps-invoke-cli binding <应用名> <命令名> <请求方法> [jsonBody] -s +aios-apps-invoke-cli binding <应用名> <命令名> <请求方法> --body-file -s aios-apps-invoke-cli serve aios-apps-invoke-cli service-status aios-apps-invoke-cli status @@ -59,20 +63,22 @@ aios-apps-invoke-cli status 参数说明: -- `provider` 必填,当前可执行 provider 为 `hzg`。 - `-s` 传入当前 OpenClaw 会话的 `SessionId`。 -- 长参数使用 `--provider`、`--sessionId`。 +- 长参数使用 `--sessionId`。 - `servercommand` 的请求方法只支持 `GET` 和 `POST`。 -- `jsonBody` 必须是合法 JSON。 +- `jsonBody` 必须是合法 JSON;大请求体建议使用 `--body-file` 或 `--query-file`,避免触发操作系统命令行参数长度限制。 +- `POST` 使用 `jsonBody`、`--body-file` 或 `--body-stdin` 作为请求体来源。 +- `GET` 使用 `jsonBody`、`--query-file` 或 `--query-stdin` 作为查询参数来源,JSON 必须是字符串值对象。 +- 业务系统类型固定为 HZG,不再通过命令行传入 provider。 ## 示例 ```powershell -aios-apps-invoke-cli servercommand demo getUser POST "{\"id\":\"1001\"}" -p hzg -s thread-1 +aios-apps-invoke-cli servercommand demo getUser POST --body-file .\payload.json -s thread-1 ``` ```powershell -aios-apps-invoke-cli binding OASystem GetTableDataWithOffset POST "{\"columns\":[],\"table-name\":\"物品表\",\"page-name\":\"物品_库存\"}" -p hzg -s thread-2 +aios-apps-invoke-cli binding OASystem GetTableDataWithOffset POST --body-file .\binding-body.json -s thread-2 ``` ## Binding 支持范围 @@ -93,7 +99,6 @@ Client 会向内部固定 socket `/var/aios/run/app-invoke.sock` 写入一行 JS - `traceId` - `command` -- `provider` - `sessionId` - `parameters` - `timestamp` diff --git a/packages/aios-apps-invoke-cli/docs/design.md b/packages/aios-apps-invoke-cli/docs/design.md index 3cd7dc6..2f47ea6 100644 --- a/packages/aios-apps-invoke-cli/docs/design.md +++ b/packages/aios-apps-invoke-cli/docs/design.md @@ -5,7 +5,7 @@ `aios-apps-invoke-cli` 提供 AIOS 应用调用能力。当前设计把调用入口拆成同一 npm 包内的两个运行角色: - CLI client:短进程,保持调用方默认身份,只负责参数解析和本地 IPC。 -- Invoke service:常驻进程,负责真正访问 management web、provider SDK 和业务系统。 +- Invoke service:常驻进程,负责真正访问 management web、HZG SDK 和业务系统。 这样 OpenClaw/agent 可以继续用默认身份调用 CLI,同时把局域网业务系统访问集中到 `aios-svc` 身份的 service 进程。 @@ -18,7 +18,7 @@ aios-agent(OpenClaw) --exec--> aios-apps-invoke-cli binding/servercommand -> /var/aios/run/app-invoke.sock -> aios-apps-invoke-cli serve(aios-svc) -> management web - -> HZG provider SDK + -> HZG SDK -> 业务系统 ``` @@ -36,6 +36,7 @@ Client 主链路不读取通讯配置环境变量,固定连接内部 socket `/ Socket service 主链路读取: - `AIOS_WEB_BASE_URL`:可选,默认 `http://127.0.0.1:3030`。 +- `AIOS_APP_INVOKE_MAX_PAYLOAD_BYTES`:可选,默认 `33554432`,限制单次 socket 请求 JSON 的最大字节数。 ## 命令设计 @@ -50,8 +51,11 @@ CLI 暴露以下命令: 调用格式: ```bash -aios-apps-invoke-cli servercommand <应用名> <命令名> <请求方法> [jsonBody] -p -s -aios-apps-invoke-cli binding <应用名> <命令名> <请求方法> -p -s +aios-apps-invoke-cli servercommand <应用名> <命令名> POST [jsonBody] -s +aios-apps-invoke-cli servercommand <应用名> <命令名> POST --body-file -s +aios-apps-invoke-cli servercommand <应用名> <命令名> GET --query-file -s +aios-apps-invoke-cli binding <应用名> <命令名> <请求方法> [jsonBody] -s +aios-apps-invoke-cli binding <应用名> <命令名> <请求方法> --body-file -s aios-apps-invoke-cli serve aios-apps-invoke-cli service-status aios-apps-invoke-cli status @@ -60,10 +64,13 @@ aios-apps-invoke-cli status 约束: - 保留位置参数顺序。 -- `provider` 必须显式传入;当前实际可执行 provider 为 `hzg`。 - `-s` 语义固定为当前 OpenClaw `SessionId`。 -- 长参数分别使用 `--provider`、`--sessionId`。 +- 长参数使用 `--sessionId`。 - `servercommand` 方法只允许 `GET` 或 `POST`。 +- `POST` 请求体支持位置参数 JSON、`--body-file` 和 `--body-stdin` 三选一。 +- `GET` 查询参数支持位置参数 JSON、`--query-file` 和 `--query-stdin` 三选一,解析结果必须是字符串值对象。 +- `binding` 请求体支持位置参数 JSON、`--body-file` 和 `--body-stdin` 三选一。 +- 业务系统类型固定为 HZG,不作为 CLI、socket 协议或 management web 内部调用 API 的外部参数。 ## 模块架构 @@ -75,36 +82,36 @@ aios-apps-invoke-cli status 4. Socket service:`src/proxy/socket-service.ts` 5. 请求归一化与响应包装:`src/proxy/handler.ts` 6. Management web API client:`src/proxy/web-api-client.ts` -7. HZG provider client:`src/proxy/hzg-provider-client.ts` +7. HZG client:`src/proxy/hzg-provider-client.ts` -Client 和 service 在同一个 npm 包内发布,共用协议类型、provider 处理和测试夹具。 +Client 和 service 在同一个 npm 包内发布,共用协议类型、HZG 调用处理和测试夹具。 ## Socket 请求流程 ### servercommand -1. CLI 解析 `[应用名] [命令名] [请求方法] [jsonBody]`。 -2. 校验 `-p/--provider` 和 `-s/--sessionId`。 +1. CLI 解析 `[应用名] [命令名] [请求方法] [jsonBody]` 和 body/query 文件参数。 +2. 校验 `-s/--sessionId`。 3. 规范化 HTTP 方法为 `GET` 或 `POST`。 -4. `POST` 请求把 JSON 映射到 `parameters.body`。 -5. `GET` 请求把 JSON 映射到字符串字典 `parameters.query`。 +4. `POST` 请求把 body JSON 映射到 `parameters.body`。 +5. `GET` 请求把 query JSON 映射到字符串字典 `parameters.query`。 6. CLI 通过 socket 发送一行 JSON 请求。 7. Service 解析请求,查询 management web 中的业务系统配置。 8. Service 使用当前 `SessionId` 从 management web 解析外部 cookie。 -9. Service 调用 HZG provider SDK。 +9. Service 调用 HZG SDK。 10. Service 记录调用日志并返回一行 JSON 响应。 11. CLI 输出响应结果。 ### binding -当前实际可执行 binding 仅限 `provider=hzg`。 +当前实际可执行 binding 固定走 HZG。 -1. CLI 解析 `[应用名] [命令名] [请求方法] [jsonBody]`。 -2. 校验 `-p/--provider` 和 `-s/--sessionId`。 +1. CLI 解析 `[应用名] [命令名] [请求方法] [jsonBody]` 和 body 文件参数。 +2. 校验 `-s/--sessionId`。 3. 把兼容命令别名规范化为协议命令名。 4. CLI 通过 socket 发送一行 JSON 请求。 5. Service 再次校验并规范化 binding 命令。 -6. Service 调用对应 HZG provider SDK 方法。 +6. Service 调用对应 HZG SDK 方法。 7. Service 记录调用日志并返回一行 JSON 响应。 8. CLI 输出响应结果。 @@ -124,7 +131,6 @@ Client 发往 socket 的请求体包含: - `traceId` - `command` -- `provider` - `sessionId` - `parameters` - `timestamp` diff --git a/packages/aios-apps-invoke-cli/package.json b/packages/aios-apps-invoke-cli/package.json index 385e453..3cf8201 100644 --- a/packages/aios-apps-invoke-cli/package.json +++ b/packages/aios-apps-invoke-cli/package.json @@ -1,6 +1,6 @@ { "name": "aios-apps-invoke-cli", - "version": "1.0.2", + "version": "1.0.3", "description": "AIOS app invoke CLI client and socket service.", "license": "MIT", "type": "commonjs", diff --git a/packages/aios-apps-invoke-cli/src/client/socket-client.ts b/packages/aios-apps-invoke-cli/src/client/socket-client.ts index 2dce285..67e022b 100644 --- a/packages/aios-apps-invoke-cli/src/client/socket-client.ts +++ b/packages/aios-apps-invoke-cli/src/client/socket-client.ts @@ -1,9 +1,11 @@ import { randomUUID } from "node:crypto"; import { createConnection, type Socket } from "node:net"; -import { buildRequestEnvelope, parseResponsePayload, type CommandParameters, type Request, type RequestProvider, type Response } from "../protocol/messages"; +import { buildRequestEnvelope, parseResponsePayload, type CommandParameters, type Request, type Response } from "../protocol/messages"; import { normalizeIpcPath } from "../proxy/ipc-path.js"; +const DEFAULT_MAX_SOCKET_PAYLOAD_BYTES = 32 * 1024 * 1024; + export class AppInvokeSocketClient { private readonly socketPath: string; @@ -13,7 +15,6 @@ export class AppInvokeSocketClient { public async sendRequest( command: string, - provider: RequestProvider, sessionId: string, parameters: CommandParameters, timeoutMs: number @@ -21,7 +22,6 @@ export class AppInvokeSocketClient { const request = buildRequestEnvelope({ traceId: randomUUID(), command, - provider, parameters, sessionId, timestamp: Math.floor(Date.now() / 1_000) @@ -37,6 +37,11 @@ export class AppInvokeSocketClient { } catch (error) { throw new Error(`failed to marshal request: ${formatErrorMessage(error)}`); } + const maxPayloadBytes = readMaxSocketPayloadBytes(); + const payloadBytes = Buffer.byteLength(requestPayload, "utf8"); + if (payloadBytes > maxPayloadBytes) { + throw new Error(`app invoke request payload is too large: ${payloadBytes} bytes > ${maxPayloadBytes} bytes`); + } const socket = createConnection(this.socketPath); socket.setEncoding("utf8"); @@ -65,7 +70,11 @@ export class AppInvokeSocketClient { }, timeoutMs); const handleConnect = (): void => { - socket.write(`${requestPayload}\n`); + socket.write(`${requestPayload}\n`, (error?: Error | null) => { + if (error) { + finish(() => reject(new Error(`failed to write app invoke request: ${error.message}`))); + } + }); }; const handleData = (chunk: string): void => { @@ -101,6 +110,15 @@ export class AppInvokeSocketClient { } } +function readMaxSocketPayloadBytes(): number { + const parsed = Number(process.env.AIOS_APP_INVOKE_MAX_PAYLOAD_BYTES || ""); + if (Number.isFinite(parsed) && parsed > 0) { + return Math.floor(parsed); + } + + return DEFAULT_MAX_SOCKET_PAYLOAD_BYTES; +} + function formatErrorMessage(error: unknown): string { return error instanceof Error ? error.message : String(error); } diff --git a/packages/aios-apps-invoke-cli/src/commands/root.ts b/packages/aios-apps-invoke-cli/src/commands/root.ts index 6d00b16..d992614 100644 --- a/packages/aios-apps-invoke-cli/src/commands/root.ts +++ b/packages/aios-apps-invoke-cli/src/commands/root.ts @@ -1,9 +1,11 @@ +import { readFile } from "node:fs/promises"; + import { AppInvokeSocketClient } from "../client/socket-client"; import { REQUEST_TIMEOUT_SECONDS, loadClientConfig, loadConfig, type ClientConfig, type Config } from "../config/config"; import { writeErrorLog } from "../logger"; import { createLogger, errorDetails } from "../proxy/logger"; import { createAppInvokeSocketService } from "../proxy/socket-service"; -import type { CommandParameters, RequestProvider, Response } from "../protocol/messages"; +import type { CommandParameters, Response } from "../protocol/messages"; export const allowedBindingEndpoints = [ "TableBinding", @@ -20,6 +22,22 @@ export interface CliIO { stderr(message: string): void; } +interface UserFlags { + sessionId: string; + bodyFile: string; + bodyStdin: boolean; + queryFile: string; + queryStdin: boolean; +} + +interface PayloadSourceOptions { + inline?: string; + file?: string; + stdin?: boolean; + required: boolean; + targetName: string; +} + const defaultIO: CliIO = { stdout: (message) => process.stdout.write(message), stderr: (message) => process.stderr.write(message) @@ -101,8 +119,8 @@ export async function runCli(argv: string[], io: CliIO = defaultIO): Promise { if (args.length < 3) { @@ -112,11 +130,16 @@ export async function executeServerCommand( const applicationName = args[0]; const commandName = args[1]; const httpType = args[2].toUpperCase(); - const paramString = args[3] ?? ""; if (httpType !== "GET" && httpType !== "POST") { throw new Error(`httpType must be GET or POST, got: ${httpType}`); } + if (httpType === "GET" && (flags.bodyFile || flags.bodyStdin)) { + throw new Error("GET servercommand accepts query sources only"); + } + if (httpType === "POST" && (flags.queryFile || flags.queryStdin)) { + throw new Error("POST servercommand accepts body sources only"); + } const parameters: CommandParameters = { applicationName, @@ -124,6 +147,14 @@ export async function executeServerCommand( method: httpType }; + const paramString = await resolvePayloadSource({ + inline: args[3], + file: httpType === "POST" ? flags.bodyFile : flags.queryFile, + stdin: httpType === "POST" ? flags.bodyStdin : flags.queryStdin, + required: false, + targetName: httpType === "POST" ? "body" : "query" + }); + if (paramString) { if (httpType === "POST") { parameters.body = parseJSONBody(paramString, "body"); @@ -138,7 +169,6 @@ export async function executeServerCommand( await sendCommand( config, "servercommand", - provider, sessionId, parameters, io @@ -148,17 +178,26 @@ export async function executeServerCommand( export async function executeBindingCommand( config: ClientConfig, args: string[], - provider: RequestProvider, sessionId: string, + flags: UserFlags = emptyUserFlags(), io: CliIO = defaultIO ): Promise { const { applicationName, commandName, method, jsonBody } = parseBindingArgs(args); - const body = parseJSONBody(jsonBody, "body"); + if (flags.queryFile || flags.queryStdin) { + throw new Error("binding accepts body sources only"); + } + const bodyPayload = await resolvePayloadSource({ + inline: jsonBody, + file: flags.bodyFile, + stdin: flags.bodyStdin, + required: true, + targetName: "body" + }); + const body = parseJSONBody(bodyPayload, "body"); await sendCommand( config, "binding", - provider, sessionId, { applicationName, @@ -174,7 +213,7 @@ export function parseBindingArgs(args: string[]): { applicationName: string; commandName: string; method: string; - jsonBody: string; + jsonBody?: string; } { if (!args[0]?.trim()) { throw new Error("applicationName is required"); @@ -188,10 +227,6 @@ export function parseBindingArgs(args: string[]): { throw new Error("method is required"); } - if (args.length < 4) { - throw new Error("body is required"); - } - const applicationName = args[0].trim(); const rawCommandName = args[1].trim(); const method = args[2].trim().toUpperCase(); @@ -272,7 +307,6 @@ export function parseJSONStringMap(raw: string, targetName: string): Record { + const sources = [ + options.inline !== undefined && options.inline !== "", + Boolean(options.file), + Boolean(options.stdin) + ].filter(Boolean).length; + + if (sources > 1) { + throw new Error(`${options.targetName} accepts only one source: positional JSON, file, or stdin`); + } + + if (options.file) { + return await readFile(options.file, "utf8"); + } + + if (options.stdin) { + return await readStdin(); + } + + if (options.inline !== undefined && options.inline !== "") { + return options.inline; + } + + if (options.required) { + throw new Error(`${options.targetName} is required`); + } + + return ""; +} + +async function readStdin(): Promise { + const chunks: Buffer[] = []; + for await (const chunk of process.stdin) { + chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)); + } + return Buffer.concat(chunks).toString("utf8"); +} + function requireFlagValue(flagName: string, value: string | undefined): string { if (!value) { throw new Error(`flag needs an argument: ${flagName}`); @@ -523,11 +585,15 @@ function renderServerCommandHelp(): string { "Execute a business server command endpoint.", "", "Usage:", - " aios-apps-invoke-cli servercommand [applicationName] [commandName] [method] [jsonBody] -p [provider] -s [SessionId]", + " aios-apps-invoke-cli servercommand [applicationName] [commandName] [method] --body-file [file] -s [SessionId]", + " aios-apps-invoke-cli servercommand [applicationName] [commandName] GET --query-file [file] -s [SessionId]", "", "Options:", - " -p, --provider string Request provider, currently hzg", - " -s, --sessionId string Current OpenClaw session id", + " -s, --sessionId string Current OpenClaw session id", + " --body-file string Read POST JSON body from a file", + " --body-stdin Read POST JSON body from stdin", + " --query-file string Read GET JSON query object from a file", + " --query-stdin Read GET JSON query object from stdin", "" ].join("\n"); } @@ -537,11 +603,12 @@ function renderBindingCommandHelp(): string { "Execute a system binding endpoint.", "", "Usage:", - " aios-apps-invoke-cli binding [applicationName] [commandName] [method] [jsonBody] -p [provider] -s [SessionId]", + " aios-apps-invoke-cli binding [applicationName] [commandName] [method] --body-file [file] -s [SessionId]", "", "Options:", - " -p, --provider string Request provider, currently hzg", - " -s, --sessionId string Current OpenClaw session id", + " -s, --sessionId string Current OpenClaw session id", + " --body-file string Read JSON body from a file", + " --body-stdin Read JSON body from stdin", "" ].join("\n"); } diff --git a/packages/aios-apps-invoke-cli/src/index.ts b/packages/aios-apps-invoke-cli/src/index.ts index 4cea034..523cf76 100644 --- a/packages/aios-apps-invoke-cli/src/index.ts +++ b/packages/aios-apps-invoke-cli/src/index.ts @@ -12,7 +12,6 @@ export type { BusinessSystemRecord, CommandParameters as AppInvokeCommandParameters, EnvironmentConfig as AppInvokeEnvironmentConfig, - RequestProvider as AppInvokeRequestProvider, ServiceInfo as AppInvokeServiceInfo, ServiceStatus as AppInvokeServiceStatus } from "./proxy/types"; diff --git a/packages/aios-apps-invoke-cli/src/protocol/messages.ts b/packages/aios-apps-invoke-cli/src/protocol/messages.ts index 82ffa17..076c248 100644 --- a/packages/aios-apps-invoke-cli/src/protocol/messages.ts +++ b/packages/aios-apps-invoke-cli/src/protocol/messages.ts @@ -1,10 +1,8 @@ export type CommandParameters = Record; -export type RequestProvider = "hzg"; export interface Request { traceId: string; command: string; - provider: RequestProvider; sessionId?: string; parameters?: CommandParameters; timestamp: number; @@ -42,7 +40,6 @@ export function newErrorResponse(code: number, message: string): Response { export function buildRequestEnvelope(params: { traceId: string; command: string; - provider: RequestProvider; sessionId: string; parameters: CommandParameters; timestamp: number; @@ -50,7 +47,6 @@ export function buildRequestEnvelope(params: { const { traceId, command, - provider, sessionId, parameters, timestamp @@ -59,7 +55,6 @@ export function buildRequestEnvelope(params: { return { traceId, command, - provider, sessionId, parameters, timestamp diff --git a/packages/aios-apps-invoke-cli/src/proxy/handler.ts b/packages/aios-apps-invoke-cli/src/proxy/handler.ts index 450de58..25dfbb3 100644 --- a/packages/aios-apps-invoke-cli/src/proxy/handler.ts +++ b/packages/aios-apps-invoke-cli/src/proxy/handler.ts @@ -93,17 +93,12 @@ export function normalizeRequestPayload(payload: unknown): AppInvokeRequest { if (!request.traceId || typeof request.traceId !== "string") { throw badRequest("traceId is required"); } - if (request.provider !== "hzg" && request.provider !== "phx") { - throw badRequest("provider must be one of hzg,phx"); - } - const command = assertCommand(request.command); const parameters = normalizeParameters(command, (request.parameters ?? {}) as CommandParameters); return { traceId: request.traceId, command, - provider: request.provider, CurrentUser: request.CurrentUser, sessionId: request.sessionId, AgentName: request.AgentName, @@ -137,7 +132,6 @@ export function createAppInvokeHandler( system_id: string; agent_slug?: string | null; session_id?: string | null; - provider: string; application_name: string; command_name: string; request_payload?: unknown; @@ -148,9 +142,9 @@ export function createAppInvokeHandler( }): Promise { await webApiClient.recordInvocation({ sessionId: log.session_id ?? "", - provider: log.provider === "hzg" || log.provider === "phx" ? log.provider : "hzg", applicationName: log.application_name, commandName: log.command_name, + method: String((log.request_payload as { parameters?: { method?: unknown } } | undefined)?.parameters?.method ?? ""), paramaters: JSON.stringify(log.request_payload ?? {}), isOK: log.success, errorMessage: log.error_message ?? undefined, @@ -164,7 +158,6 @@ export function createAppInvokeHandler( system_id: string; agent_slug?: string | null; session_id?: string | null; - provider: string; application_name: string; command_name: string; request_payload?: unknown; @@ -183,7 +176,6 @@ export function createAppInvokeHandler( async function handleRequest(request: AppInvokeRequest): Promise { const startedAt = Date.now(); const params = request.parameters ?? {}; - const provider = request.provider; const applicationName = String(params.applicationName ?? ""); const commandName = String(params.commandName ?? ""); let response: AppInvokeResponse; @@ -194,13 +186,9 @@ export function createAppInvokeHandler( throw badRequest("applicationName is required"); } - system = await webApiClient.findSystemForInvocation(provider, applicationName); + system = await webApiClient.findSystemForInvocation(applicationName); if (!system) { - throw notFound(`Business system not found: ${provider}/${applicationName}`); - } - - if (provider === "phx") { - throw badRequest("Unsupported provider: phx"); + throw notFound(`Business system not found: ${applicationName}`); } const providerResult = await hzgProviderClient.execute(system, request); @@ -210,7 +198,6 @@ export function createAppInvokeHandler( system_id: system.system_id, agent_slug: request.AgentName ?? null, session_id: request.sessionId ?? null, - provider, application_name: applicationName, command_name: commandName, request_payload: request, @@ -230,7 +217,6 @@ export function createAppInvokeHandler( system_id: system?.system_id ?? (applicationName || "unknown"), agent_slug: request.AgentName ?? null, session_id: request.sessionId ?? null, - provider, application_name: applicationName || "unknown", command_name: commandName || "unknown", request_payload: request, diff --git a/packages/aios-apps-invoke-cli/src/proxy/hzg-provider-client.ts b/packages/aios-apps-invoke-cli/src/proxy/hzg-provider-client.ts index 2aad60d..cd62d0c 100644 --- a/packages/aios-apps-invoke-cli/src/proxy/hzg-provider-client.ts +++ b/packages/aios-apps-invoke-cli/src/proxy/hzg-provider-client.ts @@ -159,13 +159,13 @@ export class HzgProviderClient { this.webApiClient = dependencies.webApiClient; } - public async createSessionCookie(provider: "hzg" | "phx", sessionId: string): Promise { + public async createSessionCookie(sessionId: string, applicationName = ""): Promise { if (!this.webApiClient) { throw serviceUnavailable("Web API client is not configured for cookie resolution"); } - const response = await this.webApiClient.getExternalCookie(sessionId, provider); - if (!response.cookie || response.provider !== provider) { + const response = await this.webApiClient.getExternalCookie(sessionId, applicationName); + if (!response.cookie) { throw serviceUnavailable("Failed to resolve provider cookie from web API"); } @@ -184,11 +184,7 @@ export class HzgProviderClient { throw badRequest("sessionId is required for Huozige invocation"); } - if (request.provider !== "hzg") { - throw badRequest(`Unsupported HZG provider request: ${request.provider}`); - } - - const cookie = await this.createSessionCookie(request.provider, sessionId); + const cookie = await this.createSessionCookie(sessionId, String(request.parameters?.applicationName ?? "")); const params = request.parameters ?? {}; const command = request.command; const method = String(params.method ?? "POST").toUpperCase(); diff --git a/packages/aios-apps-invoke-cli/src/proxy/types.ts b/packages/aios-apps-invoke-cli/src/proxy/types.ts index 07a6749..35da139 100644 --- a/packages/aios-apps-invoke-cli/src/proxy/types.ts +++ b/packages/aios-apps-invoke-cli/src/proxy/types.ts @@ -1,12 +1,9 @@ -export type RequestProvider = "phx" | "hzg"; - export type CommandParameters = Record; export type AppInvokeCommand = "servercommand" | "binding"; export interface AppInvokeRequest { traceId: string; command: AppInvokeCommand; - provider: RequestProvider; CurrentUser?: string; sessionId?: string; AgentName?: string; @@ -24,7 +21,7 @@ export interface AppInvokeResponse { export interface BusinessSystemRecord { id?: number; system_id: string; - provider: RequestProvider; + provider: "hzg"; name?: string; application_name: string; description?: string; @@ -44,7 +41,6 @@ export interface InvocationLogInput { system_id: string; agent_slug?: string | null; session_id?: string | null; - provider: string; application_name: string; command_name: string; request_payload?: unknown; @@ -56,15 +52,14 @@ export interface InvocationLogInput { } export interface ExternalCookieResponse { - provider: RequestProvider; cookie: string; } export interface ExternalInvocationLogInput { sessionId: string; - provider: RequestProvider; applicationName: string; commandName: string; + method?: string; paramaters: string; isOK: boolean; errorMessage?: string; diff --git a/packages/aios-apps-invoke-cli/src/proxy/web-api-client.ts b/packages/aios-apps-invoke-cli/src/proxy/web-api-client.ts index 6f54f33..9172af1 100644 --- a/packages/aios-apps-invoke-cli/src/proxy/web-api-client.ts +++ b/packages/aios-apps-invoke-cli/src/proxy/web-api-client.ts @@ -25,8 +25,8 @@ export class WebApiClient { this.baseUrl = options.baseUrl.replace(/\/+$/, ""); } - public async findSystemForInvocation(provider: string, applicationName: string): Promise { - const url = `${this.baseUrl}/api/internal/app-invoke/system?provider=${encodeURIComponent(provider)}&applicationName=${encodeURIComponent(applicationName)}`; + public async findSystemForInvocation(applicationName: string): Promise { + const url = `${this.baseUrl}/api/internal/app-invoke/system?applicationName=${encodeURIComponent(applicationName)}`; const response = await fetch(url, { method: "GET", headers: this.buildHeaders() @@ -43,8 +43,12 @@ export class WebApiClient { return await response.json() as BusinessSystemRecord; } - public async getExternalCookie(sessionId: string, provider: string): Promise { - const url = `${this.baseUrl}/api/external/cookie?sessionId=${encodeURIComponent(sessionId)}&provider=${encodeURIComponent(provider)}`; + public async getExternalCookie(sessionId: string, applicationName = ""): Promise { + const params = new URLSearchParams({ sessionId }); + if (applicationName) { + params.set("applicationName", applicationName); + } + const url = `${this.baseUrl}/api/external/cookie?${params.toString()}`; const response = await fetch(url, { method: "GET", headers: this.buildHeaders() diff --git a/packages/aios-apps-invoke-cli/test/proxy/socket-service.test.ts b/packages/aios-apps-invoke-cli/test/proxy/socket-service.test.ts index 16f91d2..d0bc965 100644 --- a/packages/aios-apps-invoke-cli/test/proxy/socket-service.test.ts +++ b/packages/aios-apps-invoke-cli/test/proxy/socket-service.test.ts @@ -26,8 +26,7 @@ test("socket service handles app invoke requests without transport env vars", as const logs: Array> = []; const service = createAppInvokeSocketService(createTestEnv(socketPath), { webApiClient: { - async findSystemForInvocation(provider: string, applicationName: string) { - assert.equal(provider, "hzg"); + async findSystemForInvocation(applicationName: string) { assert.equal(applicationName, "demo-app"); return { system_id: "system-1", @@ -61,7 +60,6 @@ test("socket service handles app invoke requests without transport env vars", as const client = new AppInvokeSocketClient(socketPath); const response = await client.sendRequest( "binding", - "hzg", "thread-1", { applicationName: "demo-app", diff --git a/packages/aios-apps-invoke-cli/test/proxy/web-api-client.test.ts b/packages/aios-apps-invoke-cli/test/proxy/web-api-client.test.ts index 984bf47..b381751 100644 --- a/packages/aios-apps-invoke-cli/test/proxy/web-api-client.test.ts +++ b/packages/aios-apps-invoke-cli/test/proxy/web-api-client.test.ts @@ -32,9 +32,9 @@ test("WebApiClient resolves system without bearer token", async () => { baseUrl: "http://127.0.0.1:3030" }); - const system = await client.findSystemForInvocation("hzg", "demo"); + const system = await client.findSystemForInvocation("demo"); assert.equal(system?.system_id, "system-1"); - assert.equal(calls[0]?.url, "http://127.0.0.1:3030/api/internal/app-invoke/system?provider=hzg&applicationName=demo"); + assert.equal(calls[0]?.url, "http://127.0.0.1:3030/api/internal/app-invoke/system?applicationName=demo"); assert.equal((calls[0]?.headers as Record).Authorization, undefined); } finally { globalThis.fetch = originalFetch; @@ -52,7 +52,6 @@ test("WebApiClient resolves external cookie", async () => { }); return new Response(JSON.stringify({ - provider: "hzg", cookie: "ForguncyServer=test-cookie" }), { status: 200, @@ -67,10 +66,9 @@ test("WebApiClient resolves external cookie", async () => { baseUrl: "http://127.0.0.1:3030" }); - const response = await client.getExternalCookie("session-1", "hzg"); - assert.equal(response.provider, "hzg"); + const response = await client.getExternalCookie("session-1", "demo"); assert.equal(response.cookie, "ForguncyServer=test-cookie"); - assert.equal(calls[0]?.url, "http://127.0.0.1:3030/api/external/cookie?sessionId=session-1&provider=hzg"); + assert.equal(calls[0]?.url, "http://127.0.0.1:3030/api/external/cookie?sessionId=session-1&applicationName=demo"); } finally { globalThis.fetch = originalFetch; } @@ -96,7 +94,6 @@ test("WebApiClient records invocation log with external logging contract", async await client.recordInvocation({ sessionId: "session-1", - provider: "hzg", applicationName: "demo", commandName: "cmd", paramaters: "{\"id\":\"1001\"}", @@ -141,12 +138,11 @@ test("WebApiClient preserves full error response details when invocation logging await assert.rejects( () => client.recordInvocation({ sessionId: "s-missing", - provider: "hzg", applicationName: "demo", commandName: "cmd", paramaters: "{\"id\":\"1001\"}", isOK: false, - errorMessage: "provider failed", + errorMessage: "invoke failed", response: "{\"ok\":false}", durationInMS: 12 }), diff --git a/packages/aios-apps-invoke-cli/test/root.test.ts b/packages/aios-apps-invoke-cli/test/root.test.ts index f25b6c2..2912af9 100644 --- a/packages/aios-apps-invoke-cli/test/root.test.ts +++ b/packages/aios-apps-invoke-cli/test/root.test.ts @@ -1,6 +1,5 @@ import { executeStatus, - normalizeRequestProvider, parseJSONBody, parseJSONStringMap, parseBindingArgs, @@ -56,8 +55,15 @@ describe("parseBindingArgs", () => { expect(() => parseBindingArgs(["OASystem", "CalcBindingDataSource"])).toThrowError(/method is required/); }); - it("rejects missing body", () => { - expect(() => parseBindingArgs(["OASystem", "CalcBindingDataSource", "POST"])).toThrowError(/body is required/); + it("allows body to come from a file or stdin", () => { + const result = parseBindingArgs(["OASystem", "CalcBindingDataSource", "POST"]); + + expect(result).toEqual({ + applicationName: "OASystem", + commandName: "CalcBindingDataSource", + method: "POST", + jsonBody: undefined + }); }); it("rejects unsupported binding command", () => { @@ -92,33 +98,21 @@ describe("parseJSONStringMap", () => { }); }); -describe("normalizeRequestProvider", () => { - it("accepts hzg with mixed casing and trims whitespace", () => { - expect(normalizeRequestProvider(" HZG ")).toBe("hzg"); - }); - - it("rejects unsupported values", () => { - expect(() => normalizeRequestProvider("abc")).toThrowError( - /provider must be "hzg", got: abc/ - ); - }); -}); - describe("status command", () => { - it("fails when invoke commands omit required provider", async () => { + it("fails when binding commands omit body source", async () => { const stderr: string[] = []; - const exitCode = await runCli(["servercommand", "app", "cmd", "GET", "-s", "s"], { + const exitCode = await runCli(["binding", "app", "GetTableDataWithOffset", "POST", "-s", "s"], { stdout: () => undefined, stderr: (message) => stderr.push(message) }); expect(exitCode).toBe(1); - expect(parseCliErrorMessage(stderr)).toBe('required flag(s) "provider" not set'); + expect(parseCliErrorMessage(stderr)).toBe("body is required"); }); it("fails when invoke commands omit required SessionId", async () => { const stderr: string[] = []; - const exitCode = await runCli(["servercommand", "app", "cmd", "GET", "-p", "hzg"], { + const exitCode = await runCli(["servercommand", "app", "cmd", "GET"], { stdout: () => undefined, stderr: (message) => stderr.push(message) }); @@ -127,10 +121,10 @@ describe("status command", () => { expect(parseCliErrorMessage(stderr)).toBe('required flag(s) "sessionId" not set'); }); - it("fails when invoke commands use an unsupported provider", async () => { + it("rejects removed provider flags", async () => { const stderr: string[] = []; const exitCode = await runCli( - ["servercommand", "app", "cmd", "GET", "-p", "abc", "-s", "s"], + ["servercommand", "app", "cmd", "GET", "-p", "hzg", "-s", "s"], { stdout: () => undefined, stderr: (message) => stderr.push(message) @@ -138,7 +132,7 @@ describe("status command", () => { ); expect(exitCode).toBe(1); - expect(parseCliErrorMessage(stderr)).toBe('provider must be "hzg", got: abc'); + expect(parseCliErrorMessage(stderr)).toBe("unknown flag: -p"); }); it("prints service status without app invoke transport env vars", async () => { @@ -164,7 +158,7 @@ describe("status command", () => { it("accepts --sessionId as the canonical long flag", async () => { const stderr: string[] = []; const exitCode = await runCli( - ["servercommand", "app", "cmd", "GET", "-p", "hzg", "--sessionId", "tid"], + ["servercommand", "app", "cmd", "GET", "--sessionId", "tid"], { stdout: () => undefined, stderr: (message) => stderr.push(message) @@ -179,7 +173,7 @@ describe("status command", () => { it("rejects unknown long flags", async () => { const stderr: string[] = []; const exitCode = await runCli( - ["servercommand", "app", "cmd", "GET", "-p", "hzg", "--legacySessionFlag", "legacy-id", "-s", "tid"], + ["servercommand", "app", "cmd", "GET", "--legacySessionFlag", "legacy-id", "-s", "tid"], { stdout: () => undefined, stderr: (message) => stderr.push(message) @@ -193,7 +187,7 @@ describe("status command", () => { it("rejects removed --userName flag", async () => { const stderr: string[] = []; const exitCode = await runCli( - ["servercommand", "app", "cmd", "GET", "-p", "hzg", "--userName", "u", "-s", "tid"], + ["servercommand", "app", "cmd", "GET", "--userName", "u", "-s", "tid"], { stdout: () => undefined, stderr: (message) => stderr.push(message) @@ -207,7 +201,7 @@ describe("status command", () => { it("rejects removed --agentName flag", async () => { const stderr: string[] = []; const exitCode = await runCli( - ["servercommand", "app", "cmd", "GET", "-p", "hzg", "--agentName", "main", "-s", "tid"], + ["servercommand", "app", "cmd", "GET", "--agentName", "main", "-s", "tid"], { stdout: () => undefined, stderr: (message) => stderr.push(message) @@ -244,8 +238,8 @@ describe("status command", () => { expect(exitCode).toBe(0); const output = stdout.join(""); - expect(output).toContain("--provider"); expect(output).toContain("--sessionId"); + expect(output).toContain("--body-file"); expect(output).not.toContain("--currentUser"); expect(output).not.toContain("--agentName"); expect(output).not.toContain("--userName"); @@ -254,7 +248,7 @@ describe("status command", () => { it("rejects removed short flags -u and -a", async () => { const stderr: string[] = []; - const exitCode = await runCli(["servercommand", "app", "cmd", "GET", "-p", "hzg", "-s", "tid", "-u", "u"], { + const exitCode = await runCli(["servercommand", "app", "cmd", "GET", "-s", "tid", "-u", "u"], { stdout: () => undefined, stderr: (message) => stderr.push(message) }); @@ -265,7 +259,7 @@ describe("status command", () => { it("rejects removed short flag -a", async () => { const stderr: string[] = []; - const exitCode = await runCli(["servercommand", "app", "cmd", "GET", "-p", "hzg", "-s", "tid", "-a", "main"], { + const exitCode = await runCli(["servercommand", "app", "cmd", "GET", "-s", "tid", "-a", "main"], { stdout: () => undefined, stderr: (message) => stderr.push(message) }); diff --git a/packages/aios-management-web/package.json b/packages/aios-management-web/package.json index b5ab793..ef45542 100644 --- a/packages/aios-management-web/package.json +++ b/packages/aios-management-web/package.json @@ -1,6 +1,6 @@ { "name": "aios-management-web", - "version": "1.0.15", + "version": "1.0.16", "type": "module", "files": [ "dist", diff --git a/packages/aios-management-web/server/src/api/routes/index.js b/packages/aios-management-web/server/src/api/routes/index.js index 15ded6a..fab6054 100644 --- a/packages/aios-management-web/server/src/api/routes/index.js +++ b/packages/aios-management-web/server/src/api/routes/index.js @@ -133,12 +133,12 @@ function getCatalogSyncStatuses(services) { function getRuntimeEnvConfig(services) { const runtimeEnvConfigService = services.runtimeEnvConfigService; + const networkConfig = runtimeEnvConfigService?.getNetworkAllowlistConfig?.() || {}; return { admin_inbound_topic: services.env?.mqtt?.adminInboundTopic || "", ...runtimeEnvConfigService?.getRuntimeCredentials(), - agent_internal_network_allowlist: runtimeEnvConfigService - ?.getNetworkAllowlistConfig() - ?.agent_internal_network_allowlist || "" + agent_internal_network_allowlist: networkConfig.agent_internal_network_allowlist || "", + huozige_server_ip: networkConfig.huozige_server_ip || "" }; } @@ -294,17 +294,16 @@ export function createRoutes(services) { })); router.get("/internal/app-invoke/system", requireInternal, asyncRoute(async (req, res) => { - const provider = String(req.query.provider || ""); const applicationName = String(req.query.applicationName || ""); - if (!provider || !applicationName) { - throw badRequest("缺少 provider 或 applicationName 参数"); + if (!applicationName) { + throw badRequest("缺少 applicationName 参数"); } - const system = services.systemService.findSystemForInvocation(provider, applicationName); + const system = services.systemService.findSystemForInvocation(applicationName); if (!system) { res.status(404).json({ code: "not_found", - message: `未找到业务系统:${provider}/${applicationName}` + message: `未找到业务系统:${applicationName}` }); return; } @@ -329,7 +328,6 @@ export function createRoutes(services) { router.get("/external/cookie", requireServiceApiAuth, asyncRoute(async (req, res) => { const result = await services.externalService.getCookie( req.query.sessionId, - req.query.provider, req.query.applicationName ); res.json(result); @@ -778,11 +776,9 @@ export function createRoutes(services) { router.post("/systems", upload.single("ontology"), asyncRoute(async (req, res) => { services.authService.assertAdmin(req.currentUser); const payload = { - provider: req.body.provider, application_name: req.body.application_name, description: req.body.description, scheme: req.body.scheme, - host: req.body.host, port: req.body.port, status: req.body.status }; @@ -848,6 +844,17 @@ export function createRoutes(services) { })); })); + router.get("/systems/logs/:traceId/artifacts/:direction", asyncRoute(async (req, res) => { + const artifact = await services.systemService.downloadInvocationArtifact( + req.params.traceId, + req.params.direction + ); + res.setHeader("Content-Type", artifact.contentType); + res.setHeader("Content-Disposition", `attachment; filename="${artifact.fileName}"`); + res.setHeader("Cache-Control", "no-store"); + res.send(artifact.buffer); + })); + router.get("/systems/stats", asyncRoute(async (req, res) => { res.json(services.systemService.listStats({ applicationName: req.query.application_name || "", @@ -921,7 +928,7 @@ export function createRoutes(services) { services, req, "保存 IP 白名单", - "更新 env.json 中的 AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST" + "更新 env.json 中的 AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST / AIOS_HUOZIGE_SERVER_IP" ); res.json(result); })); diff --git a/packages/aios-management-web/server/src/app.js b/packages/aios-management-web/server/src/app.js index f1c94dd..1cdddc8 100644 --- a/packages/aios-management-web/server/src/app.js +++ b/packages/aios-management-web/server/src/app.js @@ -44,7 +44,7 @@ export function createServerApp() { const largeLanguageModelService = new LargeLanguageModelService({ db, managementClient }); const topicPingService = new TopicPingService({ db, env, managementClient }); const agentService = new AgentService({ db, managementClient, objectStorage, env }); - const systemService = new SystemService({ db, objectStorage, managementClient }); + const systemService = new SystemService({ db, objectStorage, managementClient, runtimeEnvConfigService, env }); const externalService = new ExternalService({ db, agentService, diff --git a/packages/aios-management-web/server/src/config/env.js b/packages/aios-management-web/server/src/config/env.js index a09cc9b..d08e580 100644 --- a/packages/aios-management-web/server/src/config/env.js +++ b/packages/aios-management-web/server/src/config/env.js @@ -54,6 +54,10 @@ export function loadEnv() { ? path.join(aiosRoot, "management", "data") : path.join("data"); const dataDir = path.resolve(readEnvValue("AIOS_WEB_DATA_DIR", defaultDataDir)); + const defaultAiosDataDir = aiosRoot + ? path.join(aiosRoot, "data") + : path.join("data"); + const aiosDataDir = path.resolve(readEnvValue("AIOS_DATA_DIR", defaultAiosDataDir)); const defaultRunDir = aiosRoot ? path.join(aiosRoot, "run") : path.join("run"); @@ -73,6 +77,7 @@ export function loadEnv() { return { port: readEnvNumber("AIOS_WEB_PORT", defaultPort), dataDir, + aiosDataDir, runDir, logDir, userEnvJsonPath: path.resolve(readEnvValue("AIOS_USER_ENV_JSON", defaultUserEnvJsonPath)), diff --git a/packages/aios-management-web/server/src/db/index.js b/packages/aios-management-web/server/src/db/index.js index 8457d98..1a0d61a 100644 --- a/packages/aios-management-web/server/src/db/index.js +++ b/packages/aios-management-web/server/src/db/index.js @@ -8,7 +8,7 @@ import { loadEnv } from "../config/env.js"; import { hashPassword } from "../utils/security.js"; const env = loadEnv(); -const SCHEMA_VERSION = 8; +const SCHEMA_VERSION = 9; const DEFAULT_ACCESS_TOKEN = "sk-1234567890qwertyuiop"; const DEFAULT_PORTAL_NAME = "数字员工控制台"; const DEFAULT_BRAND_SUBTITLE = "Your Company Name"; @@ -332,6 +332,62 @@ function migrateV7ToV8() { addAgentFallbackLlmColumns(); } +function createSystemInvocationLogTables() { + return ` + CREATE TABLE IF NOT EXISTS system_invocation_artifacts ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trace_id TEXT NOT NULL, + direction TEXT NOT NULL CHECK (direction IN ('request', 'response')), + content_type TEXT NOT NULL, + byte_size INTEGER NOT NULL, + sha256 TEXT NOT NULL, + storage_bucket TEXT, + storage_object_key TEXT, + preview_json TEXT NOT NULL DEFAULT '{}', + redaction_status TEXT NOT NULL DEFAULT 'previewed', + created_at TEXT NOT NULL + ); + + CREATE TABLE IF NOT EXISTS system_invocation_logs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + trace_id TEXT NOT NULL UNIQUE, + agent_slug TEXT, + session_id TEXT, + application_name TEXT NOT NULL, + command_name TEXT NOT NULL, + method TEXT NOT NULL DEFAULT '', + request_size_bytes INTEGER NOT NULL DEFAULT 0, + response_size_bytes INTEGER NOT NULL DEFAULT 0, + request_sha256 TEXT NOT NULL DEFAULT '', + response_sha256 TEXT NOT NULL DEFAULT '', + request_preview_json TEXT NOT NULL DEFAULT '{}', + response_preview_json TEXT NOT NULL DEFAULT '{}', + request_artifact_id INTEGER, + response_artifact_id INTEGER, + response_time_ms INTEGER NOT NULL, + success INTEGER NOT NULL, + error_message TEXT, + created_at TEXT NOT NULL, + FOREIGN KEY (request_artifact_id) REFERENCES system_invocation_artifacts(id) ON DELETE SET NULL, + FOREIGN KEY (response_artifact_id) REFERENCES system_invocation_artifacts(id) ON DELETE SET NULL + ); + + CREATE INDEX IF NOT EXISTS idx_system_invocation_logs_session_id + ON system_invocation_logs(session_id); + + CREATE INDEX IF NOT EXISTS idx_system_invocation_artifacts_trace_id + ON system_invocation_artifacts(trace_id); + `; +} + +function migrateV8ToV9() { + db.exec(` + DROP TABLE IF EXISTS system_invocation_logs; + DROP TABLE IF EXISTS system_invocation_artifacts; + ${createSystemInvocationLogTables()} + `); +} + function addAgentCreatorColumn() { if (!hasTable("agents")) { return; @@ -547,24 +603,7 @@ function createSchema() { FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE SET NULL ); - CREATE TABLE system_invocation_logs ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - trace_id TEXT NOT NULL UNIQUE, - agent_slug TEXT, - session_id TEXT, - provider TEXT NOT NULL, - application_name TEXT NOT NULL, - command_name TEXT NOT NULL, - request_payload_json TEXT NOT NULL, - response_payload_json TEXT NOT NULL, - response_time_ms INTEGER NOT NULL, - success INTEGER NOT NULL, - error_message TEXT, - created_at TEXT NOT NULL - ); - - CREATE INDEX idx_system_invocation_logs_session_id - ON system_invocation_logs(session_id); + ${createSystemInvocationLogTables()} CREATE TABLE management_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, @@ -753,6 +792,9 @@ function migrate() { if (SCHEMA_VERSION >= 8) { migrateV7ToV8(); } + if (SCHEMA_VERSION >= 9) { + migrateV8ToV9(); + } setSchemaVersion(SCHEMA_VERSION); db.exec("COMMIT"); } catch (error) { @@ -778,6 +820,9 @@ function migrate() { if (SCHEMA_VERSION >= 8) { migrateV7ToV8(); } + if (SCHEMA_VERSION >= 9) { + migrateV8ToV9(); + } setSchemaVersion(SCHEMA_VERSION); db.exec("COMMIT"); } catch (error) { @@ -800,6 +845,9 @@ function migrate() { if (SCHEMA_VERSION >= 8) { migrateV7ToV8(); } + if (SCHEMA_VERSION >= 9) { + migrateV8ToV9(); + } setSchemaVersion(SCHEMA_VERSION); db.exec("COMMIT"); } catch (error) { @@ -819,6 +867,9 @@ function migrate() { if (SCHEMA_VERSION >= 8) { migrateV7ToV8(); } + if (SCHEMA_VERSION >= 9) { + migrateV8ToV9(); + } setSchemaVersion(SCHEMA_VERSION); db.exec("COMMIT"); } catch (error) { @@ -835,6 +886,9 @@ function migrate() { if (SCHEMA_VERSION >= 8) { migrateV7ToV8(); } + if (SCHEMA_VERSION >= 9) { + migrateV8ToV9(); + } setSchemaVersion(SCHEMA_VERSION); db.exec("COMMIT"); } catch (error) { @@ -848,6 +902,9 @@ function migrate() { if (SCHEMA_VERSION >= 8) { migrateV7ToV8(); } + if (SCHEMA_VERSION >= 9) { + migrateV8ToV9(); + } setSchemaVersion(SCHEMA_VERSION); db.exec("COMMIT"); } catch (error) { @@ -858,6 +915,19 @@ function migrate() { db.exec("BEGIN"); try { migrateV7ToV8(); + if (SCHEMA_VERSION >= 9) { + migrateV8ToV9(); + } + setSchemaVersion(SCHEMA_VERSION); + db.exec("COMMIT"); + } catch (error) { + db.exec("ROLLBACK"); + throw error; + } + } else if (version === 8 && SCHEMA_VERSION >= 9) { + db.exec("BEGIN"); + try { + migrateV8ToV9(); setSchemaVersion(SCHEMA_VERSION); db.exec("COMMIT"); } catch (error) { @@ -884,6 +954,9 @@ function migrate() { addAgentCreatorColumn(); migrateV6ToV7(); migrateV7ToV8(); + if (!hasTableColumn("system_invocation_logs", "request_size_bytes")) { + migrateV8ToV9(); + } ensureCoreRows(); updateLegacyDefaultBranding(); } diff --git a/packages/aios-management-web/server/src/services/external-service.js b/packages/aios-management-web/server/src/services/external-service.js index 15c211d..c04d95c 100644 --- a/packages/aios-management-web/server/src/services/external-service.js +++ b/packages/aios-management-web/server/src/services/external-service.js @@ -6,6 +6,7 @@ import { normalizeUsage, statusAfterUsageRefresh } from "./agent-quota.js"; const MAX_COOKIE_LENGTH = 16 * 1024; const EXTERNAL_SESSION_DIGIT_LENGTH = 30; +const FIXED_BUSINESS_SYSTEM_PROVIDER = "hzg"; function generateExternalSessionId() { let digits = ""; @@ -15,14 +16,6 @@ function generateExternalSessionId() { return `s-${digits}`; } -function normalizeProvider(value) { - const provider = String(value || "").trim().toLowerCase(); - if (provider !== "hzg" && provider !== "phx") { - throw badRequest("provider 必须是 hzg 或 phx"); - } - return provider; -} - function normalizeCookie(value) { const cookie = String(value || "").trim(); if (cookie.length > MAX_COOKIE_LENGTH) { @@ -106,7 +99,7 @@ export class ExternalService { this.auditLogService = auditLogService; } - upsertSessionCookie(sessionId, provider, cookie) { + upsertSessionCookie(sessionId, cookie) { const now = toChinaISOString(); const normalizedCookie = normalizeCookie(cookie); this.db.prepare(` @@ -116,7 +109,7 @@ export class ExternalService { ON CONFLICT(session_id, provider) DO UPDATE SET cookie = excluded.cookie, updated_at = excluded.updated_at - `).run(sessionId, provider, normalizedCookie, now, now); + `).run(sessionId, FIXED_BUSINESS_SYSTEM_PROVIDER, normalizedCookie, now, now); } listAgentsForUser(username) { @@ -179,7 +172,7 @@ export class ExternalService { `).get(aiosUser.id, agent.id); if (existing?.session_id) { if (normalizedCookie) { - this.upsertSessionCookie(existing.session_id, "hzg", normalizedCookie); + this.upsertSessionCookie(existing.session_id, normalizedCookie); } return { sessionId: existing.session_id, @@ -197,7 +190,7 @@ export class ExternalService { `).run(sessionId, aiosUser.id, agent.id, now, now); if (normalizedCookie) { - this.upsertSessionCookie(sessionId, "hzg", normalizedCookie); + this.upsertSessionCookie(sessionId, normalizedCookie); } this.auditLogService.write({ @@ -214,9 +207,9 @@ export class ExternalService { }; } - async getCookie(sessionId, provider, applicationName = "") { + async getCookie(sessionId, applicationName = "") { const normalizedSessionId = String(sessionId || "").trim(); - const normalizedProvider = normalizeProvider(provider); + const normalizedProvider = FIXED_BUSINESS_SYSTEM_PROVIDER; const normalizedApplicationName = String(applicationName || "").trim().toLowerCase(); if (!normalizedSessionId) { throw badRequest("sessionId 不能为空"); @@ -229,7 +222,6 @@ export class ExternalService { `).get(normalizedSessionId, normalizedProvider); if (existing?.cookie) { return { - provider: normalizedProvider, cookie: existing.cookie }; } @@ -246,22 +238,17 @@ export class ExternalService { throw notFound(`会话不存在:${normalizedSessionId} 。请确保格式正确。`); } - if (normalizedProvider !== "hzg") { - throw notFound(`未找到 provider=${normalizedProvider} 的 Cookie`); - } - const system = normalizedApplicationName - ? this.systemService.findSystemForInvocation(normalizedProvider, normalizedApplicationName) - : this.systemService.findLatestActiveSystemByProvider(normalizedProvider); + ? this.systemService.findSystemForInvocation(normalizedApplicationName) + : this.systemService.findLatestActiveSystem(); if (!system) { - throw notFound(`未找到 provider=${normalizedProvider} 的可用业务系统`); + throw notFound("未找到可用业务系统"); } const cookie = await this.hzgProviderClient.createSessionCookie(system.base_url, normalizedSessionId); - this.upsertSessionCookie(normalizedSessionId, normalizedProvider, cookie); + this.upsertSessionCookie(normalizedSessionId, cookie); return { - provider: normalizedProvider, cookie }; } @@ -300,7 +287,6 @@ export class ExternalService { async recordInvocation(payload) { const sessionId = String(payload?.sessionId || "").trim(); - const provider = normalizeProvider(payload?.provider); const applicationName = String(payload?.applicationName || "").trim().toLowerCase(); const commandName = String(payload?.commandName || "").trim(); if (!sessionId || !applicationName || !commandName) { @@ -321,18 +307,18 @@ export class ExternalService { throw notFound(`会话不存在:${sessionId}`); } - const system = this.systemService.findSystemForInvocation(provider, applicationName); + const system = this.systemService.findSystemForInvocation(applicationName); if (!system) { - throw notFound(`未找到业务系统:${provider}/${applicationName}`); + throw notFound(`未找到业务系统:${applicationName}`); } await this.systemService.recordInvocation({ trace_id: randomUUID(), agent_slug: session.agent_slug, session_id: sessionId, - provider: system.provider || provider, application_name: applicationName, command_name: commandName, + method: payload?.method ? String(payload.method) : "", request_payload: parseJsonString(payload?.paramaters, {}), response_payload: normalizeResponsePayload(payload?.response), response_time_ms: Number(payload?.durationInMS || 0), diff --git a/packages/aios-management-web/server/src/services/portal-service.js b/packages/aios-management-web/server/src/services/portal-service.js index fe53619..097777a 100644 --- a/packages/aios-management-web/server/src/services/portal-service.js +++ b/packages/aios-management-web/server/src/services/portal-service.js @@ -1067,8 +1067,8 @@ export class PortalService { LIMIT 8 `).all().map((row) => ({ ...row, - request_payload: jsonParse(row.request_payload_json, {}), - response_payload: jsonParse(row.response_payload_json, {}) + request_preview: jsonParse(row.request_preview_json, {}), + response_preview: jsonParse(row.response_preview_json, {}) })); const agentUsage = this.db.prepare(` SELECT diff --git a/packages/aios-management-web/server/src/services/runtime-env-config-service.js b/packages/aios-management-web/server/src/services/runtime-env-config-service.js index 4b35658..66fb46d 100644 --- a/packages/aios-management-web/server/src/services/runtime-env-config-service.js +++ b/packages/aios-management-web/server/src/services/runtime-env-config-service.js @@ -4,6 +4,7 @@ import path from "node:path"; import { badRequest } from "../utils/errors.js"; export const AGENT_INTERNAL_NETWORK_ALLOWLIST_KEY = "AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST"; +export const HUOZIGE_SERVER_IP_KEY = "AIOS_HUOZIGE_SERVER_IP"; function firstText(...values) { for (const value of values) { @@ -26,6 +27,72 @@ function normalizeAllowlistValue(value) { return String(value).trim(); } +function normalizeServerIpValue(value) { + const normalized = String(value || "").trim(); + if ( + normalized.includes("/") + || normalized.includes("\\") + || normalized.includes("?") + || normalized.includes("#") + ) { + throw badRequest("对话应用/业务应用服务器格式不合法"); + } + return normalized; +} + +function parseAllowlistItems(value) { + if (value === undefined || value === null) { + return []; + } + + if (Array.isArray(value)) { + return value.map((item) => String(item || "").trim()).filter(Boolean); + } + + const text = String(value || "").trim(); + if (!text) { + return []; + } + + if (text.startsWith("[")) { + try { + const parsed = JSON.parse(text); + if (Array.isArray(parsed)) { + return parseAllowlistItems(parsed); + } + } catch { + } + } + + return text.split(/[,\s;]+/).map((item) => item.trim()).filter(Boolean); +} + +function uniqueAllowlistItems(items) { + const seen = new Set(); + const result = []; + for (const item of items) { + if (!item || seen.has(item)) { + continue; + } + seen.add(item); + result.push(item); + } + return result; +} + +function updateAllowlistServerIp(allowlistValue, previousIp, nextIp) { + const previous = normalizeServerIpValue(previousIp); + const next = normalizeServerIpValue(nextIp); + const retainedItems = parseAllowlistItems(allowlistValue) + .filter((item) => item !== previous); + + if (next) { + retainedItems.push(next); + } + + return uniqueAllowlistItems(retainedItems).join(","); +} + function readJsonObject(filePath, label = "JSON 文件") { if (!fs.existsSync(filePath)) { return {}; @@ -71,7 +138,8 @@ export class RuntimeEnvConfigService { getNetworkAllowlistConfig() { const payload = readJsonObject(this.envPath, "env.json"); return { - agent_internal_network_allowlist: normalizeAllowlistValue(payload[AGENT_INTERNAL_NETWORK_ALLOWLIST_KEY]) + agent_internal_network_allowlist: normalizeAllowlistValue(payload[AGENT_INTERNAL_NETWORK_ALLOWLIST_KEY]), + huozige_server_ip: normalizeServerIpValue(payload[HUOZIGE_SERVER_IP_KEY]) }; } @@ -87,9 +155,34 @@ export class RuntimeEnvConfigService { updateNetworkAllowlistConfig(payload) { const current = readJsonObject(this.envPath); - current[AGENT_INTERNAL_NETWORK_ALLOWLIST_KEY] = normalizeAllowlistValue( - payload?.agent_internal_network_allowlist - ); + const previousServerIp = normalizeServerIpValue(current[HUOZIGE_SERVER_IP_KEY]); + const hasServerIpPayload = Object.prototype.hasOwnProperty.call(payload || {}, "huozige_server_ip"); + + if (Object.prototype.hasOwnProperty.call(payload || {}, "agent_internal_network_allowlist")) { + current[AGENT_INTERNAL_NETWORK_ALLOWLIST_KEY] = normalizeAllowlistValue( + payload?.agent_internal_network_allowlist + ); + } + + if (hasServerIpPayload) { + const nextServerIp = normalizeServerIpValue(payload?.huozige_server_ip); + current[HUOZIGE_SERVER_IP_KEY] = nextServerIp; + current[AGENT_INTERNAL_NETWORK_ALLOWLIST_KEY] = updateAllowlistServerIp( + current[AGENT_INTERNAL_NETWORK_ALLOWLIST_KEY], + previousServerIp, + nextServerIp + ); + } else { + const currentServerIp = normalizeServerIpValue(current[HUOZIGE_SERVER_IP_KEY]); + if (currentServerIp) { + current[AGENT_INTERNAL_NETWORK_ALLOWLIST_KEY] = updateAllowlistServerIp( + current[AGENT_INTERNAL_NETWORK_ALLOWLIST_KEY], + "", + currentServerIp + ); + } + } + writeJsonObject(this.envPath, current); return this.getNetworkAllowlistConfig(); } diff --git a/packages/aios-management-web/server/src/services/system-service.js b/packages/aios-management-web/server/src/services/system-service.js index 3c0f57f..fa91ba9 100644 --- a/packages/aios-management-web/server/src/services/system-service.js +++ b/packages/aios-management-web/server/src/services/system-service.js @@ -1,4 +1,7 @@ import AdmZip from "adm-zip"; +import crypto from "node:crypto"; +import fs from "node:fs/promises"; +import path from "node:path"; import { toChinaISOString } from "../utils/china-time.js"; import { jsonParse } from "../db/index.js"; @@ -6,6 +9,12 @@ import { badRequest, conflict, notFound } from "../utils/errors.js"; const APPLICATION_NAME_PATTERN = /^[a-z0-9]+(?:-[a-z0-9]+)*$/; const VALID_SCHEMES = new Set(["http", "https"]); +const FIXED_BUSINESS_SYSTEM_PROVIDER = "hzg"; +const PAYLOAD_PREVIEW_MAX_STRING_LENGTH = 512; +const PAYLOAD_PREVIEW_MAX_ARRAY_ITEMS = 20; +const PAYLOAD_PREVIEW_MAX_OBJECT_KEYS = 50; +const PAYLOAD_PREVIEW_MAX_DEPTH = 6; +const INVOCATION_ARTIFACT_BUCKET = "aios-data"; function normalizeText(value) { return String(value || "").trim(); @@ -27,17 +36,6 @@ function normalizeScheme(value) { return scheme; } -function normalizeHost(value) { - const host = normalizeText(value); - if (!host) { - throw badRequest("主机名不能为空"); - } - if (host.includes("/") || host.includes("\\") || host.includes("?") || host.includes("#")) { - throw badRequest("主机名格式不合法"); - } - return host; -} - function normalizeApplicationName(value) { const applicationName = normalizeText(value).toLowerCase(); if (!applicationName) { @@ -53,6 +51,12 @@ function hasOwn(payload, key) { return Object.prototype.hasOwnProperty.call(payload || {}, key); } +function assertHostNotProvided(payload) { + if (hasOwn(payload, "host") && normalizeText(payload.host)) { + throw badRequest("业务系统主机名由基础设置中的对话应用/业务应用服务器统一配置"); + } +} + function isRemoteNotFoundError(error) { const message = String(error?.message || error?.details?.message || "").toLowerCase(); const code = String(error?.code || "").toLowerCase(); @@ -71,14 +75,16 @@ function buildBaseUrl(scheme, host, port, applicationName) { return `${scheme}://${host}:${port}/${applicationName}`; } -function decorateSystemRow(row) { +function decorateSystemRow(row, configuredHost = "") { if (!row) { return null; } - const baseUrl = buildBaseUrl(row.scheme, row.host, row.port, row.application_name); + const host = normalizeText(configuredHost) || row.host; + const baseUrl = buildBaseUrl(row.scheme, host, row.port, row.application_name); return { ...row, + host, is_builtin: Boolean(row.is_builtin), ontology_name: row.application_name, system_id: row.application_name, @@ -89,11 +95,144 @@ function decorateSystemRow(row) { }; } +function sha256(buffer) { + return crypto.createHash("sha256").update(buffer).digest("hex"); +} + +function jsonBuffer(value) { + try { + return Buffer.from(JSON.stringify(value ?? null), "utf8"); + } catch { + return Buffer.from(JSON.stringify(String(value)), "utf8"); + } +} + +function previewPayload(value, depth = 0) { + if (depth >= PAYLOAD_PREVIEW_MAX_DEPTH) { + return ""; + } + + if (typeof value === "string") { + if (value.length <= PAYLOAD_PREVIEW_MAX_STRING_LENGTH) { + return value; + } + + return { + type: "string", + omitted: true, + length: value.length, + preview: value.slice(0, PAYLOAD_PREVIEW_MAX_STRING_LENGTH) + }; + } + + if (Array.isArray(value)) { + return { + type: "array", + length: value.length, + items: value.slice(0, PAYLOAD_PREVIEW_MAX_ARRAY_ITEMS).map((item) => previewPayload(item, depth + 1)), + truncated: value.length > PAYLOAD_PREVIEW_MAX_ARRAY_ITEMS + }; + } + + if (value && typeof value === "object") { + const entries = Object.entries(value); + const result = {}; + for (const [key, child] of entries.slice(0, PAYLOAD_PREVIEW_MAX_OBJECT_KEYS)) { + result[key] = previewPayload(child, depth + 1); + } + if (entries.length > PAYLOAD_PREVIEW_MAX_OBJECT_KEYS) { + result.__truncated_keys = entries.length - PAYLOAD_PREVIEW_MAX_OBJECT_KEYS; + } + return result; + } + + return value; +} + +function summarizePayload(value) { + const buffer = jsonBuffer(value); + return { + buffer, + byteSize: buffer.byteLength, + sha256: sha256(buffer), + preview: previewPayload(value) + }; +} + +function compactChinaDateKey(value) { + return toChinaISOString(value).slice(0, 10).replace(/-/g, ""); +} + +function compactChinaTimestamp(value) { + const timestamp = toChinaISOString(value); + return [ + timestamp.slice(0, 4), + timestamp.slice(5, 7), + timestamp.slice(8, 10), + timestamp.slice(11, 13), + timestamp.slice(14, 16), + timestamp.slice(17, 19), + timestamp.slice(20, 23) + ].join(""); +} + +function normalizeFileSegment(value, fallback) { + const segment = normalizeText(value) + .toLowerCase() + .replace(/[^a-z0-9_-]+/g, "-") + .replace(/^-+|-+$/g, ""); + return segment || fallback; +} + +function randomSixDigits() { + return crypto.randomInt(0, 1_000_000).toString().padStart(6, "0"); +} + +function isPathInside(rootPath, targetPath) { + const relativePath = path.relative(rootPath, targetPath); + return Boolean(relativePath) && !relativePath.startsWith("..") && !path.isAbsolute(relativePath); +} + export class SystemService { - constructor({ db, objectStorage, managementClient }) { + constructor({ db, objectStorage, managementClient, runtimeEnvConfigService, env = {} }) { this.db = db; this.objectStorage = objectStorage; this.managementClient = managementClient; + this.runtimeEnvConfigService = runtimeEnvConfigService; + this.aiosDataDir = path.resolve(env.aiosDataDir || process.env.AIOS_DATA_DIR || "data"); + } + + getConfiguredHuozigeServerIp() { + return normalizeText( + this.runtimeEnvConfigService + ?.getNetworkAllowlistConfig + ?.() + ?.huozige_server_ip + ); + } + + requireConfiguredHuozigeServerIp() { + const host = this.getConfiguredHuozigeServerIp(); + if (!host) { + throw badRequest("请先在基础设置中配置对话应用/业务应用服务器"); + } + return host; + } + + resolveSystemHost(row) { + const host = this.getConfiguredHuozigeServerIp() || normalizeText(row?.host); + if (!host) { + throw badRequest("请先在基础设置中配置对话应用/业务应用服务器"); + } + return host; + } + + decorateSystemRow(row) { + return decorateSystemRow(row, this.getConfiguredHuozigeServerIp()); + } + + buildSystemBaseUrl(row) { + return buildBaseUrl(row.scheme, this.resolveSystemHost(row), row.port, row.application_name); } listSystems() { @@ -104,11 +243,11 @@ export class SystemService { FROM business_systems s LEFT JOIN artifacts a ON a.id = s.ontology_artifact_id ORDER BY s.updated_at DESC - `).all().map(decorateSystemRow); + `).all().map((row) => this.decorateSystemRow(row)); } getSystemById(id) { - return decorateSystemRow( + return this.decorateSystemRow( this.db.prepare(` SELECT s.*, @@ -133,24 +272,23 @@ export class SystemService { return { application_name: row.application_name, - provider: row.provider, - baseUrl: buildBaseUrl(row.scheme, row.host, row.port, row.application_name), + baseUrl: this.buildSystemBaseUrl(row), scheme: row.scheme, - host: row.host, + host: this.resolveSystemHost(row), port: row.port, status: row.status }; } - findLatestActiveSystemByProvider(provider) { + findLatestActiveSystem() { const row = this.db.prepare(` SELECT * FROM business_systems WHERE provider = ? AND status = 'active' ORDER BY updated_at DESC, id DESC LIMIT 1 - `).get(normalizeText(provider).toLowerCase()); - return decorateSystemRow(row); + `).get(FIXED_BUSINESS_SYSTEM_PROVIDER); + return this.decorateSystemRow(row); } validateUniqueApplicationName(applicationName, excludeId = null) { @@ -172,38 +310,30 @@ export class SystemService { } normalizeCreatePayload(payload) { - const provider = normalizeText(payload.provider).toLowerCase(); + assertHostNotProvided(payload); const applicationName = normalizeApplicationName(payload.application_name); - if (!provider || !applicationName) { - throw badRequest("提供方和应用名不能为空"); - } - return { - provider, + provider: FIXED_BUSINESS_SYSTEM_PROVIDER, application_name: applicationName, description: normalizeText(payload.description), scheme: normalizeScheme(payload.scheme), - host: normalizeHost(payload.host), + host: this.requireConfiguredHuozigeServerIp(), port: normalizePort(payload.port), status: normalizeText(payload.status) === "disabled" ? "disabled" : "active" }; } normalizeUpdatePayload(payload, current) { - const provider = hasOwn(payload, "provider") ? normalizeText(payload.provider).toLowerCase() : current.provider; + assertHostNotProvided(payload); const applicationName = hasOwn(payload, "application_name") ? normalizeApplicationName(payload.application_name) : current.application_name; - if (!provider || !applicationName) { - throw badRequest("提供方和应用名不能为空"); - } - return { - provider, + provider: FIXED_BUSINESS_SYSTEM_PROVIDER, application_name: applicationName, description: hasOwn(payload, "description") ? normalizeText(payload.description) : current.description, scheme: hasOwn(payload, "scheme") ? normalizeScheme(payload.scheme) : current.scheme, - host: hasOwn(payload, "host") ? normalizeHost(payload.host) : current.host, + host: this.resolveSystemHost(current), port: hasOwn(payload, "port") ? normalizePort(payload.port) : current.port, status: hasOwn(payload, "status") ? (normalizeText(payload.status) === "disabled" ? "disabled" : "active") @@ -267,12 +397,7 @@ export class SystemService { normalized.application_name ]); - const baseUrl = buildBaseUrl( - normalized.scheme, - normalized.host, - normalized.port, - normalized.application_name - ); + const baseUrl = this.buildSystemBaseUrl(normalized); await this.managementClient.call("apps.create", { systemId: normalized.application_name, name: normalized.application_name, @@ -366,12 +491,7 @@ export class SystemService { const existingArtifact = artifact || this.getArtifactStorageInfo(artifactId); - const baseUrl = buildBaseUrl( - normalized.scheme, - normalized.host, - normalized.port, - normalized.application_name - ); + const baseUrl = this.buildSystemBaseUrl(normalized); const previousSystemId = current.application_name; const { deletedApplicationNames } = await this.deleteRemoteOntologiesIfPresent([ previousSystemId, @@ -457,7 +577,7 @@ export class SystemService { throw notFound("业务系统不存在"); } - const baseUrl = buildBaseUrl(current.scheme, current.host, current.port, current.application_name); + const baseUrl = this.buildSystemBaseUrl(current); const response = await fetch(baseUrl, { method: "GET" }); const ok = response.ok; const result = { @@ -476,34 +596,143 @@ export class SystemService { return result; } - findSystemForInvocation(provider, applicationName) { - return decorateSystemRow(this.db.prepare(` + findSystemForInvocation(applicationName) { + return this.decorateSystemRow(this.db.prepare(` SELECT * FROM business_systems WHERE provider = ? AND application_name = ? AND status = 'active' - `).get(normalizeText(provider).toLowerCase(), normalizeApplicationName(applicationName))); + `).get(FIXED_BUSINESS_SYSTEM_PROVIDER, normalizeApplicationName(applicationName))); + } + + async writeInvocationArtifactFile({ applicationName, summary, createdAt }) { + const dateKey = compactChinaDateKey(createdAt); + const timestamp = compactChinaTimestamp(createdAt); + const systemName = normalizeFileSegment(applicationName, "unknown-system"); + const relativeDir = path.posix.join("logs", "invoke", dateKey); + const absoluteDir = path.join(this.aiosDataDir, "logs", "invoke", dateKey); + await fs.mkdir(absoluteDir, { recursive: true }); + + for (let index = 0; index < 5; index += 1) { + const fileName = `${systemName}-${timestamp}-${randomSixDigits()}.log`; + const absolutePath = path.join(absoluteDir, fileName); + try { + await fs.writeFile(absolutePath, summary.buffer, { flag: "wx" }); + return { + bucket: INVOCATION_ARTIFACT_BUCKET, + objectKey: path.posix.join(relativeDir, fileName) + }; + } catch (error) { + if (error?.code === "EEXIST") { + continue; + } + throw error; + } + } + + throw new Error("failed to allocate invocation payload file name"); + } + + resolveInvocationArtifactFilePath(objectKey) { + const rootPath = path.resolve(this.aiosDataDir); + const segments = normalizeText(objectKey).split(/[\\/]+/).filter(Boolean); + const filePath = path.resolve(rootPath, ...segments); + if (!isPathInside(rootPath, filePath)) { + throw notFound("调用 payload artifact 不存在"); + } + return filePath; + } + + async persistInvocationArtifact({ traceId, direction, applicationName, summary, createdAt }) { + let storage = null; + let redactionStatus = "metadata_only"; + try { + storage = await this.writeInvocationArtifactFile({ + applicationName, + summary, + createdAt + }); + redactionStatus = "stored"; + } catch { + storage = null; + } + + const result = this.db.prepare(` + INSERT INTO system_invocation_artifacts ( + trace_id, direction, content_type, byte_size, sha256, + storage_bucket, storage_object_key, preview_json, redaction_status, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `).run( + traceId, + direction, + "application/json", + summary.byteSize, + summary.sha256, + storage?.bucket || null, + storage?.objectKey || null, + JSON.stringify(summary.preview ?? {}), + redactionStatus, + createdAt + ); + + return { + id: result.lastInsertRowid, + direction, + content_type: "application/json", + byte_size: summary.byteSize, + sha256: summary.sha256, + storage_bucket: storage?.bucket || null, + storage_object_key: storage?.objectKey || null, + preview: summary.preview, + redaction_status: redactionStatus + }; } async recordInvocation(log) { + const createdAt = log.created_at || toChinaISOString(); + const traceId = log.trace_id; + const requestSummary = summarizePayload(log.request_payload ?? {}); + const responseSummary = summarizePayload(log.response_payload ?? {}); + const requestArtifact = await this.persistInvocationArtifact({ + traceId, + direction: "request", + applicationName: log.application_name, + summary: requestSummary, + createdAt + }); + const responseArtifact = await this.persistInvocationArtifact({ + traceId, + direction: "response", + applicationName: log.application_name, + summary: responseSummary, + createdAt + }); + this.db.prepare(` INSERT INTO system_invocation_logs ( - trace_id, agent_slug, session_id, provider, application_name, - command_name, request_payload_json, response_payload_json, response_time_ms, - success, error_message, created_at - ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + trace_id, agent_slug, session_id, application_name, command_name, method, + request_size_bytes, response_size_bytes, request_sha256, response_sha256, + request_preview_json, response_preview_json, request_artifact_id, response_artifact_id, + response_time_ms, success, error_message, created_at + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) `).run( - log.trace_id, + traceId, log.agent_slug || null, log.session_id || null, - log.provider, log.application_name, log.command_name, - JSON.stringify(log.request_payload ?? {}), - JSON.stringify(log.response_payload ?? {}), + log.method || "", + requestSummary.byteSize, + responseSummary.byteSize, + requestSummary.sha256, + responseSummary.sha256, + JSON.stringify(requestSummary.preview ?? {}), + JSON.stringify(responseSummary.preview ?? {}), + requestArtifact.id, + responseArtifact.id, log.response_time_ms, log.success ? 1 : 0, log.error_message || null, - log.created_at || toChinaISOString() + createdAt ); try { @@ -511,24 +740,33 @@ export class SystemService { systemId: log.application_name, agentId: log.agent_slug, conversationId: log.session_id, - provider: log.provider, + provider: FIXED_BUSINESS_SYSTEM_PROVIDER, applicationName: log.application_name, commandName: log.command_name, - inputParams: log.request_payload, + inputParams: requestSummary.preview, responseTimeMs: log.response_time_ms, success: log.success, - responseContent: log.response_payload + responseContent: responseSummary.preview }); } catch {} } listLogs({ applicationName = "", agentSlug = "" } = {}) { return this.db.prepare(` - SELECT * - FROM system_invocation_logs - WHERE (? = '' OR application_name = ?) - AND (? = '' OR agent_slug = ?) - ORDER BY created_at DESC + SELECT + l.*, + request_artifact.redaction_status AS request_redaction_status, + request_artifact.storage_object_key AS request_storage_object_key, + response_artifact.redaction_status AS response_redaction_status, + response_artifact.storage_object_key AS response_storage_object_key + FROM system_invocation_logs l + LEFT JOIN system_invocation_artifacts request_artifact + ON request_artifact.id = l.request_artifact_id + LEFT JOIN system_invocation_artifacts response_artifact + ON response_artifact.id = l.response_artifact_id + WHERE (? = '' OR l.application_name = ?) + AND (? = '' OR l.agent_slug = ?) + ORDER BY l.created_at DESC LIMIT 200 `).all( normalizeText(applicationName).toLowerCase(), @@ -538,16 +776,58 @@ export class SystemService { ).map((row) => ({ ...row, system_id: row.application_name, - request_payload: jsonParse(row.request_payload_json, {}), - response_payload: jsonParse(row.response_payload_json, {}) + success: Boolean(row.success), + request_preview: jsonParse(row.request_preview_json, {}), + response_preview: jsonParse(row.response_preview_json, {}), + request_artifact: row.request_artifact_id ? { + id: row.request_artifact_id, + direction: "request", + byte_size: row.request_size_bytes, + sha256: row.request_sha256, + redaction_status: row.request_redaction_status, + downloadable: Boolean(row.request_storage_object_key) + } : null, + response_artifact: row.response_artifact_id ? { + id: row.response_artifact_id, + direction: "response", + byte_size: row.response_size_bytes, + sha256: row.response_sha256, + redaction_status: row.response_redaction_status, + downloadable: Boolean(row.response_storage_object_key) + } : null })); } + async downloadInvocationArtifact(traceId, direction) { + const normalizedDirection = normalizeText(direction); + if (normalizedDirection !== "request" && normalizedDirection !== "response") { + throw badRequest("payload direction must be request or response"); + } + + const row = this.db.prepare(` + SELECT * + FROM system_invocation_artifacts + WHERE trace_id = ? AND direction = ? + ORDER BY id DESC + LIMIT 1 + `).get(normalizeText(traceId), normalizedDirection); + if (!row || row.storage_bucket !== INVOCATION_ARTIFACT_BUCKET || !row.storage_object_key) { + throw notFound("调用 payload artifact 不存在"); + } + + const filePath = this.resolveInvocationArtifactFilePath(row.storage_object_key); + const buffer = await fs.readFile(filePath); + return { + buffer, + contentType: row.content_type || "application/json", + fileName: path.basename(filePath) + }; + } + listStats({ applicationName = "", agentSlug = "" } = {}) { return this.db.prepare(` SELECT application_name, - provider, command_name, COUNT(*) AS call_count, SUM(CASE WHEN success = 1 THEN 1 ELSE 0 END) AS success_count, @@ -555,7 +835,7 @@ export class SystemService { FROM system_invocation_logs WHERE (? = '' OR application_name = ?) AND (? = '' OR agent_slug = ?) - GROUP BY application_name, provider, command_name + GROUP BY application_name, command_name ORDER BY call_count DESC, avg_response_ms ASC `).all( normalizeText(applicationName).toLowerCase(), diff --git a/packages/aios-management-web/server/test/db-reset-migration.test.js b/packages/aios-management-web/server/test/db-reset-migration.test.js index e5d100e..55163a2 100644 --- a/packages/aios-management-web/server/test/db-reset-migration.test.js +++ b/packages/aios-management-web/server/test/db-reset-migration.test.js @@ -203,6 +203,11 @@ it("resets drifted legacy schemas to the latest database structure", async () => const skillColumns = db.prepare("PRAGMA table_info(skills)").all().map((row) => row.name); const systemColumns = db.prepare("PRAGMA table_info(business_systems)").all().map((row) => row.name); const invocationColumns = db.prepare("PRAGMA table_info(system_invocation_logs)").all().map((row) => row.name); + const invocationArtifactTable = db.prepare(` + SELECT name + FROM sqlite_master + WHERE type = 'table' AND name = 'system_invocation_artifacts' + `).get(); const sessionCookieColumns = db.prepare("PRAGMA table_info(external_session_cookies)").all(); const externalSessionColumns = db.prepare("PRAGMA table_info(external_sessions)").all(); const aiosUsersTable = db.prepare(` @@ -220,6 +225,11 @@ it("resets drifted legacy schemas to the latest database structure", async () => FROM sqlite_master WHERE type = 'index' AND name = 'idx_system_invocation_logs_session_id' `).get(); + const artifactTraceIdIndexes = db.prepare(` + SELECT name + FROM sqlite_master + WHERE type = 'index' AND name = 'idx_system_invocation_artifacts_trace_id' + `).get(); const legacyTables = db.prepare(` SELECT name FROM sqlite_master @@ -229,7 +239,7 @@ it("resets drifted legacy schemas to the latest database structure", async () => const settingsColumns = db.prepare("PRAGMA table_info(settings)").all().map((row) => row.name); - expect(schemaVersion?.value).toBe("8"); + expect(schemaVersion?.value).toBe("9"); expect(settingsColumns).toContain("logo_mime_type"); expect(settingsColumns).toContain("logo_data"); expect(aiosUsersTable?.name).toBe("aios_users"); @@ -264,10 +274,23 @@ it("resets drifted legacy schemas to the latest database structure", async () => expect(systemColumns).not.toContain("connectivity_status"); expect(systemColumns).not.toContain("last_test_result_json"); expect(invocationColumns).not.toContain("system_name"); + expect(invocationColumns).toContain("method"); + expect(invocationColumns).toContain("request_size_bytes"); + expect(invocationColumns).toContain("response_size_bytes"); + expect(invocationColumns).toContain("request_sha256"); + expect(invocationColumns).toContain("response_sha256"); + expect(invocationColumns).toContain("request_preview_json"); + expect(invocationColumns).toContain("response_preview_json"); + expect(invocationColumns).toContain("request_artifact_id"); + expect(invocationColumns).toContain("response_artifact_id"); + expect(invocationColumns).not.toContain("request_payload_json"); + expect(invocationColumns).not.toContain("response_payload_json"); + expect(invocationArtifactTable?.name).toBe("system_invocation_artifacts"); expect(sessionCookieColumns.find((column) => column.name === "cookie")?.notnull).toBe(1); expect(externalSessionColumns.find((column) => column.name === "session_id")?.pk).toBe(1); expect(agentSkillBindingsTable).toBeUndefined(); expect(sessionIdIndexes?.name).toBe("idx_system_invocation_logs_session_id"); + expect(artifactTraceIdIndexes?.name).toBe("idx_system_invocation_artifacts_trace_id"); expect(() => db.prepare(` INSERT INTO external_session_cookies ( session_id, provider, cookie, created_at, updated_at @@ -432,7 +455,7 @@ it("migrates v1 agents to template relationships without dropping data", async ( ORDER BY slug `).all(); - expect(schemaVersion?.value).toBe("8"); + expect(schemaVersion?.value).toBe("9"); expect(agentColumns).toContain("template_name"); expect(agentColumns).toContain("llm_model_ref"); expect(agentColumns).toContain("fallback_llm_model_ref"); @@ -554,7 +577,7 @@ it("migrates v3 templates to default llm columns and removes template descriptio const templateColumns = db.prepare("PRAGMA table_info(agent_templates)").all().map((row) => row.name); const template = db.prepare("SELECT template_name, default_llm_model_ref FROM agent_templates WHERE template_name = ?").get("default"); - expect(schemaVersion?.value).toBe("8"); + expect(schemaVersion?.value).toBe("9"); expect(templateColumns).toContain("default_llm_provider_id"); expect(templateColumns).toContain("default_llm_model_id"); expect(templateColumns).toContain("default_llm_model_ref"); @@ -638,7 +661,7 @@ it("migrates v4 settings through uploaded logo columns without dropping data", a const schemaVersion = db.prepare("SELECT value FROM schema_meta WHERE key = 'schema_version'").get(); const settings = db.prepare("SELECT portal_name, brand_subtitle, theme_color, logo_mime_type, logo_data FROM settings WHERE id = 1").get(); - expect(schemaVersion?.value).toBe("8"); + expect(schemaVersion?.value).toBe("9"); expect(settings).toEqual({ portal_name: "AIOS", brand_subtitle: "Test", @@ -779,7 +802,7 @@ it("migrates v5 templates by dropping template description without dropping data WHERE template_name = ? `).get("default"); - expect(schemaVersion?.value).toBe("8"); + expect(schemaVersion?.value).toBe("9"); expect(templateColumns).not.toContain("description"); expect(template).toEqual({ id: 1, diff --git a/packages/aios-management-web/server/test/env-config.test.js b/packages/aios-management-web/server/test/env-config.test.js index 40a8d19..af7d3e1 100644 --- a/packages/aios-management-web/server/test/env-config.test.js +++ b/packages/aios-management-web/server/test/env-config.test.js @@ -51,6 +51,22 @@ it("defaults web data dir under AIOS_ROOT management directory", () => { } }); +it("reads AIOS data dir for runtime payload files", () => { + const originalDataDir = process.env.AIOS_DATA_DIR; + process.env.AIOS_DATA_DIR = "/var/aios/data"; + + try { + const env = loadEnv(); + expect(env.aiosDataDir).toBe(path.resolve("/var/aios/data")); + } finally { + if (originalDataDir === undefined) { + delete process.env.AIOS_DATA_DIR; + } else { + process.env.AIOS_DATA_DIR = originalDataDir; + } + } +}); + it("defaults credentials json path under AIOS_ROOT", () => { const originalRoot = process.env.AIOS_ROOT; const originalCredentialsJson = process.env.AIOS_CREDENTIALS_JSON; diff --git a/packages/aios-management-web/server/test/external-service.test.js b/packages/aios-management-web/server/test/external-service.test.js index 85b2280..dc1a8fa 100644 --- a/packages/aios-management-web/server/test/external-service.test.js +++ b/packages/aios-management-web/server/test/external-service.test.js @@ -87,14 +87,14 @@ function createService(db, overrides = {}) { } }, systemService: { - findLatestActiveSystemByProvider() { + findLatestActiveSystem() { return { provider: "hzg", application_name: "crm-app", base_url: "https://example.com:443/crm-app" }; }, - findSystemForInvocation(_provider, applicationName) { + findSystemForInvocation(applicationName) { return { provider: "hzg", application_name: applicationName, @@ -269,9 +269,8 @@ it("persists caller cookie on session creation and reuses it for external cookie cookie: "ForguncyServer=caller-cookie" }); - const cookie = await service.getCookie(session.sessionId, "hzg"); + const cookie = await service.getCookie(session.sessionId, "crm-app"); expect(cookie).toEqual({ - provider: "hzg", cookie: "ForguncyServer=caller-cookie" }); }); @@ -322,11 +321,10 @@ it("loads cached cookie and lazily creates missing hzg cookie", async () => { } }); - const first = await service.getCookie("s-1", "hzg"); - const second = await service.getCookie("s-1", "hzg"); + const first = await service.getCookie("s-1", "crm-app"); + const second = await service.getCookie("s-1", "crm-app"); expect(first).toEqual({ - provider: "hzg", cookie: "ForguncyServer=test-cookie" }); expect(second).toEqual(first); @@ -369,11 +367,10 @@ it("records external invocation using session-bound agent and resolved business const recorded = []; const service = createService(db, { systemService: { - findLatestActiveSystemByProvider() { + findLatestActiveSystem() { return null; }, - findSystemForInvocation(provider, applicationName) { - expect(provider).toBe("hzg"); + findSystemForInvocation(applicationName) { expect(applicationName).toBe("crm-app"); return { provider: "hzg", @@ -389,7 +386,6 @@ it("records external invocation using session-bound agent and resolved business await service.recordInvocation({ sessionId: "s-2", - provider: "hzg", applicationName: "crm-app", commandName: "GetList", paramaters: "{\"page\":1}", @@ -403,7 +399,6 @@ it("records external invocation using session-bound agent and resolved business expect(recorded[0]).toEqual(jasmine.objectContaining({ agent_slug: "agent-a", session_id: "s-2", - provider: "hzg", application_name: "crm-app", command_name: "GetList", response_time_ms: 123, diff --git a/packages/aios-management-web/server/test/runtime-env-config-service.test.js b/packages/aios-management-web/server/test/runtime-env-config-service.test.js index acc5156..853408d 100644 --- a/packages/aios-management-web/server/test/runtime-env-config-service.test.js +++ b/packages/aios-management-web/server/test/runtime-env-config-service.test.js @@ -21,7 +21,8 @@ it("reads the agent internal network allowlist from env.json", () => { const service = new RuntimeEnvConfigService({ envPath }); expect(service.getNetworkAllowlistConfig()).toEqual({ - agent_internal_network_allowlist: "192.168.1.10,10.20.0.0/16" + agent_internal_network_allowlist: "192.168.1.10,10.20.0.0/16", + huozige_server_ip: "" }); } finally { fs.rmSync(tempDir, { recursive: true, force: true }); @@ -45,7 +46,8 @@ it("updates the agent internal network allowlist without dropping other env.json const saved = JSON.parse(fs.readFileSync(envPath, "utf8")); expect(result).toEqual({ - agent_internal_network_allowlist: "192.168.1.10 ; 10.20.0.0/16" + agent_internal_network_allowlist: "192.168.1.10 ; 10.20.0.0/16", + huozige_server_ip: "" }); expect(saved).toEqual({ AIOS_MANAGEMENT_CONSOLE_PORT: 3030, @@ -57,6 +59,36 @@ it("updates the agent internal network allowlist without dropping other env.json } }); +it("updates the huozige server ip and replaces it in the allowlist", () => { + const tempDir = createTempDir(); + const envPath = path.join(tempDir, "env.json"); + fs.writeFileSync(envPath, JSON.stringify({ + AIOS_MANAGEMENT_CONSOLE_PORT: 3030, + AIOS_HUOZIGE_SERVER_IP: "192.168.1.10", + AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST: "10.0.0.2,192.168.1.10;10.20.0.0/16" + }, null, 2)); + + try { + const service = new RuntimeEnvConfigService({ envPath }); + const result = service.updateNetworkAllowlistConfig({ + huozige_server_ip: "192.168.1.11" + }); + const saved = JSON.parse(fs.readFileSync(envPath, "utf8")); + + expect(result).toEqual({ + agent_internal_network_allowlist: "10.0.0.2,10.20.0.0/16,192.168.1.11", + huozige_server_ip: "192.168.1.11" + }); + expect(saved).toEqual({ + AIOS_MANAGEMENT_CONSOLE_PORT: 3030, + AIOS_HUOZIGE_SERVER_IP: "192.168.1.11", + AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST: "10.0.0.2,10.20.0.0/16,192.168.1.11" + }); + } finally { + fs.rmSync(tempDir, { recursive: true, force: true }); + } +}); + it("reads mqtt and s3 credentials from credentials.json", () => { const tempDir = createTempDir(); const credentialsPath = path.join(tempDir, "credentials.json"); diff --git a/packages/aios-management-web/server/test/runtime-settings-route.test.js b/packages/aios-management-web/server/test/runtime-settings-route.test.js index f63fc25..a959992 100644 --- a/packages/aios-management-web/server/test/runtime-settings-route.test.js +++ b/packages/aios-management-web/server/test/runtime-settings-route.test.js @@ -105,7 +105,8 @@ it("includes the env allowlist config in bootstrap", async () => { }, getNetworkAllowlistConfig() { return { - agent_internal_network_allowlist: "192.168.1.10" + agent_internal_network_allowlist: "192.168.1.10", + huozige_server_ip: "192.168.1.20" }; } } @@ -124,6 +125,7 @@ it("includes the env allowlist config in bootstrap", async () => { expect(response.status).toBe(200); expect(response.body.env_config.agent_internal_network_allowlist).toBe("192.168.1.10"); + expect(response.body.env_config.huozige_server_ip).toBe("192.168.1.20"); expect(response.body.env_config.mqtt_broker_username).toBe("mqtt-user"); expect(response.body.env_config.mqtt_broker_password).toBe("mqtt-pass"); expect(response.body.env_config.s3_api_username).toBe("s3-user"); @@ -174,7 +176,7 @@ it("updates the env allowlist config and writes an audit log", async () => { userId: 1, username: "aios", action: "保存 IP 白名单", - detail: "更新 env.json 中的 AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST" + detail: "更新 env.json 中的 AIOS_AGENT_INTERNAL_NETWORK_ALLOWLIST / AIOS_HUOZIGE_SERVER_IP" }]); } finally { await close(server); diff --git a/packages/aios-management-web/server/test/system-service-alignment.test.js b/packages/aios-management-web/server/test/system-service-alignment.test.js index 74ef952..01406e2 100644 --- a/packages/aios-management-web/server/test/system-service-alignment.test.js +++ b/packages/aios-management-web/server/test/system-service-alignment.test.js @@ -73,6 +73,13 @@ it("maps website fields to management action runtime apps.create params", async actionCalls.push({ action, params }); return {}; } + }, + runtimeEnvConfigService: { + getNetworkAllowlistConfig() { + return { + huozige_server_ip: "example.com" + }; + } } }); @@ -84,7 +91,6 @@ it("maps website fields to management action runtime apps.create params", async application_name: "demo-app", description: "desc", scheme: "https", - host: "example.com", port: 443, status: "active" }, @@ -104,7 +110,6 @@ it("maps website fields to management action runtime apps.create params", async application_name: "demo-app", description: "desc", scheme: "https", - host: "example.com", port: 443, status: "active" }, @@ -144,7 +149,7 @@ it("maps website fields to management action runtime apps.create params", async }); }); -it("recreates remote system when scheme/host/port fields change", async () => { +it("recreates remote system with the configured huozige server host", async () => { const currentSystem = { id: 1, provider: "hzg", @@ -159,7 +164,9 @@ it("recreates remote system when scheme/host/port fields change", async () => { const updatedSystem = { ...currentSystem, description: "new desc", - host: "new.example.com", + scheme: "http", + host: "configured.example.com", + port: 8443, status: "active" }; const db = createDb({ currentSystem, createdSystem: updatedSystem }); @@ -172,13 +179,21 @@ it("recreates remote system when scheme/host/port fields change", async () => { actionCalls.push({ action, params }); return {}; } + }, + runtimeEnvConfigService: { + getNetworkAllowlistConfig() { + return { + huozige_server_ip: "configured.example.com" + }; + } } }); const result = await service.updateSystem(1, { payload: { description: "new desc", - host: "new.example.com", + scheme: "http", + port: 8443, status: "active" }, file: null, @@ -202,8 +217,8 @@ it("recreates remote system when scheme/host/port fields change", async () => { provider: "hzg", ontologyName: "demo-app", description: "new desc", - applicationEndpoint: "https://new.example.com:443/demo-app", - sessionServiceEndpoint: "https://new.example.com:443/demo-app", + applicationEndpoint: "http://configured.example.com:8443/demo-app", + sessionServiceEndpoint: "http://configured.example.com:8443/demo-app", status: "active", bucket: "admin-in", objectKey: "ontology/existing.zip", @@ -246,3 +261,75 @@ it("rejects deleting built-in ontology systems", async () => { } expect(actionCalls).toEqual([]); }); + +it("stores invocation payload files under AIOS data logs invoke date folders", async () => { + const invokeDataDir = fs.mkdtempSync(path.join(os.tmpdir(), "aios-data-invoke-test-")); + try { + const artifactRows = []; + const db = { + prepare(sql) { + return { + run: (...args) => { + if (sql.includes("INSERT INTO system_invocation_artifacts")) { + const row = { + id: artifactRows.length + 1, + trace_id: args[0], + direction: args[1], + content_type: args[2], + byte_size: args[3], + sha256: args[4], + storage_bucket: args[5], + storage_object_key: args[6], + preview_json: args[7], + redaction_status: args[8], + created_at: args[9] + }; + artifactRows.push(row); + return { lastInsertRowid: row.id }; + } + return { lastInsertRowid: 1 }; + }, + get: (...args) => { + if (sql.includes("FROM system_invocation_artifacts")) { + return artifactRows.find((row) => row.trace_id === args[0] && row.direction === args[1]) || null; + } + return null; + }, + all: () => [] + }; + } + }; + const service = new SystemService({ + db, + objectStorage: {}, + managementClient: { async call() {} }, + env: { aiosDataDir: invokeDataDir } + }); + const buffer = Buffer.from('{"ok":true}', "utf8"); + + const artifact = await service.persistInvocationArtifact({ + traceId: "trace-1", + direction: "request", + applicationName: "crm-app", + summary: { + buffer, + byteSize: buffer.byteLength, + sha256: "sha", + preview: { ok: true } + }, + createdAt: "2026-07-06T05:04:03.002+08:00" + }); + + expect(artifact.storage_bucket).toBe("aios-data"); + expect(artifact.storage_object_key).toMatch(/^logs\/invoke\/20260706\/crm-app-20260706050403002-\d{6}\.log$/); + const filePath = path.join(invokeDataDir, ...artifact.storage_object_key.split("/")); + expect(fs.existsSync(filePath)).toBeTrue(); + expect(fs.readFileSync(filePath, "utf8")).toBe('{"ok":true}'); + + const downloaded = await service.downloadInvocationArtifact("trace-1", "request"); + expect(downloaded.fileName).toMatch(/^crm-app-20260706050403002-\d{6}\.log$/); + expect(downloaded.buffer.toString("utf8")).toBe('{"ok":true}'); + } finally { + fs.rmSync(invokeDataDir, { recursive: true, force: true }); + } +}); diff --git a/packages/aios-management-web/src/pages/SettingsPage.jsx b/packages/aios-management-web/src/pages/SettingsPage.jsx index 1d74eea..11a7e19 100644 --- a/packages/aios-management-web/src/pages/SettingsPage.jsx +++ b/packages/aios-management-web/src/pages/SettingsPage.jsx @@ -569,9 +569,10 @@ export function SettingsPage({ useEffect(() => { form.setFieldsValue({ ...settings, + huozige_server_ip: envConfig?.huozige_server_ip || "", theme_color: DEFAULT_THEME_COLOR }); - }, [form, settings]); + }, [envConfig, form, settings]); useEffect(() => { networkForm.setFieldsValue({ @@ -667,12 +668,34 @@ export function SettingsPage({ const handleSaveSettings = async (values) => { setSettingsSaving(true); try { + const previousHuozigeServerIp = String(envConfig?.huozige_server_ip || "").trim(); + const nextHuozigeServerIp = String(values.huozige_server_ip || "").trim(); + const settingsValues = { ...values }; + delete settingsValues.huozige_server_ip; + let savedRuntimeConfig = null; await runWithOperationMask("正在保存基础设置,请稍候...", async () => { await onSave({ - ...values, + ...settingsValues, theme_color: DEFAULT_THEME_COLOR }); + if (nextHuozigeServerIp !== previousHuozigeServerIp) { + savedRuntimeConfig = await onSaveNetworkAllowlist({ + huozige_server_ip: nextHuozigeServerIp + }); + } }); + if (savedRuntimeConfig) { + form.setFieldsValue({ + huozige_server_ip: savedRuntimeConfig.huozige_server_ip || "" + }); + networkForm.setFieldsValue({ + agent_internal_network_allowlist: savedRuntimeConfig.agent_internal_network_allowlist || "" + }); + notify?.success("对话应用/业务应用服务器已保存"); + setContainerRestartConfirmTitle("对话应用/业务应用服务器已保存"); + setContainerRestartConfirmContent("活字格服务器地址已保存到 env.json,并已同步调整 IP 白名单:旧地址已移除,新地址已加入。重启容器后生效。点击确定将立即重启整个容器;取消后可以继续使用当前页面。"); + setContainerRestartConfirmOpen(true); + } } catch (error) { notify?.error(`基础设置保存失败:${getErrorMessage(error, "保存失败")}`); } finally { @@ -954,6 +977,32 @@ export function SettingsPage({ > + { + const normalized = String(value || "").trim(); + if ( + !normalized + || ( + !normalized.includes("/") + && !normalized.includes("\\") + && !normalized.includes("?") + && !normalized.includes("#") + ) + ) { + return Promise.resolve(); + } + return Promise.reject(new Error("服务器地址不能包含路径、查询参数或片段")); + } + } + ]} + > + + diff --git a/packages/aios-management-web/src/pages/SystemsPage.jsx b/packages/aios-management-web/src/pages/SystemsPage.jsx index 626c029..7483a77 100644 --- a/packages/aios-management-web/src/pages/SystemsPage.jsx +++ b/packages/aios-management-web/src/pages/SystemsPage.jsx @@ -10,9 +10,9 @@ import { InputNumber, Modal, Row, - Select, Space, Spin, + Switch, Table, Tabs, Tag, @@ -46,10 +46,8 @@ function getSystemStatusLabel(status) { function getInitialValues() { return { - provider: "hzg", - scheme: "https", - port: 443, - status: "active" + https_enabled: false, + port: 80 }; } @@ -121,11 +119,12 @@ export function SystemsPage() { const formData = new FormData(); Object.entries(values).forEach(([key, value]) => { - if (key === "ontology" || value === undefined) { + if (key === "ontology" || key === "https_enabled" || value === undefined) { return; } formData.set(key, String(value)); }); + formData.set("scheme", values.https_enabled ? "https" : "http"); if (ontologyFile) { formData.set("ontology", ontologyFile); @@ -273,7 +272,6 @@ export function SystemsPage() { showSizeChanger: false }} columns={[ - { title: "提供方", dataIndex: "provider" }, { title: "应用名", dataIndex: "application_name" }, { title: "类型", @@ -298,13 +296,9 @@ export function SystemsPage() { onClick={() => { setEditing(row); form.setFieldsValue({ - provider: row.provider, application_name: row.application_name, - description: row.description, - scheme: row.scheme, - host: row.host, - port: row.port, - status: row.status + https_enabled: row.scheme === "https", + port: row.port }); setOpen(true); }} @@ -375,7 +369,6 @@ export function SystemsPage() { showSizeChanger: false }} columns={[ - { title: "提供方", dataIndex: "provider" }, { title: "应用名", dataIndex: "application_name" }, { title: "指令", dataIndex: "command_name" }, { title: "次数", dataIndex: "call_count" }, @@ -472,15 +465,6 @@ export function SystemsPage() { >
- - - - - - - - - - - + -