@ww-ai-lab/openclaw-office 2026.3.8 → 2026.3.21
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 +14 -30
- package/README.zh.md +14 -30
- package/bin/openclaw-office.js +342 -16
- package/dist/assets/{ActivityHeatmap-CnKWcUWF.js → ActivityHeatmap-CvGomuJN.js} +1 -1
- package/dist/assets/{CostPieChart-DmUzkXKt.js → CostPieChart-BIcqqhp7.js} +3 -3
- package/dist/assets/{NetworkGraph-C0F11y9I.js → NetworkGraph-B-nEMJ_M.js} +1 -1
- package/dist/assets/{Scene3D-q5P1N1qu.js → Scene3D-DC1g2KU1.js} +2 -2
- package/dist/assets/{TokenLineChart-odcmdCAg.js → TokenLineChart-CfPgD-hP.js} +1 -1
- package/dist/assets/{generateCategoricalChart-Onj3oDrM.js → generateCategoricalChart-DUrRp22n.js} +1 -1
- package/dist/assets/index-DehfYC23.css +1 -0
- package/dist/assets/index-DqQHpfxy.js +462 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/bin/openclaw-office-config.d.ts +0 -44
- package/bin/openclaw-office-config.js +0 -219
- package/bin/openclaw-office-server.d.ts +0 -34
- package/bin/openclaw-office-server.js +0 -342
- package/dist/assets/index-DweWoR1-.css +0 -1
- package/dist/assets/index-QZYyP0qG.js +0 -462
package/dist/index.html
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
6
|
<title>OpenClaw Office</title>
|
|
7
7
|
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
|
8
|
-
<script type="module" crossorigin src="/assets/index-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
8
|
+
<script type="module" crossorigin src="/assets/index-DqQHpfxy.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DehfYC23.css">
|
|
10
10
|
</head>
|
|
11
11
|
<body>
|
|
12
12
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,44 +0,0 @@
|
|
|
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;
|
|
@@ -1,219 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
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
|
-
};
|
|
@@ -1,342 +0,0 @@
|
|
|
1
|
-
import { createServer } from "node:http";
|
|
2
|
-
import { access, readFile } from "node:fs/promises";
|
|
3
|
-
import { request as httpRequest } from "node:http";
|
|
4
|
-
import { request as httpsRequest } from "node:https";
|
|
5
|
-
import { networkInterfaces } from "node:os";
|
|
6
|
-
import { extname, join } from "node:path";
|
|
7
|
-
|
|
8
|
-
const RUNTIME_CONNECTION_PATH = "/__openclaw/connection";
|
|
9
|
-
|
|
10
|
-
export const MIME_TYPES = {
|
|
11
|
-
".html": "text/html; charset=utf-8",
|
|
12
|
-
".js": "application/javascript; charset=utf-8",
|
|
13
|
-
".css": "text/css; charset=utf-8",
|
|
14
|
-
".json": "application/json; charset=utf-8",
|
|
15
|
-
".svg": "image/svg+xml",
|
|
16
|
-
".png": "image/png",
|
|
17
|
-
".jpg": "image/jpeg",
|
|
18
|
-
".jpeg": "image/jpeg",
|
|
19
|
-
".gif": "image/gif",
|
|
20
|
-
".ico": "image/x-icon",
|
|
21
|
-
".woff": "font/woff",
|
|
22
|
-
".woff2": "font/woff2",
|
|
23
|
-
".ttf": "font/ttf",
|
|
24
|
-
".webp": "image/webp",
|
|
25
|
-
".glb": "model/gltf-binary",
|
|
26
|
-
".gltf": "model/gltf+json",
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export function createRuntimeConfigScript(config) {
|
|
30
|
-
const runtimeConfig = JSON.stringify({
|
|
31
|
-
gatewayUrl: config.browserGatewayUrl,
|
|
32
|
-
gatewayToken: config.token,
|
|
33
|
-
});
|
|
34
|
-
return `<script>window.__OPENCLAW_CONFIG__=${runtimeConfig};</script>`;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
export function formatStartupSummary(config) {
|
|
38
|
-
const lines = [
|
|
39
|
-
"",
|
|
40
|
-
" \x1b[36m\u{1F3E2} OpenClaw Office\x1b[0m",
|
|
41
|
-
"",
|
|
42
|
-
` \x1b[32m\u{27A1}\x1b[0m Local: \x1b[36mhttp://localhost:${config.port}\x1b[0m`,
|
|
43
|
-
];
|
|
44
|
-
|
|
45
|
-
if (config.host === "0.0.0.0") {
|
|
46
|
-
const nets = networkInterfaces();
|
|
47
|
-
for (const name of Object.keys(nets)) {
|
|
48
|
-
for (const net of nets[name] || []) {
|
|
49
|
-
if (net.family === "IPv4" && !net.internal) {
|
|
50
|
-
lines.push(` \x1b[32m\u{27A1}\x1b[0m Network: \x1b[36mhttp://${net.address}:${config.port}\x1b[0m`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
lines.push("");
|
|
57
|
-
lines.push(
|
|
58
|
-
` \x1b[32m\u{27A1}\x1b[0m Gateway: \x1b[33m${config.gatewayUrl}\x1b[0m \x1b[90m(from ${config.gatewayUrlSource})\x1b[0m`,
|
|
59
|
-
);
|
|
60
|
-
lines.push(
|
|
61
|
-
` \x1b[32m\u{27A1}\x1b[0m Proxy: \x1b[36m${config.browserGatewayUrl}\x1b[0m`,
|
|
62
|
-
);
|
|
63
|
-
|
|
64
|
-
if (config.token) {
|
|
65
|
-
lines.push(
|
|
66
|
-
` \x1b[32m\u{2713}\x1b[0m Token: \x1b[32mloaded\x1b[0m \x1b[90m(from ${config.tokenSource})\x1b[0m`,
|
|
67
|
-
);
|
|
68
|
-
} else {
|
|
69
|
-
lines.push(` \x1b[33m\u{26A0}\x1b[0m Token: \x1b[33mnot found\x1b[0m`);
|
|
70
|
-
lines.push("");
|
|
71
|
-
lines.push(" \x1b[90mTo connect to Gateway, provide a token:\x1b[0m");
|
|
72
|
-
lines.push(" \x1b[90m openclaw-office --token <your-token>\x1b[0m");
|
|
73
|
-
lines.push(" \x1b[90m or install openclaw CLI and the token will be auto-detected\x1b[0m");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
lines.push("");
|
|
77
|
-
lines.push(" Press \x1b[1mCtrl+C\x1b[0m to stop");
|
|
78
|
-
lines.push("");
|
|
79
|
-
|
|
80
|
-
return lines.join("\n");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function buildSocketErrorResponse(statusCode, message) {
|
|
84
|
-
return `HTTP/1.1 ${statusCode} ${message}\r\nConnection: close\r\n\r\n`;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
function toUpstreamHttpUrl(gatewayUrl) {
|
|
88
|
-
const upstreamUrl = new URL(gatewayUrl);
|
|
89
|
-
if (upstreamUrl.protocol === "ws:") {
|
|
90
|
-
upstreamUrl.protocol = "http:";
|
|
91
|
-
} else if (upstreamUrl.protocol === "wss:") {
|
|
92
|
-
upstreamUrl.protocol = "https:";
|
|
93
|
-
} else if (upstreamUrl.protocol !== "http:" && upstreamUrl.protocol !== "https:") {
|
|
94
|
-
throw new Error(`Unsupported Gateway protocol: ${upstreamUrl.protocol}`);
|
|
95
|
-
}
|
|
96
|
-
return upstreamUrl;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function toUpstreamOrigin(gatewayUrl) {
|
|
100
|
-
return toUpstreamHttpUrl(gatewayUrl).origin;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function serializeUpgradeResponse(statusCode, statusMessage, headers) {
|
|
104
|
-
const lines = [`HTTP/1.1 ${statusCode} ${statusMessage}`];
|
|
105
|
-
for (const [key, value] of Object.entries(headers)) {
|
|
106
|
-
if (value === undefined) {
|
|
107
|
-
continue;
|
|
108
|
-
}
|
|
109
|
-
if (Array.isArray(value)) {
|
|
110
|
-
for (const item of value) {
|
|
111
|
-
lines.push(`${key}: ${item}`);
|
|
112
|
-
}
|
|
113
|
-
continue;
|
|
114
|
-
}
|
|
115
|
-
lines.push(`${key}: ${value}`);
|
|
116
|
-
}
|
|
117
|
-
lines.push("\r\n");
|
|
118
|
-
return lines.join("\r\n");
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function pipeProxySockets(downstreamSocket, upstreamSocket, downstreamHead, upstreamHead) {
|
|
122
|
-
if (downstreamHead.length > 0) {
|
|
123
|
-
upstreamSocket.write(downstreamHead);
|
|
124
|
-
}
|
|
125
|
-
if (upstreamHead.length > 0) {
|
|
126
|
-
downstreamSocket.write(upstreamHead);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
downstreamSocket.pipe(upstreamSocket);
|
|
130
|
-
upstreamSocket.pipe(downstreamSocket);
|
|
131
|
-
|
|
132
|
-
const closeBoth = () => {
|
|
133
|
-
if (!downstreamSocket.destroyed) {
|
|
134
|
-
downstreamSocket.destroy();
|
|
135
|
-
}
|
|
136
|
-
if (!upstreamSocket.destroyed) {
|
|
137
|
-
upstreamSocket.destroy();
|
|
138
|
-
}
|
|
139
|
-
};
|
|
140
|
-
|
|
141
|
-
downstreamSocket.on("error", closeBoth);
|
|
142
|
-
upstreamSocket.on("error", closeBoth);
|
|
143
|
-
downstreamSocket.on("close", closeBoth);
|
|
144
|
-
upstreamSocket.on("close", closeBoth);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
export function proxyWebSocketUpgrade(req, downstreamSocket, downstreamHead, config) {
|
|
148
|
-
const pathname = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`).pathname;
|
|
149
|
-
if (pathname !== config.browserGatewayUrl) {
|
|
150
|
-
downstreamSocket.end(buildSocketErrorResponse(404, "Not Found"));
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
let upstreamUrl;
|
|
155
|
-
try {
|
|
156
|
-
upstreamUrl = toUpstreamHttpUrl(config.gatewayUrl);
|
|
157
|
-
} catch (error) {
|
|
158
|
-
downstreamSocket.end(buildSocketErrorResponse(502, "Bad Gateway"));
|
|
159
|
-
return;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const requestImpl = upstreamUrl.protocol === "https:" ? httpsRequest : httpRequest;
|
|
163
|
-
const upstreamReq = requestImpl(upstreamUrl, {
|
|
164
|
-
method: "GET",
|
|
165
|
-
headers: {
|
|
166
|
-
...req.headers,
|
|
167
|
-
host: upstreamUrl.host,
|
|
168
|
-
origin: toUpstreamOrigin(config.gatewayUrl),
|
|
169
|
-
connection: "Upgrade",
|
|
170
|
-
upgrade: "websocket",
|
|
171
|
-
},
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
let settled = false;
|
|
175
|
-
const fail = (statusCode, message) => {
|
|
176
|
-
if (settled) {
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
settled = true;
|
|
180
|
-
downstreamSocket.end(buildSocketErrorResponse(statusCode, message));
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
upstreamReq.on("upgrade", (upstreamRes, upstreamSocket, upstreamHead) => {
|
|
184
|
-
if (settled) {
|
|
185
|
-
upstreamSocket.destroy();
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
settled = true;
|
|
189
|
-
downstreamSocket.write(
|
|
190
|
-
serializeUpgradeResponse(
|
|
191
|
-
upstreamRes.statusCode || 101,
|
|
192
|
-
upstreamRes.statusMessage || "Switching Protocols",
|
|
193
|
-
upstreamRes.headers,
|
|
194
|
-
),
|
|
195
|
-
);
|
|
196
|
-
pipeProxySockets(downstreamSocket, upstreamSocket, downstreamHead, upstreamHead);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
upstreamReq.on("response", (upstreamRes) => {
|
|
200
|
-
upstreamRes.resume();
|
|
201
|
-
fail(upstreamRes.statusCode || 502, upstreamRes.statusMessage || "Bad Gateway");
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
upstreamReq.on("error", () => {
|
|
205
|
-
fail(502, "Bad Gateway");
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
downstreamSocket.on("error", () => {
|
|
209
|
-
upstreamReq.destroy();
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
downstreamSocket.on("close", () => {
|
|
213
|
-
upstreamReq.destroy();
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
upstreamReq.end();
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
async function tryReadFile(filePath) {
|
|
220
|
-
try {
|
|
221
|
-
await access(filePath);
|
|
222
|
-
return await readFile(filePath);
|
|
223
|
-
} catch {
|
|
224
|
-
return null;
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async function readJsonBody(req) {
|
|
229
|
-
const chunks = [];
|
|
230
|
-
for await (const chunk of req) {
|
|
231
|
-
chunks.push(typeof chunk === "string" ? Buffer.from(chunk) : chunk);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
if (chunks.length === 0) {
|
|
235
|
-
return {};
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
export function createOfficeServer({
|
|
242
|
-
config,
|
|
243
|
-
distDir,
|
|
244
|
-
createHttpServer = createServer,
|
|
245
|
-
}) {
|
|
246
|
-
const configScript = createRuntimeConfigScript(config);
|
|
247
|
-
const runtimeState = {
|
|
248
|
-
currentGatewayUrl: config.gatewayUrl,
|
|
249
|
-
defaultGatewayUrl: config.gatewayUrl,
|
|
250
|
-
};
|
|
251
|
-
let indexHtmlCache = null;
|
|
252
|
-
|
|
253
|
-
async function getIndexHtml() {
|
|
254
|
-
if (indexHtmlCache) {
|
|
255
|
-
return indexHtmlCache;
|
|
256
|
-
}
|
|
257
|
-
const raw = await readFile(join(distDir, "index.html"), "utf-8");
|
|
258
|
-
indexHtmlCache = raw.replace("</head>", `${configScript}\n</head>`);
|
|
259
|
-
return indexHtmlCache;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const server = createHttpServer(async (req, res) => {
|
|
263
|
-
const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
|
|
264
|
-
const pathname = decodeURIComponent(url.pathname);
|
|
265
|
-
|
|
266
|
-
if (pathname === RUNTIME_CONNECTION_PATH) {
|
|
267
|
-
if (req.method === "GET") {
|
|
268
|
-
const mode =
|
|
269
|
-
runtimeState.currentGatewayUrl === runtimeState.defaultGatewayUrl ? "local" : "remote";
|
|
270
|
-
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
|
271
|
-
res.end(
|
|
272
|
-
JSON.stringify({
|
|
273
|
-
mode,
|
|
274
|
-
gatewayUrl: runtimeState.currentGatewayUrl,
|
|
275
|
-
}),
|
|
276
|
-
);
|
|
277
|
-
return;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
if (req.method === "POST") {
|
|
281
|
-
try {
|
|
282
|
-
const body = await readJsonBody(req);
|
|
283
|
-
if (body?.mode === "remote" && typeof body.gatewayUrl === "string" && body.gatewayUrl) {
|
|
284
|
-
runtimeState.currentGatewayUrl = body.gatewayUrl;
|
|
285
|
-
} else if (body?.mode === "local") {
|
|
286
|
-
runtimeState.currentGatewayUrl = runtimeState.defaultGatewayUrl;
|
|
287
|
-
} else {
|
|
288
|
-
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
289
|
-
res.end("Invalid connection config payload");
|
|
290
|
-
return;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
res.writeHead(204);
|
|
294
|
-
res.end();
|
|
295
|
-
return;
|
|
296
|
-
} catch {
|
|
297
|
-
res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
|
|
298
|
-
res.end("Invalid JSON payload");
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
res.writeHead(405, { "Content-Type": "text/plain; charset=utf-8" });
|
|
304
|
-
res.end("Method Not Allowed");
|
|
305
|
-
return;
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
if (pathname === "/" || pathname === "/index.html") {
|
|
309
|
-
const html = await getIndexHtml();
|
|
310
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
311
|
-
res.end(html);
|
|
312
|
-
return;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
const filePath = join(distDir, pathname);
|
|
316
|
-
const content = await tryReadFile(filePath);
|
|
317
|
-
|
|
318
|
-
if (content) {
|
|
319
|
-
const ext = extname(filePath).toLowerCase();
|
|
320
|
-
const mime = MIME_TYPES[ext] || "application/octet-stream";
|
|
321
|
-
res.writeHead(200, { "Content-Type": mime });
|
|
322
|
-
res.end(content);
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
const html = await getIndexHtml();
|
|
327
|
-
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
|
|
328
|
-
res.end(html);
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
server.on("upgrade", (req, socket, head) => {
|
|
332
|
-
proxyWebSocketUpgrade(req, socket, head, {
|
|
333
|
-
...config,
|
|
334
|
-
gatewayUrl: runtimeState.currentGatewayUrl,
|
|
335
|
-
});
|
|
336
|
-
});
|
|
337
|
-
|
|
338
|
-
return {
|
|
339
|
-
server,
|
|
340
|
-
getIndexHtml,
|
|
341
|
-
};
|
|
342
|
-
}
|