@ww-ai-lab/openclaw-office 2026.3.6 → 2026.3.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -33,7 +33,19 @@
33
33
 
34
34
  #### Demo Video
35
35
 
36
- https://github.com/WW-AI-Lab/openclaw-office/raw/main/assets/iShot_2026-03-02_21.33.38.mp4
36
+ <p align="center">
37
+ <a href="https://www.youtube.com/watch?v=ACXSFTSlVLY">
38
+ <img src="https://img.youtube.com/vi/ACXSFTSlVLY/maxresdefault.jpg" alt="OpenClaw Office Demo Video" width="100%" />
39
+ </a>
40
+ </p>
41
+
42
+ <p align="center">
43
+ ▶ Click the preview image above to play on YouTube
44
+ </p>
45
+
46
+ > GitHub sanitizes repository README content and does not allow embedded YouTube/iframe players, so the most reliable “preview + click-to-play” pattern is a linked video thumbnail.
47
+
48
+ [Watch on YouTube](https://www.youtube.com/watch?v=ACXSFTSlVLY)
37
49
 
38
50
  ### Console
39
51
 
@@ -124,6 +136,15 @@ OPENCLAW_GATEWAY_TOKEN=<token> openclaw-office
124
136
  | `--host <host>` | Bind address | `0.0.0.0` |
125
137
  | `-h, --help` | Show help | — |
126
138
 
139
+ The production server exposes Office publicly and proxies browser WebSocket traffic through the same origin path `/gateway-ws`. Its upstream Gateway address is resolved in this order:
140
+
141
+ 1. `--gateway`
142
+ 2. `OPENCLAW_GATEWAY_URL`
143
+ 3. persisted Office config at `~/.openclaw/openclaw-office.json`
144
+ 4. default `ws://localhost:18789`
145
+
146
+ When `--gateway` or `OPENCLAW_GATEWAY_URL` is provided, Office automatically persists the value to `~/.openclaw/openclaw-office.json` for future restarts.
147
+
127
148
  > **Note:** This serves the pre-built production bundle. For development with hot reload, see [Development](#development) below.
128
149
 
129
150
  ---
@@ -138,11 +159,10 @@ pnpm install
138
159
 
139
160
  ### 2. Configure Gateway Connection
140
161
 
141
- Create a `.env.local` file (gitignored) with your Gateway connection details:
162
+ Create a `.env.local` file (gitignored) with your Gateway token. `VITE_GATEWAY_URL` is optional and only needed if you want dev mode to proxy to a Gateway address other than the default `ws://localhost:18789`.
142
163
 
143
164
  ```bash
144
165
  cat > .env.local << 'EOF'
145
- VITE_GATEWAY_URL=ws://localhost:18789
146
166
  VITE_GATEWAY_TOKEN=<your-gateway-token>
147
167
  EOF
148
168
  ```
@@ -179,13 +199,13 @@ Ensure the OpenClaw Gateway is running on the configured address (default `local
179
199
  pnpm dev
180
200
  ```
181
201
 
182
- Open `http://localhost:5180` in your browser.
202
+ Open `http://localhost:5180` in your browser. In dev mode, the frontend always connects to the same-origin path `/gateway-ws`, and Vite proxies that path to the configured Gateway upstream (default `ws://localhost:18789`). `VITE_GATEWAY_URL` configures the proxy upstream and is not used as a browser-direct websocket URL.
183
203
 
184
204
  ### Environment Variables
185
205
 
186
206
  | Variable | Required | Default | Description |
187
207
  | -------------------- | ------------------------------------- | ---------------------- | ------------------------------------ |
188
- | `VITE_GATEWAY_URL` | No | `ws://localhost:18789` | Gateway WebSocket address |
208
+ | `VITE_GATEWAY_URL` | No | `ws://localhost:18789` | Optional override for the dev proxy upstream Gateway address |
189
209
  | `VITE_GATEWAY_TOKEN` | Yes (when connecting to real Gateway) | — | Gateway auth token |
190
210
  | `VITE_MOCK` | No | `false` | Enable mock mode (no Gateway needed) |
191
211
 
package/README.zh.md CHANGED
@@ -33,7 +33,19 @@
33
33
 
34
34
  #### 演示视频
35
35
 
36
- https://github.com/WW-AI-Lab/openclaw-office/raw/main/assets/iShot_2026-03-02_21.33.38.mp4
36
+ <p align="center">
37
+ <a href="https://www.youtube.com/watch?v=ACXSFTSlVLY">
38
+ <img src="https://img.youtube.com/vi/ACXSFTSlVLY/maxresdefault.jpg" alt="OpenClaw Office 演示视频" width="100%" />
39
+ </a>
40
+ </p>
41
+
42
+ <p align="center">
43
+ ▶ 点击上方预览图即可跳转播放
44
+ </p>
45
+
46
+ > GitHub 会清洗仓库 README 内容,不允许真正内嵌 YouTube / iframe 播放器,所以最稳妥的实现方式就是“预览图 + 点击播放跳转”。
47
+
48
+ [在 YouTube 观看](https://www.youtube.com/watch?v=ACXSFTSlVLY)
37
49
 
38
50
  ### 控制台
39
51
 
@@ -124,6 +136,15 @@ OPENCLAW_GATEWAY_TOKEN=<token> openclaw-office
124
136
  | `--host <host>` | 绑定地址 | `0.0.0.0` |
125
137
  | `-h, --help` | 显示帮助 | — |
126
138
 
139
+ 生产服务端会对外暴露 Office,并通过同源路径 `/gateway-ws` 代理浏览器的 WebSocket 流量。其上游 Gateway 地址按以下优先级解析:
140
+
141
+ 1. `--gateway`
142
+ 2. `OPENCLAW_GATEWAY_URL`
143
+ 3. Office 持久化配置 `~/.openclaw/openclaw-office.json`
144
+ 4. 默认值 `ws://localhost:18789`
145
+
146
+ 当提供 `--gateway` 或 `OPENCLAW_GATEWAY_URL` 时,Office 会自动将该值持久化到 `~/.openclaw/openclaw-office.json`,供后续重启复用。
147
+
127
148
  > **说明:** 此方式运行的是预构建的生产版本。如需热重载开发,请参见下方 [开发](#开发) 部分。
128
149
 
129
150
  ---
@@ -138,11 +159,10 @@ pnpm install
138
159
 
139
160
  ### 2. 配置 Gateway 连接
140
161
 
141
- 创建 `.env.local` 文件(已在 `.gitignore` 中,不会被提交),填入 Gateway 连接信息:
162
+ 创建 `.env.local` 文件(已在 `.gitignore` 中,不会被提交),填入 Gateway token。`VITE_GATEWAY_URL` 是可选项,仅在你希望 dev 模式代理到非默认 `ws://localhost:18789` 地址时才需要填写:
142
163
 
143
164
  ```bash
144
165
  cat > .env.local << 'EOF'
145
- VITE_GATEWAY_URL=ws://localhost:18789
146
166
  VITE_GATEWAY_TOKEN=<你的 gateway token>
147
167
  EOF
148
168
  ```
@@ -179,13 +199,13 @@ openclaw config set gateway.controlUi.dangerouslyDisableDeviceAuth true
179
199
  pnpm dev
180
200
  ```
181
201
 
182
- 在浏览器中打开 `http://localhost:5180`。
202
+ 在浏览器中打开 `http://localhost:5180`。在 dev 模式下,前端始终连接同源路径 `/gateway-ws`,再由 Vite 将该路径代理到配置的 Gateway 上游地址(默认 `ws://localhost:18789`)。`VITE_GATEWAY_URL` 仅用于配置代理上游,不会作为浏览器直连 WebSocket 地址使用。
183
203
 
184
204
  ### 环境变量
185
205
 
186
206
  | 变量 | 必须 | 默认值 | 说明 |
187
207
  | -------------------- | ------------------------- | ---------------------- | -------------------------------- |
188
- | `VITE_GATEWAY_URL` | 否 | `ws://localhost:18789` | Gateway WebSocket 地址 |
208
+ | `VITE_GATEWAY_URL` | 否 | `ws://localhost:18789` | 可选:覆盖 dev 代理的上游 Gateway 地址 |
189
209
  | `VITE_GATEWAY_TOKEN` | 是(连接真实 Gateway 时) | — | Gateway 认证 token |
190
210
  | `VITE_MOCK` | 否 | `false` | 启用 Mock 模式(不需要 Gateway) |
191
211
 
@@ -0,0 +1,44 @@
1
+ export const DEFAULT_GATEWAY_URL: string;
2
+ export const DEFAULT_PORT: number;
3
+ export const DEFAULT_HOST: string;
4
+ export const DEFAULT_PROXY_PATH: string;
5
+
6
+ export interface ParsedArgs {
7
+ token: string;
8
+ gatewayUrl: string;
9
+ port: number;
10
+ host: string;
11
+ help?: boolean;
12
+ }
13
+
14
+ export interface ResolvedConfig {
15
+ token: string;
16
+ tokenSource: string;
17
+ gatewayUrl: string;
18
+ gatewayUrlSource: string;
19
+ port: number;
20
+ host: string;
21
+ officeConfigPath: string;
22
+ browserGatewayUrl: string;
23
+ shouldPersistGatewayUrl: boolean;
24
+ }
25
+
26
+ export function getOfficeConfigPath(homeDir?: string): string;
27
+ export function parseArgs(argv?: string[]): ParsedArgs;
28
+ export function printHelp(): void;
29
+ export function readTokenFromConfig(
30
+ homeDir?: string,
31
+ ): { token: string; source: string } | null;
32
+ export function readPersistedOfficeConfig(
33
+ configPath?: string,
34
+ ): { gatewayUrl: string } | null;
35
+ export function writePersistedOfficeConfig(gatewayUrl: string, configPath?: string): void;
36
+ export function normalizeGatewayAccessUrl(rawGatewayUrl: string): {
37
+ gatewayUrl: string;
38
+ token: string;
39
+ };
40
+ export function resolveConfig(options?: {
41
+ argv?: string[];
42
+ env?: NodeJS.ProcessEnv | Record<string, string>;
43
+ homeDir?: string;
44
+ }): ResolvedConfig;
@@ -0,0 +1,219 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join } from "node:path";
4
+
5
+ export const DEFAULT_GATEWAY_URL = "ws://localhost:18789";
6
+ export const DEFAULT_PORT = 5180;
7
+ export const DEFAULT_HOST = "0.0.0.0";
8
+ export const DEFAULT_PROXY_PATH = "/gateway-ws";
9
+ const TOKEN_QUERY_PARAM = "token";
10
+
11
+ export function getOfficeConfigPath(homeDir = homedir()) {
12
+ return join(homeDir, ".openclaw", "openclaw-office.json");
13
+ }
14
+
15
+ export function parseArgs(argv = process.argv.slice(2)) {
16
+ const result = { token: "", gatewayUrl: "", port: 0, host: "" };
17
+ for (let i = 0; i < argv.length; i++) {
18
+ const arg = argv[i];
19
+ const next = argv[i + 1];
20
+ if ((arg === "--token" || arg === "-t") && next) {
21
+ result.token = next;
22
+ i++;
23
+ } else if ((arg === "--gateway" || arg === "-g") && next) {
24
+ result.gatewayUrl = next;
25
+ i++;
26
+ } else if ((arg === "--port" || arg === "-p") && next) {
27
+ result.port = parseInt(next, 10);
28
+ i++;
29
+ } else if (arg === "--host" && next) {
30
+ result.host = next;
31
+ i++;
32
+ } else if (arg === "--help" || arg === "-h") {
33
+ result.help = true;
34
+ }
35
+ }
36
+ return result;
37
+ }
38
+
39
+ export function printHelp() {
40
+ console.log(`
41
+ \x1b[36mOpenClaw Office\x1b[0m — Visual monitoring frontend for OpenClaw
42
+
43
+ \x1b[1mUsage:\x1b[0m
44
+ openclaw-office [options]
45
+
46
+ \x1b[1mOptions:\x1b[0m
47
+ -t, --token <token> Gateway auth token
48
+ -g, --gateway <url> Gateway WebSocket URL (default: ws://localhost:18789)
49
+ -p, --port <port> Server port (default: 5180, or PORT env)
50
+ --host <host> Bind address (default: 0.0.0.0)
51
+ -h, --help Show this help
52
+
53
+ \x1b[1mGateway URL persistence:\x1b[0m
54
+ The upstream Gateway URL is resolved in this order:
55
+ 1. --gateway flag
56
+ 2. OPENCLAW_GATEWAY_URL environment variable
57
+ 3. Persisted Office config at ~/.openclaw/openclaw-office.json
58
+ 4. Default ws://localhost:18789
59
+
60
+ \x1b[1mToken auto-detection:\x1b[0m
61
+ The token is resolved in this order:
62
+ 1. --token flag
63
+ 2. OPENCLAW_GATEWAY_TOKEN environment variable
64
+ 3. Auto-read from ~/.openclaw/openclaw.json
65
+
66
+ \x1b[1mExamples:\x1b[0m
67
+ openclaw-office
68
+ openclaw-office --token my-secret-token
69
+ openclaw-office --gateway ws://192.168.1.100:18789
70
+ PORT=3000 openclaw-office
71
+ `);
72
+ }
73
+
74
+ export function readTokenFromConfig(homeDir = homedir()) {
75
+ const candidates = [
76
+ join(homeDir, ".openclaw", "openclaw.json"),
77
+ join(homeDir, ".clawdbot", "clawdbot.json"),
78
+ ];
79
+
80
+ for (const filePath of candidates) {
81
+ try {
82
+ const raw = readFileSync(filePath, "utf-8");
83
+ const config = JSON.parse(raw);
84
+ const token = config?.gateway?.auth?.token;
85
+ if (token && typeof token === "string") {
86
+ return { token, source: filePath };
87
+ }
88
+ } catch {
89
+ // file not found or parse error
90
+ }
91
+ }
92
+
93
+ return null;
94
+ }
95
+
96
+ export function readPersistedOfficeConfig(configPath = getOfficeConfigPath()) {
97
+ if (!existsSync(configPath)) {
98
+ return null;
99
+ }
100
+
101
+ try {
102
+ const raw = readFileSync(configPath, "utf-8");
103
+ const parsed = JSON.parse(raw);
104
+ const gatewayUrl = parsed?.gatewayUrl;
105
+ if (typeof gatewayUrl !== "string" || gatewayUrl.length === 0) {
106
+ return null;
107
+ }
108
+ return { gatewayUrl };
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ export function writePersistedOfficeConfig(
115
+ gatewayUrl,
116
+ configPath = getOfficeConfigPath(),
117
+ ) {
118
+ mkdirSync(dirname(configPath), { recursive: true });
119
+ writeFileSync(configPath, `${JSON.stringify({ gatewayUrl }, null, 2)}\n`, "utf-8");
120
+ }
121
+
122
+ function formatParsedUrl(parsed, original) {
123
+ const serialized = parsed.toString();
124
+ if (/^[a-z]+:\/\/[^/?#]+(?:\?[^#]*)?(?:#.*)?$/i.test(original)) {
125
+ return serialized.replace(/\/$/, "");
126
+ }
127
+ return serialized;
128
+ }
129
+
130
+ export function normalizeGatewayAccessUrl(rawGatewayUrl) {
131
+ if (!rawGatewayUrl) {
132
+ return { gatewayUrl: rawGatewayUrl, token: "" };
133
+ }
134
+
135
+ try {
136
+ const parsed = new URL(rawGatewayUrl);
137
+ const token = parsed.searchParams.get(TOKEN_QUERY_PARAM) ?? "";
138
+ parsed.searchParams.delete(TOKEN_QUERY_PARAM);
139
+
140
+ if (parsed.protocol === "http:") {
141
+ parsed.protocol = "ws:";
142
+ } else if (parsed.protocol === "https:") {
143
+ parsed.protocol = "wss:";
144
+ }
145
+
146
+ return { gatewayUrl: formatParsedUrl(parsed, rawGatewayUrl), token };
147
+ } catch {
148
+ return { gatewayUrl: rawGatewayUrl, token: "" };
149
+ }
150
+ }
151
+
152
+ export function resolveConfig({
153
+ argv = process.argv.slice(2),
154
+ env = process.env,
155
+ homeDir = homedir(),
156
+ } = {}) {
157
+ const args = parseArgs(argv);
158
+ const officeConfigPath = getOfficeConfigPath(homeDir);
159
+ const persisted = readPersistedOfficeConfig(officeConfigPath);
160
+
161
+ let token = "";
162
+ let tokenSource = "";
163
+
164
+ if (args.token) {
165
+ token = args.token;
166
+ tokenSource = "command line --token";
167
+ } else if (env.OPENCLAW_GATEWAY_TOKEN) {
168
+ token = env.OPENCLAW_GATEWAY_TOKEN;
169
+ tokenSource = "OPENCLAW_GATEWAY_TOKEN env";
170
+ } else {
171
+ const fromFile = readTokenFromConfig(homeDir);
172
+ if (fromFile) {
173
+ token = fromFile.token;
174
+ tokenSource = fromFile.source;
175
+ }
176
+ }
177
+
178
+ let gatewayUrl = DEFAULT_GATEWAY_URL;
179
+ let gatewayUrlSource = "default";
180
+ if (persisted?.gatewayUrl) {
181
+ gatewayUrl = persisted.gatewayUrl;
182
+ gatewayUrlSource = officeConfigPath;
183
+ }
184
+ if (env.OPENCLAW_GATEWAY_URL) {
185
+ gatewayUrl = env.OPENCLAW_GATEWAY_URL;
186
+ gatewayUrlSource = "OPENCLAW_GATEWAY_URL env";
187
+ }
188
+ if (args.gatewayUrl) {
189
+ gatewayUrl = args.gatewayUrl;
190
+ gatewayUrlSource = "command line --gateway";
191
+ }
192
+
193
+ const normalizedGateway = normalizeGatewayAccessUrl(gatewayUrl);
194
+ gatewayUrl = normalizedGateway.gatewayUrl || gatewayUrl;
195
+
196
+ if (!token && normalizedGateway.token) {
197
+ token = normalizedGateway.token;
198
+ tokenSource = `${gatewayUrlSource} token query`;
199
+ }
200
+
201
+ const port = args.port || parseInt(env.PORT || `${DEFAULT_PORT}`, 10);
202
+ const host = args.host || env.HOST || DEFAULT_HOST;
203
+ const shouldPersistGatewayUrl =
204
+ !!gatewayUrl &&
205
+ (gatewayUrlSource === "command line --gateway" || gatewayUrlSource === "OPENCLAW_GATEWAY_URL env") &&
206
+ gatewayUrl !== persisted?.gatewayUrl;
207
+
208
+ return {
209
+ token,
210
+ tokenSource,
211
+ gatewayUrl,
212
+ gatewayUrlSource,
213
+ port,
214
+ host,
215
+ officeConfigPath,
216
+ browserGatewayUrl: DEFAULT_PROXY_PATH,
217
+ shouldPersistGatewayUrl,
218
+ };
219
+ }
@@ -0,0 +1,34 @@
1
+ import type { Server as HttpServer, IncomingMessage } from "node:http";
2
+ import type { Duplex } from "node:stream";
3
+
4
+ export const MIME_TYPES: Record<string, string>;
5
+
6
+ export interface OfficeServerConfig {
7
+ gatewayUrl: string;
8
+ browserGatewayUrl: string;
9
+ token: string;
10
+ port?: number;
11
+ host?: string;
12
+ gatewayUrlSource?: string;
13
+ tokenSource?: string;
14
+ }
15
+
16
+ export function createRuntimeConfigScript(config: {
17
+ browserGatewayUrl: string;
18
+ token: string;
19
+ }): string;
20
+ export function formatStartupSummary(config: OfficeServerConfig): string;
21
+ export function proxyWebSocketUpgrade(
22
+ req: IncomingMessage,
23
+ downstreamSocket: Duplex,
24
+ downstreamHead: Buffer,
25
+ config: OfficeServerConfig,
26
+ ): void;
27
+ export function createOfficeServer(options: {
28
+ config: OfficeServerConfig;
29
+ distDir: string;
30
+ createHttpServer?: typeof import("node:http").createServer;
31
+ }): {
32
+ server: HttpServer;
33
+ getIndexHtml: () => Promise<string>;
34
+ };