From f3e54e0a5b8f8ff3a6b66c58623b7bfd229f51ca Mon Sep 17 00:00:00 2001 From: Ning Wei <55093136@qq.com> Date: Tue, 4 Aug 2026 10:44:03 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=BF=83=E8=B7=B3?= =?UTF-8?q?=E7=9A=84=E5=A4=84=E7=90=86=E6=96=B9=E6=B3=95=EF=BC=8C=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E6=B4=BB=E5=AD=97=E6=A0=BC=E5=89=8D=EF=BC=8C=E8=BF=BD?= =?UTF-8?q?=E5=8A=A0=E4=B8=80=E4=BA=9B=E6=95=B0=E6=8D=AE=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E7=9A=84=E9=AA=8C=E8=AF=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/Dockerfile | 2 +- docker/assets/config.json | 6 +- docker/assets/scripts/kernel-startup.sh | 2 + docker/assets/scripts/startup.sh | 5 + .../workspace-templates/default/HEARTBEAT.md | 2 + .../references/invoke-rules.md | 8 ++ .../daedalus-agent-apps-invoke-cli/README.md | 21 ++++ .../src/proxy/hzg-provider-client.ts | 49 +++++++++- .../test/proxy/hzg-provider-client.test.ts | 96 +++++++++++++++++++ .../upgrade/openclaw-upgrade-contract.test.js | 27 ++++++ 10 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 docker/assets/workspace-templates/default/HEARTBEAT.md create mode 100644 packages/daedalus-agent-apps-invoke-cli/test/proxy/hzg-provider-client.test.ts diff --git a/docker/Dockerfile b/docker/Dockerfile index dae4180..00b6269 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -524,7 +524,7 @@ RUN --mount=type=bind,source=docker/assets/workspace-templates/default,target=/t if [[ -d /tmp/default-template ]]; then \ cp -a /tmp/default-template/. "${workspace_template}/"; \ fi; \ - for doc in AGENTS.md USER.md SOUL.md; do \ + for doc in AGENTS.md USER.md SOUL.md HEARTBEAT.md; do \ if [[ -f "${workspace_template}/${doc}" ]]; then \ cp "${workspace_template}/${doc}" "${seed_workspace}/${doc}"; \ fi; \ diff --git a/docker/assets/config.json b/docker/assets/config.json index 404d4f8..3a6c0d3 100644 --- a/docker/assets/config.json +++ b/docker/assets/config.json @@ -2,7 +2,11 @@ "agents": { "defaults": { "workspace": "/var/daedalus-agent/.openclaw/workspaces/default", - "skipBootstrap": true + "skipBootstrap": true, + "heartbeat": { + "isolatedSession": true, + "lightContext": true + } } }, "gateway": { diff --git a/docker/assets/scripts/kernel-startup.sh b/docker/assets/scripts/kernel-startup.sh index e0eb41a..ca88041 100644 --- a/docker/assets/scripts/kernel-startup.sh +++ b/docker/assets/scripts/kernel-startup.sh @@ -2207,6 +2207,8 @@ patch_openclaw_config() { .agents.defaults.workspace = $workspace | .agents.defaults.maxConcurrent = $gatewayMaxConcurrent | .agents.defaults.skipBootstrap = true + | .agents.defaults.heartbeat.isolatedSession = true + | .agents.defaults.heartbeat.lightContext = true | .gateway.mode = "local" | .gateway.auth.mode = "token" | .gateway.auth.token = $gatewayAuthToken diff --git a/docker/assets/scripts/startup.sh b/docker/assets/scripts/startup.sh index d68bb9b..237ea9a 100644 --- a/docker/assets/scripts/startup.sh +++ b/docker/assets/scripts/startup.sh @@ -755,6 +755,11 @@ const shared = { config.agents = config.agents || {}; config.agents.defaults = config.agents.defaults || {}; config.agents.defaults.workspace = path.posix.join(productRoot, ".openclaw/workspaces/default"); +config.agents.defaults.heartbeat = { + ...(config.agents.defaults.heartbeat || {}), + isolatedSession: true, + lightContext: true +}; config.memory = config.memory || {}; config.memory.backend = "qmd"; config.memory.qmd = { diff --git a/docker/assets/workspace-templates/default/HEARTBEAT.md b/docker/assets/workspace-templates/default/HEARTBEAT.md new file mode 100644 index 0000000..0eb4760 --- /dev/null +++ b/docker/assets/workspace-templates/default/HEARTBEAT.md @@ -0,0 +1,2 @@ +# Keep this file empty (or with only comments) to skip heartbeat API calls. +# Add tasks below when you want the agent to check something periodically. diff --git a/extensions/clawhub-skills/daedalus-agent-call-app-service/references/invoke-rules.md b/extensions/clawhub-skills/daedalus-agent-call-app-service/references/invoke-rules.md index 25e1032..0828675 100644 --- a/extensions/clawhub-skills/daedalus-agent-call-app-service/references/invoke-rules.md +++ b/extensions/clawhub-skills/daedalus-agent-call-app-service/references/invoke-rules.md @@ -121,6 +121,13 @@ daedalus-agent-apps-invoke-cli status - `target-page` - `page-limit-row-count` - `columns` 保持 ontology 文档中的原顺序,不要重排 +- 执行 CLI 前必须完成结构预检: + - `jsonBody` 必须是对象 + - `columns` 必须是非空数组;即使只有一列也不能写成单个对象 + - 每个 `columns` 元素必须是对象,且 `column-name`、`guid` 都是非空字符串 + - `table-name`、`view-name`、`list-view-location`、`page-name` 都是非空字符串 + - `target-page`、`page-limit-row-count` 都是数字 +- 任一预检失败时不要执行 CLI,明确指出缺失或类型错误的字段 ### TableBinding 到底层 HTTP body 的映射说明 @@ -142,6 +149,7 @@ daedalus-agent-apps-invoke-cli status - `orderByInfo=null` - `columnFilterQueries=null` - `totalRowBindingInfos=[]` +- 不要把包含 `bindingInfos`、`currentRowInfo` 或 `offsetConditionInfo` 的底层 HTTP body 作为 CLI 的 `jsonBody`;CLI 只接受上面的兼容 schema ### CandidatesBinding 兼容 schema diff --git a/packages/daedalus-agent-apps-invoke-cli/README.md b/packages/daedalus-agent-apps-invoke-cli/README.md index bcebf9e..372ebe3 100644 --- a/packages/daedalus-agent-apps-invoke-cli/README.md +++ b/packages/daedalus-agent-apps-invoke-cli/README.md @@ -91,6 +91,27 @@ daedalus-agent-apps-invoke-cli binding OASystem GetTableDataWithOffset POST --bo CLI 会把 `TableBinding`、`CandidatesBinding`、`DataSourceBinding` 等别名规范化为上述命令。`GetMetadata` 和 `GetMetadata2` 只作为内部辅助能力存在,service 会拒绝直接调用。 +`GetTableDataWithOffset` 的请求体必须使用 SDK 兼容结构,其中 `columns` 即使只有一列也必须是非空数组: + +```json +{ + "columns": [ + { + "column-name": "编号", + "guid": "3261feec-650a-421c-b05f-7ea8b1bed3e5" + } + ], + "table-name": "物品表", + "view-name": "物品_库存物品信息表格", + "list-view-location": "物品_库存|物品信息表格", + "page-name": "物品_库存", + "target-page": 1, + "page-limit-row-count": 0 +} +``` + +service 会在调用 SDK 前验证该结构;缺少 `columns`、传入非数组或列项缺少 `column-name` / `guid` 时会返回明确的 400 错误。 + ## Socket 协议 Client 会向内部固定 socket `/var/daedalus-agent/run/app-invoke.sock` 写入一行 JSON 请求,service 返回一行 JSON 响应。 diff --git a/packages/daedalus-agent-apps-invoke-cli/src/proxy/hzg-provider-client.ts b/packages/daedalus-agent-apps-invoke-cli/src/proxy/hzg-provider-client.ts index cd62d0c..9276f01 100644 --- a/packages/daedalus-agent-apps-invoke-cli/src/proxy/hzg-provider-client.ts +++ b/packages/daedalus-agent-apps-invoke-cli/src/proxy/hzg-provider-client.ts @@ -93,6 +93,52 @@ function readRequiredString(record: JsonObject, key: string, label: string): str return value.trim(); } +function readRequiredNumber(record: JsonObject, key: string, label: string): number { + const value = record[key]; + if (typeof value !== "number" || !Number.isFinite(value)) { + throw badRequest(`${label} must be a number`); + } + + return value; +} + +export function normalizeTableBindingBody(body: unknown): JsonObject { + if (!isJsonObject(body)) { + throw badRequest("GetTableDataWithOffset body must be a JSON object"); + } + + const normalized: JsonObject = { ...body }; + const columns = normalized.columns; + if (!Array.isArray(columns) || columns.length === 0) { + throw badRequest("GetTableDataWithOffset body.columns must be a non-empty array"); + } + + normalized.columns = columns.map((column, index) => { + if (!isJsonObject(column)) { + throw badRequest(`GetTableDataWithOffset body.columns[${index}] must be a JSON object`); + } + + return { + ...column, + "column-name": readRequiredString(column, "column-name", `GetTableDataWithOffset body.columns[${index}].column-name`), + guid: readRequiredString(column, "guid", `GetTableDataWithOffset body.columns[${index}].guid`) + }; + }); + + normalized["table-name"] = readRequiredString(normalized, "table-name", "GetTableDataWithOffset body.table-name"); + normalized["view-name"] = readRequiredString(normalized, "view-name", "GetTableDataWithOffset body.view-name"); + normalized["list-view-location"] = readRequiredString(normalized, "list-view-location", "GetTableDataWithOffset body.list-view-location"); + normalized["page-name"] = readRequiredString(normalized, "page-name", "GetTableDataWithOffset body.page-name"); + normalized["target-page"] = readRequiredNumber(normalized, "target-page", "GetTableDataWithOffset body.target-page"); + normalized["page-limit-row-count"] = readRequiredNumber( + normalized, + "page-limit-row-count", + "GetTableDataWithOffset body.page-limit-row-count" + ); + + return normalized; +} + function normalizeCalcBindingDataSourceBody(body: unknown): JsonObject { if (!isJsonObject(body)) { throw badRequest("CalcBindingDataSource body must be a JSON object"); @@ -217,8 +263,9 @@ export class HzgProviderClient { } if (command === "binding" && commandName === "GetTableDataWithOffset") { + const tableBindingBody = normalizeTableBindingBody(body); const result = await invokeWithCallback((callback) => - callGetTableDataWithOffsetWithCookie(system.service_endpoint, body, cookie, callback) + callGetTableDataWithOffsetWithCookie(system.service_endpoint, tableBindingBody, cookie, callback) ); return this.normalizeResult(result, { applicationName, commandName }); } diff --git a/packages/daedalus-agent-apps-invoke-cli/test/proxy/hzg-provider-client.test.ts b/packages/daedalus-agent-apps-invoke-cli/test/proxy/hzg-provider-client.test.ts new file mode 100644 index 0000000..3eb9aa2 --- /dev/null +++ b/packages/daedalus-agent-apps-invoke-cli/test/proxy/hzg-provider-client.test.ts @@ -0,0 +1,96 @@ +import assert from "node:assert/strict"; + +import { ServiceError } from "../../src/proxy/errors.js"; +import { HzgProviderClient, normalizeTableBindingBody } from "../../src/proxy/hzg-provider-client.js"; +import type { BusinessSystemRecord } from "../../src/proxy/types.js"; + +function assertBadRequest(action: () => unknown, expectedMessage: string): void { + assert.throws(action, (error: unknown) => { + assert.ok(error instanceof ServiceError); + assert.equal(error.statusCode, 400); + assert.equal(error.message, expectedMessage); + return true; + }); +} + +test("table binding rejects a missing columns array before the SDK uses map", () => { + assertBadRequest( + () => normalizeTableBindingBody({ "table-name": "物品表" }), + "GetTableDataWithOffset body.columns must be a non-empty array" + ); +}); + +test("table binding rejects columns that are not an array", () => { + assertBadRequest( + () => normalizeTableBindingBody({ columns: { "column-name": "编号", guid: "column-1" } }), + "GetTableDataWithOffset body.columns must be a non-empty array" + ); +}); + +test("table binding rejects a column without a guid", () => { + assertBadRequest( + () => normalizeTableBindingBody({ columns: [{ "column-name": "编号" }] }), + "GetTableDataWithOffset body.columns[0].guid is required" + ); +}); + +test("table binding normalizes the complete SDK-compatible schema", () => { + const body = { + columns: [{ "column-name": " 编号 ", guid: " column-1 " }], + "table-name": " 物品表 ", + "view-name": " 物品列表 ", + "list-view-location": " 物品页|物品列表 ", + "page-name": " 物品页 ", + "target-page": 1, + "page-limit-row-count": 0 + }; + + expect(normalizeTableBindingBody(body)).toEqual({ + columns: [{ "column-name": "编号", guid: "column-1" }], + "table-name": "物品表", + "view-name": "物品列表", + "list-view-location": "物品页|物品列表", + "page-name": "物品页", + "target-page": 1, + "page-limit-row-count": 0 + }); + expect(body.columns[0]["column-name"]).toBe(" 编号 "); +}); + +test("provider rejects an invalid table binding before invoking the SDK", async () => { + const client = new HzgProviderClient({ + webApiClient: { + async getExternalCookie() { + return { cookie: "ForguncyServer=test" }; + } + } + }); + const system: BusinessSystemRecord = { + system_id: "system-1", + provider: "hzg", + application_name: "OASystem", + service_endpoint: "https://example.com/app", + session_endpoint: "https://example.com/session", + status: "active" + }; + + await assert.rejects( + () => client.execute(system, { + traceId: "trace-1", + command: "binding", + sessionId: "session-1", + parameters: { + applicationName: "OASystem", + commandName: "GetTableDataWithOffset", + method: "POST", + body: { "table-name": "物品表" } + } + }), + (error: unknown) => { + assert.ok(error instanceof ServiceError); + assert.equal(error.statusCode, 400); + assert.equal(error.message, "GetTableDataWithOffset body.columns must be a non-empty array"); + return true; + } + ); +}); diff --git a/test/case/upgrade/openclaw-upgrade-contract.test.js b/test/case/upgrade/openclaw-upgrade-contract.test.js index 0cb2096..dc644d0 100644 --- a/test/case/upgrade/openclaw-upgrade-contract.test.js +++ b/test/case/upgrade/openclaw-upgrade-contract.test.js @@ -20,6 +20,33 @@ test("Docker image pins the supported OpenClaw and Node versions", async () => { assert.match(dockerfile, /^FROM npm-builder-base AS web-installer$/m); }); +test("default workspace skips idle heartbeat model calls", async () => { + const dockerfile = await readRepoFile("docker/Dockerfile"); + const config = JSON.parse(await readRepoFile("docker/assets/config.json")); + const startup = await readRepoFile("docker/assets/scripts/startup.sh"); + const kernelStartup = await readRepoFile("docker/assets/scripts/kernel-startup.sh"); + const heartbeat = await readRepoFile( + "docker/assets/workspace-templates/default/HEARTBEAT.md" + ); + + assert.equal( + heartbeat, + [ + "# Keep this file empty (or with only comments) to skip heartbeat API calls.", + "# Add tasks below when you want the agent to check something periodically.", + "" + ].join("\n") + ); + assert.match(dockerfile, /for doc in AGENTS\.md USER\.md SOUL\.md HEARTBEAT\.md;/); + assert.deepEqual(config.agents.defaults.heartbeat, { + isolatedSession: true, + lightContext: true + }); + assert.match(startup, /isolatedSession: true,\n lightContext: true/); + assert.match(kernelStartup, /\.agents\.defaults\.heartbeat\.isolatedSession = true/); + assert.match(kernelStartup, /\.agents\.defaults\.heartbeat\.lightContext = true/); +}); + test("Docker npm installs serialize lifecycle scripts and retry transient failures", async () => { const dockerfile = await readRepoFile("docker/Dockerfile"); const retryScript = await readRepoFile("docker/internal/kernel/build-scripts/npm-ci-with-retry.sh"); -- Gitee From b42da69301865547333549c483f48c1d5a3ad1a4 Mon Sep 17 00:00:00 2001 From: Ning Wei <55093136@qq.com> Date: Tue, 4 Aug 2026 11:48:51 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=B5=8B=E8=AF=95?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=E7=9A=84=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- test/case/integration/file-output-content.js | 3 +++ test/case/integration/file-output-content.test.js | 15 +++++++++++++++ test/case/integration/playwright-flow.js | 8 +++++++- test/package.json | 1 + 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 test/case/integration/file-output-content.js create mode 100644 test/case/integration/file-output-content.test.js diff --git a/test/case/integration/file-output-content.js b/test/case/integration/file-output-content.js new file mode 100644 index 0000000..7f95a59 --- /dev/null +++ b/test/case/integration/file-output-content.js @@ -0,0 +1,3 @@ +export function normalizeFileOutputContent(content) { + return Buffer.from(content).toString("utf8").trim(); +} diff --git a/test/case/integration/file-output-content.test.js b/test/case/integration/file-output-content.test.js new file mode 100644 index 0000000..1354a9a --- /dev/null +++ b/test/case/integration/file-output-content.test.js @@ -0,0 +1,15 @@ +import assert from "node:assert/strict"; +import test from "node:test"; + +import { normalizeFileOutputContent } from "./file-output-content.js"; + +test("normalizes file output before strict content comparison", () => { + for (const content of [ + "ping", + "ping\n", + "ping\r\n", + " ping \n" + ]) { + assert.equal(normalizeFileOutputContent(Buffer.from(content, "utf8")), "ping"); + } +}); diff --git a/test/case/integration/playwright-flow.js b/test/case/integration/playwright-flow.js index 2ff6c54..724ca26 100644 --- a/test/case/integration/playwright-flow.js +++ b/test/case/integration/playwright-flow.js @@ -7,6 +7,8 @@ import { randomBytes } from "node:crypto"; import { createRequire } from "node:module"; import { fileURLToPath } from "node:url"; +import { normalizeFileOutputContent } from "./file-output-content.js"; + const localRequire = createRequire(import.meta.url); const managementRequire = createRequire(new URL("../../../packages/daedalus-agent-management-web/package.json", import.meta.url)); @@ -1977,7 +1979,11 @@ async function verifyAgentFileOutputObject(targets, uri, fileName, envJsonPath) assertFileOutputKeyMatches(parsed.key, fileName); const content = await readMinioObjectFromContainer(container, parsed.bucket, parsed.key); - assert.deepEqual(content, Buffer.from("ping", "utf8"), `MinIO object content should be exactly ping: ${parsed.key}`); + assert.equal( + normalizeFileOutputContent(content), + "ping", + `MinIO object content should equal ping after trimming: ${parsed.key}` + ); return { container, diff --git a/test/package.json b/test/package.json index 63d4175..736d7fc 100644 --- a/test/package.json +++ b/test/package.json @@ -5,6 +5,7 @@ "scripts": { "test:brand": "node --test ./case/branding/daedalus-brand-contract.test.js", "test:upgrade-contract": "node --test ./case/upgrade/openclaw-upgrade-contract.test.js", + "test:integration:contract": "node --test ./case/integration/file-output-content.test.js", "case:integration": "node ./case/integration/playwright-flow.js", "case:stress": "node ./case/stress/stress-flow.js", "test:flow": "node ./suite/integration/run-integration-suite.js full", -- Gitee From bfc67fe73b8ad13ce1564c71046d7406cf88b7b9 Mon Sep 17 00:00:00 2001 From: Ning Wei <55093136@qq.com> Date: Tue, 4 Aug 2026 17:34:22 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8F=8F=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 5a1ac50..190da84 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ docker volume create daedalus-agent-data docker run -d --restart unless-stopped --name daedalus-agent --cap-add NET_ADMIN -p 3030-3050:3030-3050 -p 1883:1883 -p 9000:9000 -v daedalus-agent-data:/var/daedalus-agent crpi-4auaoyyj6r36p6lb.cn-hangzhou.personal.cr.aliyuncs.com/huozige_lab/daedalus-agent-x64:[版本号] ``` -`[版本号]` 请参考项目 Release,例如 `20260702.01`。运行数据保存在 Docker volume `daedalus-agent-data` 中;如需隔离多套环境,请替换为新的 volume 名称。详细说明见 [docker/README.md](./docker/README.md)。 +`[版本号]` 请参考项目 Release,例如 `20260804.01`。运行数据保存在 Docker volume `daedalus-agent-data` 中;如需隔离多套环境,请替换为新的 volume 名称。详细说明见 [docker/README.md](./docker/README.md)。 ## 步骤2:登录管理控制台 @@ -77,7 +77,7 @@ Ontology zip 需通过 `huozige-ontology-builder` 构建,[点击阅读使用 - 每日 Token 限额 - 授权用户 -创建完成后,点击 `授权用户` 按钮,将改数字员工分配和活字格管理控制台上的用户。此处需要输入的是用户名,而不是用户全名。 +创建完成后,点击 `管理用户` 按钮,将该数字员工分配和活字格管理控制台上的用户。此处需要输入的是用户名,而不是用户全名。 ## 步骤6:对接对话应用 @@ -156,4 +156,4 @@ MQTT/MinIO 密码可在 `系统设置` 页面中 `基础设置` 选项卡获取 1. 在管理控制台的【系统设置】页面中【系统日志下载】选项卡,下载当前日期的 `交互日志`,检查是否存在 `"direction":"IN"` 的入站日志和 `"direction":"OUT"` 的出站日志。 2. 如果没有进站日志,则检查对话应用全局配置中的 `DA_MQTT_CHANNEL_USERNAME` 和 `DA_MQTT_CHANNEL_PASSWORD`;如果有入站日志,但没有出站日志,则需要在管理控制台的【数字员工】页面中,点击该员工,通过右上角的【对话测试】,检查大语言模型配置是否正确。 -3. 如果存在进出站日志,请检查对话应用全局配置中的 `MQTT_TOPIC_FILTER`。 \ No newline at end of file +3. 如果存在进出站日志,请检查对话应用全局配置中的 `MQTT_TOPIC_FILTER`。 -- Gitee