mirror of
https://github.com/TianyiDataScience/openclaw-control-center.git
synced 2026-08-14 08:52:27 +00:00
fix: improve Docker hall assets and workspace guidance
Fixes #89 Refs #90 Refs #91 Refs #92 Refs #88
This commit is contained in:
+3
-3
@@ -4,9 +4,10 @@ WORKDIR /app
|
||||
|
||||
COPY package.json package-lock.json tsconfig.json ./
|
||||
COPY src ./src
|
||||
COPY scripts ./scripts
|
||||
|
||||
RUN npm ci
|
||||
RUN npm run build
|
||||
RUN npm run build && npm run avatars:export
|
||||
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
@@ -19,14 +20,13 @@ ENV UI_BIND_ADDRESS=0.0.0.0
|
||||
|
||||
COPY --from=build /app/package.json ./package.json
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY --from=build /app/runtime ./runtime
|
||||
COPY .env.example ./.env.example
|
||||
COPY README.md ./README.md
|
||||
COPY README.zh-CN.md ./README.zh-CN.md
|
||||
COPY HALL.md ./HALL.md
|
||||
COPY docs ./docs
|
||||
|
||||
RUN mkdir -p runtime
|
||||
|
||||
EXPOSE 4310
|
||||
|
||||
CMD ["node", "dist/index.js"]
|
||||
|
||||
@@ -218,6 +218,7 @@ Notes:
|
||||
- `cp .env.example .env`
|
||||
- edit `docker-compose.example.yml` mount paths and `LOCAL_API_TOKEN`
|
||||
- `docker compose -f docker-compose.example.yml up --build`
|
||||
- The Docker image now pre-generates the default Hall / staff avatar exports during build, so `/hall-avatars/*.png` is available out of the box.
|
||||
- If the Gateway runs on the Docker host:
|
||||
- keep `extra_hosts: ["host.docker.internal:host-gateway"]` on Linux Docker
|
||||
- prefer `ws://host.docker.internal:18789` for direct container-to-host traffic
|
||||
@@ -226,6 +227,11 @@ Notes:
|
||||
- `OPENCLAW_CONTROL_UI_URL`
|
||||
- `UI_BIND_ADDRESS=0.0.0.0`
|
||||
|
||||
## Multi-agent workspace layouts
|
||||
- The default fallback layout is `<OPENCLAW_WORKSPACE_ROOT>/agents/<agentId>`.
|
||||
- If your agents actually live in sibling folders such as `workspace/a` and `workspace/b`, define each agent's `workspace` explicitly in `openclaw.json`.
|
||||
- In other words, `OPENCLAW_WORKSPACE_ROOT` gives the shared parent root; custom per-agent directories still need per-agent `workspace` entries.
|
||||
|
||||
## Installation and onboarding
|
||||
|
||||
### 1. Before you start
|
||||
|
||||
@@ -210,6 +210,7 @@ npm run dev:ui
|
||||
- `cp .env.example .env`
|
||||
- 按实际路径修改 `docker-compose.example.yml` 里的挂载和 `LOCAL_API_TOKEN`
|
||||
- `docker compose -f docker-compose.example.yml up --build`
|
||||
- Docker 镜像现在会在构建阶段预生成默认 Hall / 员工头像导出,所以 `/hall-avatars/*.png` 开箱即用。
|
||||
- 如果 Gateway 跑在宿主机上:
|
||||
- Linux Docker 建议保留 `extra_hosts: ["host.docker.internal:host-gateway"]`
|
||||
- 容器直连宿主机时,优先用 `ws://host.docker.internal:18789`
|
||||
@@ -218,6 +219,11 @@ npm run dev:ui
|
||||
- `OPENCLAW_CONTROL_UI_URL`
|
||||
- `UI_BIND_ADDRESS=0.0.0.0`
|
||||
|
||||
## 多 Agent workspace 目录布局
|
||||
- 默认回退布局是 `<OPENCLAW_WORKSPACE_ROOT>/agents/<agentId>`。
|
||||
- 如果你的 agent 实际目录是 `workspace/a`、`workspace/b` 这种同级自定义目录,就需要在 `openclaw.json` 里给每个 agent 明确写 `workspace`。
|
||||
- 也就是说,`OPENCLAW_WORKSPACE_ROOT` 只提供共享根目录;每个 agent 的自定义目录仍然要靠各自的 `workspace` 字段来告诉控制中心。
|
||||
|
||||
## 安装与上手
|
||||
|
||||
### 1. 开始前准备
|
||||
|
||||
+74
-5
@@ -12,7 +12,7 @@
|
||||
|
||||
**常见原因:** 控制中心会参考 OpenClaw 的 workspace 目录和运行时信号。是否能看到完整职责信息,通常取决于:
|
||||
- OpenClaw Gateway 返回的 session 数据中是否包含 agent 元信息
|
||||
- `OPENCLAW_AGENT_ROOT` 环境变量是否指向了正确的 workspace 目录
|
||||
- `OPENCLAW_WORKSPACE_ROOT` 和 `openclaw.json` 里的 agent `workspace` 配置是否能让控制中心找到每个 agent 的真实工作目录
|
||||
|
||||
**解决方法:**
|
||||
|
||||
@@ -25,12 +25,35 @@
|
||||
└── MEMORY.md # 长期记忆
|
||||
```
|
||||
|
||||
2. **检查环境变量:**
|
||||
2. **检查多 Agent 工作区定位方式:**
|
||||
```bash
|
||||
# 确保 OPENCLAW_AGENT_ROOT 指向包含所有 workspace 的父目录
|
||||
export OPENCLAW_AGENT_ROOT=/path/to/.openclaw
|
||||
# 标准布局:workspace/agents/<agentId>
|
||||
export OPENCLAW_WORKSPACE_ROOT=/path/to/.openclaw/workspace
|
||||
```
|
||||
|
||||
如果你的目录不是默认的 `workspace/agents/<agentId>`,而是像:
|
||||
```text
|
||||
/path/to/.openclaw/workspace/a
|
||||
/path/to/.openclaw/workspace/b
|
||||
```
|
||||
那就不要只设置根目录;还需要在 `openclaw.json` 里给每个 agent 明确写出 `workspace`:
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"list": [
|
||||
{ "id": "main", "workspace": "/path/to/.openclaw/workspace" },
|
||||
{ "id": "a", "workspace": "/path/to/.openclaw/workspace/a" },
|
||||
{ "id": "b", "workspace": "/path/to/.openclaw/workspace/b" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**控制中心当前的规则是:**
|
||||
- 优先使用 `openclaw.json` 里每个 agent 自己的 `workspace`
|
||||
- 如果没写,才回退到 `<OPENCLAW_WORKSPACE_ROOT>/agents/<agentId>`
|
||||
- 所以像 `workspace/a`、`workspace/b` 这种自定义布局,如果不写 agent 级 `workspace`,控制中心是没法凭空猜到的
|
||||
|
||||
3. **确认 Gateway 正在运行:**
|
||||
```bash
|
||||
openclaw gateway status
|
||||
@@ -48,7 +71,45 @@
|
||||
|
||||
---
|
||||
|
||||
### 1.1 协作大厅 / 任务房间 — 现在可以直接上传附件吗?
|
||||
### 1.1 多 Agent workspace 目录怎么识别?
|
||||
|
||||
**直接回答:** 控制中心支持两种常见方式:
|
||||
- 默认布局:`<workspaceRoot>/agents/<agentId>`
|
||||
- 自定义布局:在 `openclaw.json` 里给每个 agent 明确写 `workspace`
|
||||
|
||||
**如果你的目录长这样:**
|
||||
```text
|
||||
workspace/
|
||||
├── a/
|
||||
├── b/
|
||||
└── shared-files...
|
||||
```
|
||||
|
||||
那就要显式配置:
|
||||
```json
|
||||
{
|
||||
"agents": {
|
||||
"list": [
|
||||
{ "id": "main", "workspace": "/abs/path/workspace" },
|
||||
{ "id": "a", "workspace": "/abs/path/workspace/a" },
|
||||
{ "id": "b", "workspace": "/abs/path/workspace/b" }
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**不要只依赖:**
|
||||
- `OPENCLAW_WORKSPACE_ROOT=/abs/path/workspace`
|
||||
|
||||
因为如果 agent 自己没有单独的 `workspace` 字段,控制中心会默认回退到:
|
||||
```text
|
||||
/abs/path/workspace/agents/a
|
||||
/abs/path/workspace/agents/b
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.2 协作大厅 / 任务房间 — 现在可以直接上传附件吗?
|
||||
|
||||
**直接回答:** 目前还**不支持**在 hall / task room 里直接上传本地文件作为消息附件。
|
||||
|
||||
@@ -177,6 +238,14 @@ openclaw sessions history <session-key> --limit 5
|
||||
|
||||
> **经验判断:** 如果你现在用的是 `wss://172.x.x.x:18789/...` 这类裸 IP 地址,连不上时先不要怀疑控制中心本身,优先检查 TLS / 证书匹配和 Docker 主机映射。
|
||||
|
||||
**快速自检:**
|
||||
```bash
|
||||
docker compose exec control-center getent hosts host.docker.internal
|
||||
docker compose exec control-center printenv GATEWAY_URL
|
||||
```
|
||||
|
||||
如果第一条没有解析出宿主机地址,或者第二条还是 `wss://172.x.x.x:18789/...` 这类裸 IP TLS 地址,优先先改配置,再看控制中心诊断页。
|
||||
|
||||
---
|
||||
|
||||
## English Version
|
||||
|
||||
+11
-3
@@ -276,6 +276,7 @@ const AGENT_DOCUMENT_FILE_CANDIDATES = [
|
||||
] as const;
|
||||
const STAFF_ROLE_EVIDENCE_FILE_CANDIDATES = [
|
||||
"IDENTITY.md",
|
||||
"SOUL.md",
|
||||
"AGENTS.md",
|
||||
"README.md",
|
||||
"MEMORY.md",
|
||||
@@ -12076,9 +12077,16 @@ function extractLabeledField(input: string, labels: string[]): string | undefine
|
||||
const lower = line.toLowerCase();
|
||||
for (const label of labels) {
|
||||
const prefix = `${label.toLowerCase()}:`;
|
||||
if (!lower.startsWith(prefix)) continue;
|
||||
const value = line.slice(prefix.length).trim();
|
||||
if (value) return value;
|
||||
if (lower.startsWith(prefix)) {
|
||||
const value = line.slice(prefix.length).trim();
|
||||
if (value) return value;
|
||||
continue;
|
||||
}
|
||||
const headingPrefix = `## ${label.toLowerCase()} `;
|
||||
if (lower.startsWith(headingPrefix)) {
|
||||
const value = line.slice(headingPrefix.length).trim();
|
||||
if (value) return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { ReadonlyToolClient } from "../src/clients/tool-client";
|
||||
import { startUiServer } from "../src/ui/server";
|
||||
|
||||
test("editable file API returns 404 instead of trying to read a directory path", async () => {
|
||||
const server = startUiServer(0, new ReadonlyToolClient());
|
||||
try {
|
||||
if (!server.listening) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
server.once("listening", resolve);
|
||||
server.once("error", reject);
|
||||
});
|
||||
}
|
||||
const address = server.address();
|
||||
if (!address || typeof address === "string") throw new Error("Failed to bind ephemeral UI port.");
|
||||
const baseUrl = `http://127.0.0.1:${address.port}`;
|
||||
const directoryPath = process.cwd();
|
||||
|
||||
const response = await fetch(
|
||||
`${baseUrl}/api/files/content?scope=workspace&path=${encodeURIComponent(directoryPath)}`,
|
||||
);
|
||||
assert.equal(response.status, 404);
|
||||
const payload = await response.json() as {
|
||||
error?: { code?: string; message?: string };
|
||||
};
|
||||
assert.equal(payload.error?.code, "NOT_FOUND");
|
||||
assert.match(payload.error?.message ?? "", /Editable file not found/i);
|
||||
} finally {
|
||||
if (server.listening) {
|
||||
await new Promise<void>((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -74,3 +74,11 @@ test("sprite png export writes a valid PNG signature", () => {
|
||||
|
||||
assert.equal(png.subarray(0, 8).toString("hex"), "89504e470d0a1a0a");
|
||||
});
|
||||
|
||||
test("Dockerfile bakes exported hall avatars into the image", async () => {
|
||||
const dockerfile = await import("node:fs/promises").then((fs) => fs.readFile("Dockerfile", "utf8"));
|
||||
|
||||
assert.match(dockerfile, /COPY scripts \.\/scripts/);
|
||||
assert.match(dockerfile, /RUN npm run build && npm run avatars:export/);
|
||||
assert.match(dockerfile, /COPY --from=build \/app\/runtime \.\/runtime/);
|
||||
});
|
||||
|
||||
@@ -91,3 +91,35 @@ test("agent workspace root follows main root, configured workspace, then agents
|
||||
"/srv/openclaw/workspace/agents/otter",
|
||||
);
|
||||
});
|
||||
|
||||
test("agent workspace root honors sibling custom workspace directories from config", () => {
|
||||
const configText = JSON.stringify({
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main", workspace: "/srv/openclaw/workspace" },
|
||||
{ id: "a", workspace: "/srv/openclaw/workspace/a" },
|
||||
{ id: "b", workspace: "/srv/openclaw/workspace/b" },
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
resolveOpenClawAgentWorkspaceRoot({
|
||||
agentId: "a",
|
||||
openclawHomeDir: "/tmp/openclaw-home/.openclaw",
|
||||
configPath: "/tmp/openclaw-home/.openclaw/openclaw.json",
|
||||
configText,
|
||||
}),
|
||||
"/srv/openclaw/workspace/a",
|
||||
);
|
||||
|
||||
assert.equal(
|
||||
resolveOpenClawAgentWorkspaceRoot({
|
||||
agentId: "b",
|
||||
openclawHomeDir: "/tmp/openclaw-home/.openclaw",
|
||||
configPath: "/tmp/openclaw-home/.openclaw/openclaw.json",
|
||||
configText,
|
||||
}),
|
||||
"/srv/openclaw/workspace/b",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -527,8 +527,10 @@ test("memory and workspace sections expose editable file workbenches", async ()
|
||||
assert(!source.includes('const agentProfileFiles = ["MEMORY.md", "USER.md", "SOUL.md", "IDENTITY.md"];'));
|
||||
assert(source.includes('const SHARED_DOCUMENT_FILE_CANDIDATES = ['));
|
||||
assert(source.includes('const AGENT_DOCUMENT_FILE_CANDIDATES = ['));
|
||||
assert(source.includes('const STAFF_ROLE_EVIDENCE_FILE_CANDIDATES = ['));
|
||||
assert(source.includes('"IDENTITY.md"'));
|
||||
assert(source.includes('"SOUL.md"'));
|
||||
assert(source.includes('const headingPrefix = `## ${label.toLowerCase()} `;'));
|
||||
assert(source.includes('"USER.md"'));
|
||||
assert(source.includes('"TASKS.md"'));
|
||||
assert(source.includes('"BOOTSTRAP.md"'));
|
||||
|
||||
Reference in New Issue
Block a user