@ww-ai-lab/openclaw-office 2026.4.8 → 2026.4.10

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.en.md CHANGED
@@ -125,6 +125,38 @@ OPENCLAW_GATEWAY_TOKEN=<token> openclaw-office
125
125
 
126
126
  ---
127
127
 
128
+ ## Install as a System Service (Background Mode)
129
+
130
+ Register OpenClaw Office as a system service so it starts automatically on boot/login — no manual command needed. Supported on macOS (launchd) and Linux (systemd --user).
131
+
132
+ ### Install the Service
133
+
134
+ ```bash
135
+ # Install as system service (token auto-detected, or specify manually)
136
+ openclaw-office service install
137
+
138
+ # Specify token and port
139
+ openclaw-office service install --token <your-token> --port 3000
140
+ ```
141
+
142
+ Once installed, the service **starts immediately** and runs in the background. It will be automatically launched on every subsequent boot/login.
143
+
144
+ ### Service Management Commands
145
+
146
+ ```bash
147
+ openclaw-office service status # Check service status
148
+ openclaw-office service stop # Stop the service
149
+ openclaw-office service start # Start the service
150
+ openclaw-office service restart # Restart the service
151
+ openclaw-office service log # Show service logs
152
+ openclaw-office service log --follow # Follow log output in real time
153
+ openclaw-office service uninstall # Remove the system service
154
+ ```
155
+
156
+ > **Tip:** After installing as a service, you can also view Gateway status and perform operations like restart from the Settings page "Service Management" panel.
157
+
158
+ ---
159
+
128
160
  ## Quick Start (from source)
129
161
 
130
162
  ### 1. Install Dependencies
package/README.md CHANGED
@@ -125,6 +125,38 @@ OPENCLAW_GATEWAY_TOKEN=<token> openclaw-office
125
125
 
126
126
  ---
127
127
 
128
+ ## 安装为系统服务(后台运行)
129
+
130
+ 将 OpenClaw Office 注册为系统服务后,它会在开机 / 登录时自动启动,无需手动运行命令。支持 macOS(launchd)和 Linux(systemd --user)。
131
+
132
+ ### 安装服务
133
+
134
+ ```bash
135
+ # 安装为系统服务(token 自动检测,也可手动指定)
136
+ openclaw-office service install
137
+
138
+ # 指定 token 和端口
139
+ openclaw-office service install --token <your-token> --port 3000
140
+ ```
141
+
142
+ 安装完成后,服务会**立即启动**并在后台运行。后续每次开机/登录,服务将自动拉起。
143
+
144
+ ### 服务管理命令
145
+
146
+ ```bash
147
+ openclaw-office service status # 查看服务状态
148
+ openclaw-office service stop # 停止服务
149
+ openclaw-office service start # 启动服务
150
+ openclaw-office service restart # 重启服务
151
+ openclaw-office service log # 查看服务日志
152
+ openclaw-office service log --follow # 实时跟踪日志
153
+ openclaw-office service uninstall # 卸载系统服务
154
+ ```
155
+
156
+ > **提示:** 安装为服务后,也可通过 Settings 页面的「服务管理」面板查看 Gateway 状态和执行重启等操作。
157
+
158
+ ---
159
+
128
160
  ## 快速开始(从源码)
129
161
 
130
162
  ### 1. 安装依赖
@@ -11,6 +11,14 @@ import { networkInterfaces, homedir } from "node:os";
11
11
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
12
12
  const distDir = resolve(__dirname, "..", "dist");
13
13
 
14
+ // --- Service subcommand routing ---
15
+ // If argv[2] is "service", delegate to the service manager module.
16
+ if (process.argv[2] === "service") {
17
+ const { runService } = await import("./service.js");
18
+ await runService();
19
+ process.exit(0);
20
+ }
21
+
14
22
  const MIME_TYPES = {
15
23
  ".html": "text/html; charset=utf-8",
16
24
  ".js": "application/javascript; charset=utf-8",
@@ -79,6 +87,13 @@ function printHelp() {
79
87
  openclaw-office --token my-secret-token
80
88
  openclaw-office --gateway ws://192.168.1.100:18789
81
89
  PORT=3000 openclaw-office
90
+
91
+ \x1b[1mService management:\x1b[0m
92
+ openclaw-office service install --token <token> # Auto-start on login/boot
93
+ openclaw-office service status # Check service status
94
+ openclaw-office service stop # Stop the service
95
+ openclaw-office service uninstall # Remove the service
96
+ openclaw-office service help # Show service help
82
97
  `);
83
98
  }
84
99
 
@@ -0,0 +1,160 @@
1
+ #!/usr/bin/env node
2
+
3
+ // Platform Service — lightweight local HTTP server for managing OpenClaw Gateway lifecycle.
4
+ // Zero external dependencies; uses only Node.js built-in modules.
5
+ // Binds exclusively to 127.0.0.1:18790 for security.
6
+
7
+ import { createServer } from "node:http";
8
+ import { execFile } from "node:child_process";
9
+ import { promisify } from "node:util";
10
+
11
+ const execFileAsync = promisify(execFile);
12
+
13
+ const HOST = "127.0.0.1";
14
+ const PORT = parseInt(process.env.PLATFORM_PORT || "18790", 10);
15
+ const COMMAND_TIMEOUT_MS = 30_000;
16
+
17
+ function findOpenclawBin() {
18
+ const explicit = process.env.OPENCLAW_BIN;
19
+ if (explicit) return explicit;
20
+ return "openclaw";
21
+ }
22
+
23
+ const OPENCLAW_BIN = findOpenclawBin();
24
+
25
+ function corsHeaders() {
26
+ return {
27
+ "Access-Control-Allow-Origin": "*",
28
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
29
+ "Access-Control-Allow-Headers": "Content-Type",
30
+ "Content-Type": "application/json; charset=utf-8",
31
+ };
32
+ }
33
+
34
+ function sendJson(res, statusCode, data) {
35
+ const body = JSON.stringify(data);
36
+ res.writeHead(statusCode, corsHeaders());
37
+ res.end(body);
38
+ }
39
+
40
+ function isLocalRequest(req) {
41
+ const remote = req.socket.remoteAddress;
42
+ return remote === "127.0.0.1" || remote === "::1" || remote === "::ffff:127.0.0.1";
43
+ }
44
+
45
+ async function runCommand(args, timeoutMs = COMMAND_TIMEOUT_MS) {
46
+ try {
47
+ const { stdout, stderr } = await execFileAsync(OPENCLAW_BIN, args, {
48
+ timeout: timeoutMs,
49
+ env: { ...process.env },
50
+ });
51
+ return { ok: true, stdout: stdout.trim(), stderr: stderr.trim() };
52
+ } catch (err) {
53
+ return {
54
+ ok: false,
55
+ stdout: err.stdout?.trim() ?? "",
56
+ stderr: err.stderr?.trim() ?? "",
57
+ message: err.message,
58
+ code: err.code,
59
+ };
60
+ }
61
+ }
62
+
63
+ async function handleServiceStatus() {
64
+ const result = await runCommand(["gateway", "status", "--json"]);
65
+ if (result.ok) {
66
+ try {
67
+ const data = JSON.parse(result.stdout);
68
+ return { ok: true, data };
69
+ } catch {
70
+ return { ok: true, data: { raw: result.stdout } };
71
+ }
72
+ }
73
+ // gateway status may return non-zero when not running — still valid info
74
+ try {
75
+ const data = JSON.parse(result.stdout || result.stderr);
76
+ return { ok: true, data };
77
+ } catch {
78
+ return {
79
+ ok: false,
80
+ error: result.stderr || result.message || "Failed to get status",
81
+ raw: result.stdout,
82
+ };
83
+ }
84
+ }
85
+
86
+ async function handleServiceAction(action) {
87
+ const result = await runCommand(["gateway", action]);
88
+ return {
89
+ ok: result.ok,
90
+ action,
91
+ stdout: result.stdout,
92
+ stderr: result.stderr,
93
+ ...(result.ok ? {} : { error: result.message }),
94
+ };
95
+ }
96
+
97
+ async function handleConfigSetup() {
98
+ const results = [];
99
+ for (const [key, value] of [
100
+ ["gateway.controlUi.dangerouslyDisableDeviceAuth", "true"],
101
+ ["gateway.controlUi.allowInsecureAuth", "true"],
102
+ ]) {
103
+ const r = await runCommand(["config", "set", key, value]);
104
+ results.push({ key, ok: r.ok, stderr: r.stderr });
105
+ }
106
+ const allOk = results.every((r) => r.ok);
107
+ return { ok: allOk, results };
108
+ }
109
+
110
+ const routes = new Map([
111
+ ["GET /api/service/status", handleServiceStatus],
112
+ ["POST /api/service/start", () => handleServiceAction("start")],
113
+ ["POST /api/service/stop", () => handleServiceAction("stop")],
114
+ ["POST /api/service/restart", () => handleServiceAction("restart")],
115
+ ["POST /api/service/install", () => handleServiceAction("install")],
116
+ ["POST /api/service/uninstall", () => handleServiceAction("uninstall")],
117
+ ["POST /api/config/setup", handleConfigSetup],
118
+ ]);
119
+
120
+ const server = createServer(async (req, res) => {
121
+ // CORS preflight
122
+ if (req.method === "OPTIONS") {
123
+ res.writeHead(204, corsHeaders());
124
+ res.end();
125
+ return;
126
+ }
127
+
128
+ // Security: only accept local requests
129
+ if (!isLocalRequest(req)) {
130
+ sendJson(res, 403, { error: "Forbidden: only local connections allowed" });
131
+ return;
132
+ }
133
+
134
+ const url = new URL(req.url || "/", `http://${HOST}:${PORT}`);
135
+ const routeKey = `${req.method} ${url.pathname}`;
136
+
137
+ // Health check
138
+ if (req.method === "GET" && url.pathname === "/api/health") {
139
+ sendJson(res, 200, { ok: true, service: "platform", pid: process.pid });
140
+ return;
141
+ }
142
+
143
+ const handler = routes.get(routeKey);
144
+ if (!handler) {
145
+ sendJson(res, 404, { error: "Not found" });
146
+ return;
147
+ }
148
+
149
+ try {
150
+ const result = await handler();
151
+ sendJson(res, result.ok ? 200 : 500, result);
152
+ } catch (err) {
153
+ sendJson(res, 500, { error: String(err) });
154
+ }
155
+ });
156
+
157
+ server.listen(PORT, HOST, () => {
158
+ console.log(`[platform] listening on http://${HOST}:${PORT}`);
159
+ console.log(`[platform] openclaw bin: ${OPENCLAW_BIN}`);
160
+ });
@@ -0,0 +1,323 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ * Linux systemd service manager for openclaw-office.
5
+ *
6
+ * Manages the OpenClaw Office service via systemd --user.
7
+ * Commands: install, uninstall, start, stop, restart, status, log
8
+ */
9
+
10
+ import { existsSync, mkdirSync, writeFileSync, unlinkSync, readFileSync } from "node:fs";
11
+ import { execSync } from "node:child_process";
12
+ import { homedir } from "node:os";
13
+ import { join } from "node:path";
14
+ import { fileURLToPath } from "node:url";
15
+
16
+ const __dirname = fileURLToPath(new URL(".", import.meta.url));
17
+
18
+ const SYSTEMD_DIR = join(homedir(), ".config", "systemd", "user");
19
+ const SERVICE_NAME = "openclaw-office.service";
20
+ const SERVICE_PATH = join(SYSTEMD_DIR, SERVICE_NAME);
21
+ const NODE_BIN = process.execPath;
22
+ const SERVER_SCRIPT = join(__dirname, "openclaw-office.js");
23
+ const LOG_DIR = join(homedir(), ".local", "state", "openclaw-office");
24
+ const STDOUT_LOG = join(LOG_DIR, "openclaw-office.log");
25
+ const STDERR_LOG = join(LOG_DIR, "openclaw-office-error.log");
26
+
27
+ // --- Colors ---
28
+
29
+ const C = {
30
+ reset: "\x1b[0m",
31
+ bold: "\x1b[1m",
32
+ green: "\x1b[32m",
33
+ red: "\x1b[31m",
34
+ yellow: "\x1b[33m",
35
+ cyan: "\x1b[36m",
36
+ gray: "\x1b[90m",
37
+ };
38
+
39
+ function printLog(msg, color = "") { console.log(`${color}${msg}${C.reset}`); }
40
+ function ok(msg) { printLog(` \u2713 ${msg}`, C.green); }
41
+ function err(msg) { printLog(` \u2717 ${msg}`, C.red); }
42
+ function info(msg) { printLog(` \u2022 ${msg}`, C.cyan); }
43
+ function warn(msg) { printLog(` \u2022 ${msg}`, C.yellow); }
44
+ function dim(msg) { printLog(` ${msg}`, C.gray); }
45
+
46
+ // --- Helpers ---
47
+
48
+ function systemctl(args) {
49
+ try {
50
+ execSync(`systemctl --user ${args}`, { stdio: "pipe" });
51
+ return true;
52
+ } catch {
53
+ return false;
54
+ }
55
+ }
56
+
57
+ function isEnabled() {
58
+ return systemctl(`is-enabled ${SERVICE_NAME}`);
59
+ }
60
+
61
+ function isActive() {
62
+ return systemctl(`is-active ${SERVICE_NAME}`);
63
+ }
64
+
65
+ function generateService(config) {
66
+ const args = [];
67
+ args.push(SERVER_SCRIPT);
68
+ if (config.gatewayUrl) args.push(`--gateway ${config.gatewayUrl}`);
69
+ if (config.port) args.push(`--port ${config.port}`);
70
+ if (config.host) args.push(`--host ${config.host}`);
71
+ if (config.token) args.push(`--token ${config.token}`);
72
+
73
+ return `[Unit]
74
+ Description=OpenClaw Office — Multi-Agent Monitoring Console
75
+ After=network.target
76
+
77
+ [Service]
78
+ Type=simple
79
+ ExecStart=${NODE_BIN} ${args.join(" ")}
80
+ Restart=on-failure
81
+ RestartSec=5
82
+ StandardOutput=append:${STDOUT_LOG}
83
+ StandardError=append:${STDERR_LOG}
84
+ Environment=HOME=${homedir()}
85
+ Environment=PATH=/usr/local/bin:/usr/bin:/bin
86
+ WorkingDirectory=${__dirname}
87
+
88
+ [Install]
89
+ WantedBy=default.target
90
+ `;
91
+ }
92
+
93
+ function generateTimer(config) {
94
+ return `[Unit]
95
+ Description=Restart OpenClaw Office if not running
96
+ [Timer]
97
+ OnBootSec=1min
98
+ OnUnitActiveSec=5min
99
+ [Install]
100
+ WantedBy=timers.target
101
+ `;
102
+ }
103
+
104
+ // --- Commands ---
105
+
106
+ /**
107
+ * @param {{ token: string, gatewayUrl?: string, port?: number, host?: string }} config
108
+ */
109
+ export async function install(config) {
110
+ if (!config.token) {
111
+ err("Token is required for service installation.");
112
+ info("Provide via --token flag or it will be auto-detected from ~/.openclaw/openclaw.json");
113
+ process.exit(1);
114
+ }
115
+
116
+ // Ensure directories
117
+ if (!existsSync(SYSTEMD_DIR)) {
118
+ mkdirSync(SYSTEMD_DIR, { recursive: true });
119
+ }
120
+ if (!existsSync(LOG_DIR)) {
121
+ mkdirSync(LOG_DIR, { recursive: true });
122
+ }
123
+
124
+ // Reload systemd user daemon
125
+ systemctl("daemon-reload");
126
+
127
+ // Stop existing service if active
128
+ if (isActive()) {
129
+ warn("Existing service found, stopping...");
130
+ systemctl(`stop ${SERVICE_NAME}`);
131
+ }
132
+
133
+ // Write service file
134
+ const service = generateService(config);
135
+ writeFileSync(SERVICE_PATH, service, "utf-8");
136
+ ok(`Service file written: ${SERVICE_PATH}`);
137
+
138
+ // Reload and enable
139
+ systemctl("daemon-reload");
140
+ systemctl(`enable ${SERVICE_NAME}`);
141
+ ok("Service enabled");
142
+
143
+ // Start the service
144
+ systemctl(`start ${SERVICE_NAME}`);
145
+ ok("Service started");
146
+
147
+ printLog("");
148
+ ok("OpenClaw Office service installed successfully!");
149
+ printLog("");
150
+ info(`Service file: ${SERVICE_PATH}`);
151
+ info(`Stdout log: ${STDOUT_LOG}`);
152
+ info(`Stderr log: ${STDERR_LOG}`);
153
+ printLog("");
154
+ info("The service will auto-start on boot and restart on failure.");
155
+ dim("To manage: systemctl --user start|stop|restart|status openclaw-office.service");
156
+ dim("To uninstall: openclaw-office service uninstall");
157
+ }
158
+
159
+ export function uninstall() {
160
+ if (!existsSync(SERVICE_PATH)) {
161
+ warn("Service not installed. Nothing to do.");
162
+ return;
163
+ }
164
+
165
+ if (isActive()) {
166
+ info("Stopping running service...");
167
+ systemctl(`stop ${SERVICE_NAME}`);
168
+ ok("Service stopped");
169
+ }
170
+
171
+ systemctl(`disable ${SERVICE_NAME}`);
172
+ systemctl("daemon-reload");
173
+
174
+ try {
175
+ unlinkSync(SERVICE_PATH);
176
+ ok(`Service file removed: ${SERVICE_PATH}`);
177
+ } catch {
178
+ err("Failed to remove service file");
179
+ process.exitCode = 1;
180
+ }
181
+
182
+ systemctl("daemon-reload");
183
+ printLog("");
184
+ ok("OpenClaw Office service uninstalled.");
185
+ }
186
+
187
+ export function start() {
188
+ if (!existsSync(SERVICE_PATH)) {
189
+ err("Service not installed. Run: openclaw-office service install --token <token>");
190
+ process.exit(1);
191
+ }
192
+ if (isActive()) {
193
+ warn("Service is already running.");
194
+ return;
195
+ }
196
+ systemctl(`start ${SERVICE_NAME}`);
197
+ if (isActive()) {
198
+ ok("Service started");
199
+ } else {
200
+ err("Failed to start service. Check logs:");
201
+ dim(`journalctl --user -u ${SERVICE_NAME} --no-pager -n 20`);
202
+ process.exitCode = 1;
203
+ }
204
+ }
205
+
206
+ export function stop() {
207
+ if (!isActive()) {
208
+ warn("Service is not running.");
209
+ return;
210
+ }
211
+ systemctl(`stop ${SERVICE_NAME}`);
212
+ ok("Service stopped");
213
+ }
214
+
215
+ export function restart() {
216
+ if (!existsSync(SERVICE_PATH)) {
217
+ err("Service not installed. Run: openclaw-office service install --token <token>");
218
+ process.exit(1);
219
+ }
220
+ systemctl(`restart ${SERVICE_NAME}`);
221
+ ok("Service restarted");
222
+ }
223
+
224
+ export function status() {
225
+ if (!existsSync(SERVICE_PATH)) {
226
+ warn("Service not installed");
227
+ printLog("");
228
+ dim("Install with: openclaw-office service install --token <token>");
229
+ return;
230
+ }
231
+
232
+ info(`Service file: ${SERVICE_PATH}`);
233
+
234
+ if (isActive()) {
235
+ ok("Status: active (running)");
236
+ } else {
237
+ err("Status: inactive (dead)");
238
+ }
239
+
240
+ if (isEnabled()) {
241
+ ok("Enabled: yes (auto-start on boot)");
242
+ } else {
243
+ warn("Enabled: no");
244
+ }
245
+
246
+ // Show recent journal entries
247
+ try {
248
+ const journal = execSync(
249
+ `journalctl --user -u ${SERVICE_NAME} --no-pager -n 10 2>/dev/null || echo "(journalctl not available)"`,
250
+ { stdio: "pipe" }
251
+ ).toString();
252
+ if (journal.trim()) {
253
+ printLog("");
254
+ info("Recent journal entries:");
255
+ dim(journal.trim());
256
+ }
257
+ } catch { /* ok */ }
258
+ }
259
+
260
+ export function showLogs(follow = false) {
261
+ // Try journalctl first, fall back to file
262
+ try {
263
+ if (follow) {
264
+ execSync(`journalctl --user -u ${SERVICE_NAME} -f 2>/dev/null`, { stdio: "inherit" });
265
+ return;
266
+ } else {
267
+ const output = execSync(
268
+ `journalctl --user -u ${SERVICE_NAME} --no-pager -n 100 2>/dev/null`,
269
+ { stdio: "pipe" }
270
+ ).toString();
271
+ if (output.trim()) {
272
+ console.log(output.trim());
273
+ return;
274
+ }
275
+ }
276
+ } catch { /* fall back to file */ }
277
+
278
+ // Fall back to file-based log
279
+ if (!existsSync(STDOUT_LOG)) {
280
+ warn("No log file found.");
281
+ return;
282
+ }
283
+ try {
284
+ if (follow) {
285
+ execSync(`tail -f "${STDOUT_LOG}"`, { stdio: "inherit" });
286
+ } else {
287
+ const content = readFileSync(STDOUT_LOG, "utf-8");
288
+ console.log(content);
289
+ }
290
+ } catch (e) {
291
+ err("Failed to read log file");
292
+ }
293
+ }
294
+
295
+ export function printHelp() {
296
+ console.log(`
297
+ ${C.cyan}OpenClaw Office — Service Management (Linux)${C.reset}
298
+
299
+ ${C.bold}Usage:${C.reset}
300
+ openclaw-office service <command> [options]
301
+
302
+ ${C.bold}Commands:${C.reset}
303
+ install Install as a systemd --user service (auto-start on boot)
304
+ uninstall Remove the systemd service
305
+ start Start the service
306
+ stop Stop the service
307
+ restart Restart the service
308
+ status Show service status
309
+ log Show service logs (add --follow to tail)
310
+
311
+ ${C.bold}Install options:${C.reset}
312
+ --token <token> Gateway auth token (required)
313
+ --gateway <url> Gateway WebSocket URL
314
+ --port <port> Server port (default: 5180)
315
+ --host <host> Bind address (default: 0.0.0.0)
316
+
317
+ ${C.bold}Examples:${C.reset}
318
+ openclaw-office service install --token my-token
319
+ openclaw-office service install --token my-token --port 3000
320
+ openclaw-office service status
321
+ openclaw-office service log --follow
322
+ `);
323
+ }