mirror of
https://github.com/TianyiDataScience/openclaw-control-center.git
synced 2026-08-14 00:47:58 +00:00
Fix standalone workspace discovery and expose USER/TASKS docs
This commit is contained in:
@@ -3,6 +3,9 @@
|
||||
GATEWAY_URL=ws://127.0.0.1:18789
|
||||
# Optional path overrides when your OpenClaw/Codex data is not in the default home locations.
|
||||
# OPENCLAW_HOME=/path/to/.openclaw
|
||||
# OPENCLAW_CONFIG_PATH=/path/to/openclaw.json
|
||||
# OPENCLAW_WORKSPACE_ROOT=/path/to/workspace
|
||||
# OPENCLAW_AGENT_ROOT=/path/to/one/agent/workspace
|
||||
# CODEX_HOME=/path/to/.codex
|
||||
# OPENCLAW_SUBSCRIPTION_SNAPSHOT_PATH=/path/to/subscription.json
|
||||
READONLY_MODE=true
|
||||
|
||||
@@ -67,6 +67,9 @@ Phase 3: apply safe first-run settings
|
||||
15. Only change these when the machine actually requires it:
|
||||
- GATEWAY_URL
|
||||
- OPENCLAW_HOME
|
||||
- OPENCLAW_CONFIG_PATH
|
||||
- OPENCLAW_WORKSPACE_ROOT
|
||||
- OPENCLAW_AGENT_ROOT
|
||||
- CODEX_HOME
|
||||
- OPENCLAW_SUBSCRIPTION_SNAPSHOT_PATH
|
||||
- UI_PORT
|
||||
|
||||
@@ -67,6 +67,9 @@
|
||||
15. 只有在本机环境确实不同的时候,才修改:
|
||||
- GATEWAY_URL
|
||||
- OPENCLAW_HOME
|
||||
- OPENCLAW_CONFIG_PATH
|
||||
- OPENCLAW_WORKSPACE_ROOT
|
||||
- OPENCLAW_AGENT_ROOT
|
||||
- CODEX_HOME
|
||||
- OPENCLAW_SUBSCRIPTION_SNAPSHOT_PATH
|
||||
- UI_PORT
|
||||
|
||||
+7
-1
@@ -314,6 +314,9 @@ UI_PORT=4310
|
||||
|
||||
# Optional only when your paths differ from the defaults:
|
||||
# OPENCLAW_HOME=/path/to/.openclaw
|
||||
# OPENCLAW_CONFIG_PATH=/path/to/openclaw.json
|
||||
# OPENCLAW_WORKSPACE_ROOT=/path/to/workspace
|
||||
# OPENCLAW_AGENT_ROOT=/path/to/one/agent/workspace
|
||||
# CODEX_HOME=/path/to/.codex
|
||||
# OPENCLAW_SUBSCRIPTION_SNAPSHOT_PATH=/path/to/subscription.json
|
||||
```
|
||||
@@ -321,6 +324,9 @@ UI_PORT=4310
|
||||
Change only these values if your environment needs it:
|
||||
- `GATEWAY_URL`: when your OpenClaw Gateway is not on the default local socket
|
||||
- `OPENCLAW_HOME`: when OpenClaw is not stored in `~/.openclaw`
|
||||
- `OPENCLAW_CONFIG_PATH`: when `openclaw.json` lives somewhere custom
|
||||
- `OPENCLAW_WORKSPACE_ROOT`: when control-center is installed outside the OpenClaw workspace tree and you want to pin the true workspace root
|
||||
- `OPENCLAW_AGENT_ROOT`: when control-center is not inside an agent workspace but you still want legacy agent-local memory/mission reads to point at one specific agent workspace
|
||||
- `CODEX_HOME`: when Codex data is not stored in `~/.codex`
|
||||
- `OPENCLAW_SUBSCRIPTION_SNAPSHOT_PATH`: when your billing/subscription snapshot lives somewhere custom
|
||||
- `UI_PORT`: when `4310` is already in use
|
||||
@@ -359,7 +365,7 @@ On your first launch, check these pages in order:
|
||||
|
||||
### 8. If something looks wrong
|
||||
- Empty live activity usually means `GATEWAY_URL` is wrong or the OpenClaw Gateway is not running.
|
||||
- Missing `Documents / Memory` agents usually means `OPENCLAW_HOME` points to the wrong OpenClaw root or `openclaw.json` is missing.
|
||||
- Missing `Documents / Memory` agents usually means `OPENCLAW_HOME`, `OPENCLAW_CONFIG_PATH`, or `OPENCLAW_WORKSPACE_ROOT` points to the wrong place, or `openclaw.json` is missing.
|
||||
- Missing usage/subscription data usually means `CODEX_HOME` or `OPENCLAW_SUBSCRIPTION_SNAPSHOT_PATH` needs to be set.
|
||||
- If you only want a safe read-only dashboard, do not change the mutation defaults.
|
||||
|
||||
|
||||
@@ -300,6 +300,9 @@ UI_PORT=4310
|
||||
|
||||
# 只有路径不是默认值时才需要设置:
|
||||
# OPENCLAW_HOME=/path/to/.openclaw
|
||||
# OPENCLAW_CONFIG_PATH=/path/to/openclaw.json
|
||||
# OPENCLAW_WORKSPACE_ROOT=/path/to/workspace
|
||||
# OPENCLAW_AGENT_ROOT=/path/to/one/agent/workspace
|
||||
# CODEX_HOME=/path/to/.codex
|
||||
# OPENCLAW_SUBSCRIPTION_SNAPSHOT_PATH=/path/to/subscription.json
|
||||
```
|
||||
@@ -307,6 +310,9 @@ UI_PORT=4310
|
||||
一般只需要在这些情况下修改:
|
||||
- `GATEWAY_URL`:你的 Gateway 不在默认本地地址
|
||||
- `OPENCLAW_HOME`:OpenClaw 不在 `~/.openclaw`
|
||||
- `OPENCLAW_CONFIG_PATH`:`openclaw.json` 不在默认位置
|
||||
- `OPENCLAW_WORKSPACE_ROOT`:control-center 装在工作区树外,需要显式指定工作区根目录
|
||||
- `OPENCLAW_AGENT_ROOT`:control-center 不在某个 agent 工作区内,但你仍希望旧版记忆/说明读取链指向指定 agent
|
||||
- `CODEX_HOME`:Codex 数据不在 `~/.codex`
|
||||
- `OPENCLAW_SUBSCRIPTION_SNAPSHOT_PATH`:订阅或账单快照文件在自定义位置
|
||||
- `UI_PORT`:`4310` 已被占用
|
||||
@@ -344,7 +350,7 @@ UI_MODE=true npm run dev
|
||||
|
||||
### 8. 如果看起来不对
|
||||
- 实时活动全空,通常是 `GATEWAY_URL` 错了,或者 OpenClaw Gateway 没启动
|
||||
- `文档 / 记忆` 范围不对,通常是 `OPENCLAW_HOME` 指错了,或者 `openclaw.json` 不可读
|
||||
- `文档 / 记忆` 范围不对,通常是 `OPENCLAW_HOME` / `OPENCLAW_CONFIG_PATH` / `OPENCLAW_WORKSPACE_ROOT` 指错了,或者 `openclaw.json` 不可读
|
||||
- `用量 / 订阅` 没数据,通常是 `CODEX_HOME` 或 `OPENCLAW_SUBSCRIPTION_SNAPSHOT_PATH` 没配对
|
||||
- 如果你只是想先安全观察,不要改默认的只读和 mutation 开关
|
||||
|
||||
|
||||
@@ -1,5 +1,34 @@
|
||||
# Progress
|
||||
|
||||
## Phase 154 (Workspace root overrides and USER/TASKS doc discovery) — Completed
|
||||
- Scope:
|
||||
- Fix document and memory file discovery when control-center is installed outside the OpenClaw workspace tree.
|
||||
- Expose `USER.md` and `TASKS.md` in the workspace file workbench instead of silently omitting them.
|
||||
- Changed files:
|
||||
- `src/ui/server.ts`
|
||||
- `.env.example`
|
||||
- `README.md`
|
||||
- `README.en.md`
|
||||
- `INSTALL_PROMPT.md`
|
||||
- `INSTALL_PROMPT.en.md`
|
||||
- `test/ui-render-smoke.test.ts`
|
||||
- `docs/PROGRESS.md`
|
||||
- Implementation:
|
||||
- Added explicit support for `OPENCLAW_CONFIG_PATH`, `OPENCLAW_WORKSPACE_ROOT`, and `OPENCLAW_AGENT_ROOT` in the UI server path resolution layer.
|
||||
- Replaced the hardcoded `process.cwd()`-relative workspace-root assumption with a safer resolution order:
|
||||
- explicit `OPENCLAW_WORKSPACE_ROOT`
|
||||
- inferred workspace root from `openclaw.json`
|
||||
- fallback to `OPENCLAW_HOME/workspace`
|
||||
- Resolved configured agent workspace paths relative to the OpenClaw config directory so relative `workspace` entries no longer break when control-center is installed elsewhere.
|
||||
- Added `USER.md` and `TASKS.md` to both shared and per-agent workspace document candidates, and raised their sort priority so they surface naturally in the workbench.
|
||||
- Extended install docs and copy-paste install prompts so standalone installs outside the workspace tree now point users to the correct override envs instead of leaving discovery to fail silently.
|
||||
- Verification:
|
||||
- `npm run build`
|
||||
- `npm test`
|
||||
- `npm run smoke:ui`
|
||||
- Remaining gap:
|
||||
- The legacy agent-local memory summary helpers still use `OPENCLAW_AGENT_ROOT` for a few non-workbench fallbacks. Workbench discovery is fixed, but fully decoupling every legacy fallback from repo location can be done later if standalone users report it.
|
||||
|
||||
## Phase 153 (CLI insight cards for connection, memory, context, security, and update state) — Completed
|
||||
- Scope:
|
||||
- Add high-value OpenClaw CLI visibility surfaces without changing the existing information architecture, visual style, or operator workflows.
|
||||
|
||||
+94
-7
@@ -1,5 +1,6 @@
|
||||
import { createServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { readdir, readFile, stat, writeFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { basename, dirname, extname, join, relative, resolve } from "node:path";
|
||||
@@ -127,13 +128,15 @@ import type {
|
||||
|
||||
const SNAPSHOT_PATH = join(process.cwd(), "runtime", "last-snapshot.json");
|
||||
const OPENCLAW_HOME_DIR = process.env.OPENCLAW_HOME?.trim() || join(homedir(), ".openclaw");
|
||||
const OPENCLAW_CONFIG_PATH = process.env.OPENCLAW_CONFIG_PATH?.trim() || join(OPENCLAW_HOME_DIR, "openclaw.json");
|
||||
const OPENCLAW_CONFIG_DIR = dirname(OPENCLAW_CONFIG_PATH);
|
||||
const OPENCLAW_CRON_JOBS_CANDIDATES = [
|
||||
join(OPENCLAW_HOME_DIR, "cron", "jobs.json"),
|
||||
join(process.cwd(), "..", "..", "..", "..", "cron", "jobs.json"),
|
||||
];
|
||||
const DOCS_DIR = join(process.cwd(), "docs");
|
||||
const README_PATH = join(process.cwd(), "README.md");
|
||||
const AGENT_ROOT_DIR = join(process.cwd(), "..");
|
||||
const AGENT_ROOT_DIR = process.env.OPENCLAW_AGENT_ROOT?.trim() || join(process.cwd(), "..");
|
||||
const MEMORY_DIR_CANDIDATES = [
|
||||
join(AGENT_ROOT_DIR, "memory"),
|
||||
join(process.cwd(), "runtime", "digests"),
|
||||
@@ -163,7 +166,11 @@ const EDITABLE_TEXT_CONTENT_MAX_CHARS = 240_000;
|
||||
const SEARCH_LIMIT_MAX = 200;
|
||||
const TASK_RUNTIME_ACTIVITY_WINDOW_MS = 6 * 60 * 60 * 1000;
|
||||
const STALLED_RUNNING_SESSION_WINDOW_MS = 2 * 60 * 60 * 1000;
|
||||
const OPENCLAW_WORKSPACE_ROOT = resolve(process.cwd(), "..", "..", "..");
|
||||
const OPENCLAW_WORKSPACE_ROOT = resolveOpenClawWorkspaceRoot({
|
||||
explicitWorkspaceRoot: process.env.OPENCLAW_WORKSPACE_ROOT?.trim(),
|
||||
openclawHomeDir: OPENCLAW_HOME_DIR,
|
||||
configPath: OPENCLAW_CONFIG_PATH,
|
||||
});
|
||||
const WORKSPACE_EDITABLE_SKIP_DIRS = new Set(["node_modules", ".git", "dist", "coverage"]);
|
||||
const WORKSPACE_EDITABLE_EXTENSIONS = new Set([".md", ".markdown"]);
|
||||
const MEMORY_EDITABLE_EXTENSIONS = new Set([".md", ".markdown", ".txt"]);
|
||||
@@ -171,6 +178,8 @@ const SHARED_DOCUMENT_FILE_CANDIDATES = [
|
||||
"AGENTS.md",
|
||||
"IDENTITY.md",
|
||||
"SOUL.md",
|
||||
"USER.md",
|
||||
"TASKS.md",
|
||||
"BOOTSTRAP.md",
|
||||
"HEARTBEAT.md",
|
||||
"TOOLS.md",
|
||||
@@ -181,6 +190,8 @@ const AGENT_DOCUMENT_FILE_CANDIDATES = [
|
||||
"AGENTS.md",
|
||||
"IDENTITY.md",
|
||||
"SOUL.md",
|
||||
"USER.md",
|
||||
"TASKS.md",
|
||||
"HEARTBEAT.md",
|
||||
"TOOLS.md",
|
||||
"README.md",
|
||||
@@ -214,7 +225,6 @@ const DASHBOARD_SECTIONS = [
|
||||
"settings",
|
||||
] as const;
|
||||
const CONTROL_CENTER_MAPPING_TASK_IDS = new Set(["due-fast", "todo-second", "already-running", "unassigned"]);
|
||||
const OPENCLAW_CONFIG_PATH = join(OPENCLAW_HOME_DIR, "openclaw.json");
|
||||
const LEGACY_DASHBOARD_ROUTE_SECTION = {
|
||||
"/calendar": "projects-tasks",
|
||||
"/heartbeat": "overview",
|
||||
@@ -225,6 +235,74 @@ const LEGACY_DASHBOARD_ROUTE_ANCHOR = {
|
||||
"/heartbeat": "heartbeat-health",
|
||||
"/tools": "tool-connectors",
|
||||
} as const;
|
||||
function resolveOpenClawWorkspaceRoot(input: {
|
||||
explicitWorkspaceRoot?: string;
|
||||
openclawHomeDir: string;
|
||||
configPath: string;
|
||||
}): string {
|
||||
const explicit = input.explicitWorkspaceRoot?.trim();
|
||||
if (explicit) return resolve(explicit);
|
||||
const configText = safeReadTextFileSync(input.configPath);
|
||||
const inferred = inferWorkspaceRootFromConfigText(configText, dirname(input.configPath));
|
||||
if (inferred) return inferred;
|
||||
return join(input.openclawHomeDir, "workspace");
|
||||
}
|
||||
|
||||
function safeReadTextFileSync(path: string): string | undefined {
|
||||
try {
|
||||
return readFileSync(path, "utf8");
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function inferWorkspaceRootFromConfigText(raw: string | undefined, configDir: string): string | undefined {
|
||||
if (!raw?.trim()) return undefined;
|
||||
try {
|
||||
return inferWorkspaceRootFromConfigObject(JSON.parse(raw) as unknown, configDir);
|
||||
} catch {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function inferWorkspaceRootFromConfigObject(input: unknown, configDir: string): string | undefined {
|
||||
const root = asObject(input);
|
||||
const agents = asObject(root?.agents);
|
||||
const list = asArray(agents?.list);
|
||||
const mainRow = list
|
||||
.map((item) => asObject(item))
|
||||
.find((row) => normalizeLookupKey(asString(row?.id)?.trim() ?? asString(row?.name)?.trim() ?? "") === "main");
|
||||
const explicitMainWorkspace = asString(mainRow?.workspace)?.trim();
|
||||
if (explicitMainWorkspace) return resolve(configDir, explicitMainWorkspace);
|
||||
|
||||
const inferredRoots = new Set<string>();
|
||||
for (const item of list) {
|
||||
const row = asObject(item);
|
||||
const rawWorkspace = asString(row?.workspace)?.trim();
|
||||
if (!rawWorkspace) continue;
|
||||
const workspacePath = resolve(configDir, rawWorkspace);
|
||||
const parentDir = dirname(workspacePath);
|
||||
if (basename(parentDir).toLowerCase() !== "agents") continue;
|
||||
inferredRoots.add(dirname(parentDir));
|
||||
}
|
||||
if (inferredRoots.size === 1) return [...inferredRoots][0];
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function resolveOpenClawWorkspaceRootForSmoke(input: {
|
||||
explicitWorkspaceRoot?: string;
|
||||
openclawHomeDir: string;
|
||||
configText?: string;
|
||||
configPath?: string;
|
||||
}): string {
|
||||
const explicit = input.explicitWorkspaceRoot?.trim();
|
||||
if (explicit) return resolve(explicit);
|
||||
const configDir = dirname(input.configPath?.trim() || join(input.openclawHomeDir, "openclaw.json"));
|
||||
const inferred = inferWorkspaceRootFromConfigText(input.configText, configDir);
|
||||
if (inferred) return inferred;
|
||||
return join(input.openclawHomeDir, "workspace");
|
||||
}
|
||||
|
||||
const TASK_STATES: TaskState[] = ["todo", "in_progress", "blocked", "done"];
|
||||
const SESSION_STATES: AgentRunState[] = ["idle", "running", "blocked", "waiting_approval", "error"];
|
||||
const DOC_LINKS = [
|
||||
@@ -8663,7 +8741,9 @@ function resolveStaffWorkspaceRoot(member: TeamMemberSnapshot): string {
|
||||
const key = normalizeLookupKey(member.agentId);
|
||||
if (key === "main") return OPENCLAW_WORKSPACE_ROOT;
|
||||
const workspace = member.workspace.trim();
|
||||
if (workspace && workspace !== "未标注" && workspace !== "unlisted") return workspace;
|
||||
if (workspace && workspace !== "未标注" && workspace !== "unlisted") {
|
||||
return resolve(OPENCLAW_CONFIG_DIR, workspace);
|
||||
}
|
||||
return join(OPENCLAW_WORKSPACE_ROOT, "agents", member.agentId);
|
||||
}
|
||||
|
||||
@@ -8834,13 +8914,12 @@ async function buildEditableFileEntry(input: {
|
||||
if (!meta.isFile() || meta.size > EDITABLE_TEXT_FILE_MAX_BYTES) return undefined;
|
||||
const raw = await safeReadTextFile(input.sourcePath);
|
||||
if (raw === undefined) return undefined;
|
||||
const ext = extname(input.sourcePath).toLowerCase();
|
||||
const relativePath = input.relativeBase
|
||||
? relative(input.relativeBase, input.sourcePath) || basename(input.sourcePath)
|
||||
: basename(input.sourcePath);
|
||||
return {
|
||||
scope: input.scope,
|
||||
title: extractMarkdownHeading(raw) || basename(input.sourcePath, ext) || basename(input.sourcePath),
|
||||
title: basename(input.sourcePath) || relativePath,
|
||||
excerpt: toPlainSummary(raw, 160),
|
||||
category: input.category,
|
||||
sourcePath: input.sourcePath,
|
||||
@@ -9038,6 +9117,8 @@ function documentFilePriority(relativePath: string): number {
|
||||
"agents.md",
|
||||
"identity.md",
|
||||
"soul.md",
|
||||
"user.md",
|
||||
"tasks.md",
|
||||
"bootstrap.md",
|
||||
"heartbeat.md",
|
||||
"tools.md",
|
||||
@@ -9127,7 +9208,7 @@ function resolveEditableAgentScopesFromConfig(input: unknown): EditableAgentScop
|
||||
const workspaceRoot =
|
||||
facetKey === "main"
|
||||
? OPENCLAW_WORKSPACE_ROOT
|
||||
: resolve(asString(row.workspace)?.trim() || join(OPENCLAW_WORKSPACE_ROOT, "agents", rawId));
|
||||
: resolveConfiguredWorkspaceRoot(asString(row.workspace)?.trim(), rawId);
|
||||
output.push({
|
||||
agentId: rawId,
|
||||
facetKey,
|
||||
@@ -9211,6 +9292,12 @@ function resolveEditableAgentScopesFromWorkspaceAgentIds(agentIds: string[]): Ed
|
||||
return scopes.sort(compareEditableAgentScopes);
|
||||
}
|
||||
|
||||
function resolveConfiguredWorkspaceRoot(rawWorkspace: string | undefined, agentId: string): string {
|
||||
const workspace = rawWorkspace?.trim();
|
||||
if (workspace) return resolve(OPENCLAW_CONFIG_DIR, workspace);
|
||||
return join(OPENCLAW_WORKSPACE_ROOT, "agents", agentId);
|
||||
}
|
||||
|
||||
async function resolveEditableFileEntry(
|
||||
scope: EditableFileScope,
|
||||
sourcePath: string,
|
||||
|
||||
@@ -425,11 +425,14 @@ test("memory and workspace sections expose editable file workbenches", async ()
|
||||
assert(source.includes('const AGENT_DOCUMENT_FILE_CANDIDATES = ['));
|
||||
assert(source.includes('"IDENTITY.md"'));
|
||||
assert(source.includes('"SOUL.md"'));
|
||||
assert(source.includes('"USER.md"'));
|
||||
assert(source.includes('"TASKS.md"'));
|
||||
assert(source.includes('"BOOTSTRAP.md"'));
|
||||
assert(source.includes("listMemoryFacetOptions()"));
|
||||
assert(source.includes("listWorkspaceFacetOptions()"));
|
||||
assert(source.includes("facetOptions: memoryFacetOptions"));
|
||||
assert(source.includes("facetOptions: workspaceFacetOptions"));
|
||||
assert(source.includes('title: basename(input.sourcePath) || relativePath,'));
|
||||
assert(source.includes("defaultFacetKey: \"main\""));
|
||||
assert(source.includes("defaultFacetKey: \"main\""));
|
||||
assert(source.includes("includeAllFacet: false"));
|
||||
@@ -504,10 +507,40 @@ test("memory and workspace sections expose editable file workbenches", async ()
|
||||
|
||||
test("editable agent scopes follow configured agents before workspace folders", async () => {
|
||||
const {
|
||||
resolveOpenClawWorkspaceRootForSmoke,
|
||||
resolveEditableAgentScopesFromConfigForSmoke,
|
||||
resolveEditableAgentScopesWithFallbackForSmoke,
|
||||
} = await import("../src/ui/server");
|
||||
|
||||
assert.equal(
|
||||
resolveOpenClawWorkspaceRootForSmoke({
|
||||
openclawHomeDir: "/home/test/.openclaw",
|
||||
configPath: "/home/test/.openclaw/openclaw.json",
|
||||
configText: JSON.stringify({
|
||||
agents: {
|
||||
list: [
|
||||
{ id: "main" },
|
||||
{ id: "pandas", workspace: "/srv/openclaw/workspace/agents/pandas" },
|
||||
],
|
||||
},
|
||||
}),
|
||||
}),
|
||||
"/srv/openclaw/workspace",
|
||||
);
|
||||
assert.equal(
|
||||
resolveOpenClawWorkspaceRootForSmoke({
|
||||
explicitWorkspaceRoot: "/data/openclaw/workspace",
|
||||
openclawHomeDir: "/home/test/.openclaw",
|
||||
}),
|
||||
"/data/openclaw/workspace",
|
||||
);
|
||||
assert.equal(
|
||||
resolveOpenClawWorkspaceRootForSmoke({
|
||||
openclawHomeDir: "/home/test/.openclaw",
|
||||
}),
|
||||
"/home/test/.openclaw/workspace",
|
||||
);
|
||||
|
||||
const scopes = resolveEditableAgentScopesFromConfigForSmoke({
|
||||
agents: {
|
||||
list: [
|
||||
|
||||
Reference in New Issue
Block a user