@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 +32 -0
- package/README.md +32 -0
- package/bin/openclaw-office.js +15 -0
- package/bin/platform.js +160 -0
- package/bin/service-linux.js +323 -0
- package/bin/service-macos.js +298 -0
- package/bin/service.js +212 -0
- package/dist/assets/{ActivityHeatmap-BVgzAYW-.js → ActivityHeatmap-BVAmBnn5.js} +1 -1
- package/dist/assets/{CostPieChart-Dg4fOesa.js → CostPieChart-DB3TDR5m.js} +2 -2
- package/dist/assets/{NetworkGraph-CPH0c9B4.js → NetworkGraph-DJLn-4T4.js} +1 -1
- package/dist/assets/{TokenLineChart-C6Owrh9O.js → TokenLineChart-Cr_ZVTqu.js} +1 -1
- package/dist/assets/{generateCategoricalChart-P5eTV1iF.js → generateCategoricalChart-CtpJIFZ4.js} +1 -1
- package/dist/assets/index-B40qDa_1.css +1 -0
- package/dist/assets/index-Dc4yBvYP.js +497 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/dist/assets/index-BjE9oQsT.js +0 -482
- package/dist/assets/index-BpCZg7Ed.css +0 -1
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. 安装依赖
|
package/bin/openclaw-office.js
CHANGED
|
@@ -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
|
|
package/bin/platform.js
ADDED
|
@@ -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
|
+
}
|