openclaw-cn-termux 0.2.0 → 0.2.1-beta.0
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/CHANGELOG.md +4 -0
- package/README.md +63 -5
- package/dist/build-info.json +3 -3
- package/dist/control-ui/gateway-host-fix.js +83 -0
- package/dist/control-ui/index.html +1 -0
- package/dist/daemon/proot.js +7 -4
- package/dist/daemon/service.js +30 -1
- package/dist/daemon/termux-sv.js +302 -0
- package/dist/gateway/server-http.js +16 -0
- package/dist/infra/clipboard-stub.js +18 -0
- package/dist/infra/clipboard.js +1 -0
- package/dist/logging/logger.js +8 -2
- package/dist/wizard/onboarding.finalize.js +1 -7
- package/package.json +3 -2
- package/patches/@mariozechner__clipboard.patch +58 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,10 @@ Docs: https://clawd.org.cn/
|
|
|
4
4
|
|
|
5
5
|
## 0.2.0
|
|
6
6
|
|
|
7
|
+
### Bug 修复
|
|
8
|
+
|
|
9
|
+
- **修复公网 IP 直连 Control UI 时 WebSocket 断开 (1006)** (#547, thanks @Yogdunana):通过 `http://<公网IP>:端口` 访问控制台时,浏览器 `Origin` 与 CSWSH 白名单不匹配导致 WebSocket 升级返回 403。修复:`isValidWebSocketOrigin()` 在 `Origin` 与请求头 `Host` 同源时放行;新增 `gateway-host-fix.js` 前置脚本,按需纠正 `localStorage` 中过期的 `gatewayUrl`。
|
|
10
|
+
|
|
7
11
|
### 新增功能
|
|
8
12
|
|
|
9
13
|
- **新增 ePhone AI 模型供应商**:在模型配置向导中新增 [ePhone AI](https://platform.ephone.ai) 聚合平台,置顶为默认推荐供应商。兼容 OpenAI 协议(`https://api.ephone.ai/v1`),预设 claude-sonnet-4-6、claude-opus-4-6、MiniMax-M2.7、gpt-5.4、kimi-k2.5 五个可选模型,同时支持手动输入任意模型 ID。配置文档:https://clawd.org.cn/providers/ephone.html
|
package/README.md
CHANGED
|
@@ -56,6 +56,12 @@
|
|
|
56
56
|
curl -fsSL https://clawd.org.cn/install.sh | bash
|
|
57
57
|
```
|
|
58
58
|
|
|
59
|
+
**Termux 原生环境(非 proot 环境):**
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
curl -fsSL https://raw.githubusercontent.com/byteuser1977/termux-install-openclaw/main/scripts/install-termux-native.sh | bash
|
|
63
|
+
```
|
|
64
|
+
|
|
59
65
|
**Termux(推荐在 Termux 中通过 proot-distro 运行 Ubuntu):**
|
|
60
66
|
|
|
61
67
|
```bash
|
|
@@ -96,10 +102,11 @@ pnpm openclaw-termux onboard --install-daemon
|
|
|
96
102
|
|
|
97
103
|
**一键安装(推荐):**
|
|
98
104
|
|
|
99
|
-
| 环境
|
|
100
|
-
|
|
|
101
|
-
| Termux | `curl -fsSL https://raw.githubusercontent.com/byteuser1977/termux-install-openclaw/main/scripts/install-
|
|
102
|
-
|
|
|
105
|
+
| 环境 | 一键安装命令 |
|
|
106
|
+
| ------------ | ---------------------------------------------------------------------------------------------------------------------------- |
|
|
107
|
+
| Termux 原生 | `curl -fsSL https://raw.githubusercontent.com/byteuser1977/termux-install-openclaw/main/scripts/install-termux-native.sh \| bash` |
|
|
108
|
+
| Termux (Proot) | `curl -fsSL https://raw.githubusercontent.com/byteuser1977/termux-install-openclaw/main/scripts/install-ubuntu.sh \| bash` |
|
|
109
|
+
| AidLux | `curl -fsSL https://raw.githubusercontent.com/byteuser1977/termux-install-openclaw/main/scripts/install-aidlux.sh \| bash` |
|
|
103
110
|
|
|
104
111
|
**手动安装步骤:**
|
|
105
112
|
|
|
@@ -119,7 +126,54 @@ openclaw-termux gateway run
|
|
|
119
126
|
|
|
120
127
|
详细文档:[Termux 完整安装手册](https://clawd.org.cn/docs/installation) · [AidLux 安装手册](https://clawd.org.cn/docs/aidlux-installation)
|
|
121
128
|
|
|
122
|
-
##
|
|
129
|
+
## � Termux 原生环境安装说明
|
|
130
|
+
|
|
131
|
+
本项目已支持 Termux 原生环境(非 proot)下的服务注册和管理,使用 Termux 的 runit 服务管理器。
|
|
132
|
+
|
|
133
|
+
### 安装要求
|
|
134
|
+
|
|
135
|
+
- Termux 版本 ≥ 0.118.0
|
|
136
|
+
- Node.js ≥ 22
|
|
137
|
+
- `sv` 命令可用(安装 `termux-services` 包)
|
|
138
|
+
|
|
139
|
+
```bash
|
|
140
|
+
pkg update && pkg install nodejs termux-services
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### 服务管理
|
|
144
|
+
|
|
145
|
+
安装后,网关服务将自动注册为 Termux runit 服务,可通过以下命令管理:
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
# 查看服务状态
|
|
149
|
+
sv status openclaw-gateway
|
|
150
|
+
|
|
151
|
+
# 启动服务
|
|
152
|
+
sv up openclaw-gateway
|
|
153
|
+
|
|
154
|
+
# 停止服务
|
|
155
|
+
sv down openclaw-gateway
|
|
156
|
+
|
|
157
|
+
# 重启服务
|
|
158
|
+
sv restart openclaw-gateway
|
|
159
|
+
|
|
160
|
+
# 查看服务日志
|
|
161
|
+
svlogd -tt ~/.termux/runit/openclaw-gateway-log
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### 与 Proot Termux 的区别
|
|
165
|
+
|
|
166
|
+
| 特性 | Termux 原生 (runit) | Proot Termux (init.d) |
|
|
167
|
+
| -------------- | ------------------- | --------------------- |
|
|
168
|
+
| 服务管理器 | runit (sv) | init.d 脚本 |
|
|
169
|
+
| 服务目录 | `~/.termux/runit/` | `~/.init.d/` |
|
|
170
|
+
| 日志管理 | svlogd | nohup + 日志文件 |
|
|
171
|
+
| 系统集成 | 更原生 | 兼容模式 |
|
|
172
|
+
| 推荐场景 | 纯 Termux 环境 | proot-distro 等环境 |
|
|
173
|
+
|
|
174
|
+
详细文档:[Termux 原生环境安装指南](https://clawd.org.cn/docs/installation/termux-native)
|
|
175
|
+
|
|
176
|
+
## �🔧 配置
|
|
123
177
|
|
|
124
178
|
配置文件位于 `~/.openclaw/` 目录(桌面端)或 `~/.openclaw-cn-termux/` 目录(Termux)。
|
|
125
179
|
|
|
@@ -175,6 +229,10 @@ openclaw-termux gateway run
|
|
|
175
229
|
<a href="https://github.com/yanghua"><img src="https://avatars.githubusercontent.com/yanghua?v=4" width="48" height="48" alt="yanghua" /></a>
|
|
176
230
|
<a href="https://github.com/qqdxyg"><img src="https://avatars.githubusercontent.com/qqdxyg?v=4" width="48" height="48" alt="qqdxyg" /></a>
|
|
177
231
|
<a href="https://github.com/ddupg"><img src="https://avatars.githubusercontent.com/ddupg?v=4" width="48" height="48" alt="ddupg" /></a>
|
|
232
|
+
<a href="https://github.com/Daiyimo"><img src="https://avatars.githubusercontent.com/Daiyimo?v=4" width="48" height="48" alt="Daiyimo" /></a>
|
|
233
|
+
<a href="https://github.com/yebohong02"><img src="https://avatars.githubusercontent.com/yebohong02?v=4" width="48" height="48" alt="yebohong02" /></a>
|
|
234
|
+
<a href="https://github.com/lvjianchaos"><img src="https://avatars.githubusercontent.com/lvjianchaos?v=4" width="48" height="48" alt="lvjianchaos" /></a>
|
|
235
|
+
<a href="https://github.com/Yogdunana"><img src="https://avatars.githubusercontent.com/Yogdunana?v=4" width="48" height="48" alt="Yogdunana" /></a>
|
|
178
236
|
</p>
|
|
179
237
|
|
|
180
238
|
## 📋 开发计划
|
package/dist/build-info.json
CHANGED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
(function () {
|
|
2
|
+
try {
|
|
3
|
+
var key = "clawdbot.control.settings.v1";
|
|
4
|
+
var params = new URLSearchParams(window.location.search);
|
|
5
|
+
var raw = localStorage.getItem(key);
|
|
6
|
+
var o = raw ? JSON.parse(raw) : {};
|
|
7
|
+
var changed = false;
|
|
8
|
+
var strip = false;
|
|
9
|
+
|
|
10
|
+
var tok = params.get("token");
|
|
11
|
+
if (tok != null) {
|
|
12
|
+
var tt = tok.trim();
|
|
13
|
+
if (tt) {
|
|
14
|
+
o.token = tt;
|
|
15
|
+
changed = true;
|
|
16
|
+
}
|
|
17
|
+
params.delete("token");
|
|
18
|
+
strip = true;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
var explicitGatewayFromQuery = false;
|
|
22
|
+
var gu = params.get("gatewayUrl");
|
|
23
|
+
if (gu != null) {
|
|
24
|
+
var gg = gu.trim();
|
|
25
|
+
if (gg) {
|
|
26
|
+
// Only accept gatewayUrl that points to the same host (prevent open-redirect to attacker WS)
|
|
27
|
+
try {
|
|
28
|
+
var parsed = new URL(gg);
|
|
29
|
+
if (parsed.hostname === location.hostname) {
|
|
30
|
+
o.gatewayUrl = gg;
|
|
31
|
+
explicitGatewayFromQuery = true;
|
|
32
|
+
changed = true;
|
|
33
|
+
}
|
|
34
|
+
} catch (_) { /* ignore malformed URL */ }
|
|
35
|
+
}
|
|
36
|
+
params.delete("gatewayUrl");
|
|
37
|
+
strip = true;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
var sess = params.get("session");
|
|
41
|
+
if (sess != null) {
|
|
42
|
+
var sk = sess.trim();
|
|
43
|
+
if (sk) {
|
|
44
|
+
o.sessionKey = sk;
|
|
45
|
+
o.lastActiveSessionKey = sk;
|
|
46
|
+
changed = true;
|
|
47
|
+
}
|
|
48
|
+
params.delete("session");
|
|
49
|
+
strip = true;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
var host = location.hostname;
|
|
53
|
+
if (!explicitGatewayFromQuery && host !== "localhost" && host !== "127.0.0.1") {
|
|
54
|
+
var wsUrl = (location.protocol === "https:" ? "wss" : "ws") + "://" + location.host;
|
|
55
|
+
var g = (o.gatewayUrl && String(o.gatewayUrl).trim()) || "";
|
|
56
|
+
var needsFix = !g || /127\.0\.0\.1|localhost/.test(g);
|
|
57
|
+
if (!needsFix && g) {
|
|
58
|
+
try {
|
|
59
|
+
needsFix = new URL(g).host !== location.host;
|
|
60
|
+
} catch {
|
|
61
|
+
needsFix = true;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (needsFix) {
|
|
65
|
+
o.gatewayUrl = wsUrl;
|
|
66
|
+
changed = true;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (changed) {
|
|
71
|
+
localStorage.setItem(key, JSON.stringify(o));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (strip) {
|
|
75
|
+
var next = params.toString();
|
|
76
|
+
var u = new URL(window.location.href);
|
|
77
|
+
u.search = next;
|
|
78
|
+
window.history.replaceState({}, "", u.toString());
|
|
79
|
+
}
|
|
80
|
+
} catch (e) {
|
|
81
|
+
console.warn("[gateway-host-fix]", e);
|
|
82
|
+
}
|
|
83
|
+
})();
|
package/dist/daemon/proot.js
CHANGED
|
@@ -87,10 +87,7 @@ export function isRunningInAidlux() {
|
|
|
87
87
|
}
|
|
88
88
|
return false;
|
|
89
89
|
}
|
|
90
|
-
export function
|
|
91
|
-
if (isRunningInProotDistro() || isRunningInAidlux()) {
|
|
92
|
-
return true;
|
|
93
|
-
}
|
|
90
|
+
export function isRunningInPureTermux() {
|
|
94
91
|
const isTermux = Boolean(process.env.TERMUX_VERSION ||
|
|
95
92
|
process.env.TERMUX_MAIN_PACKAGE_FORMAT ||
|
|
96
93
|
process.env.TERMUX_PREFIX ||
|
|
@@ -98,3 +95,9 @@ export function isRunningInTermux() {
|
|
|
98
95
|
process.env.ANDROID_ROOT?.includes("com.termux"));
|
|
99
96
|
return isTermux;
|
|
100
97
|
}
|
|
98
|
+
export function isRunningInTermux() {
|
|
99
|
+
if (isRunningInProotDistro() || isRunningInAidlux()) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
return isRunningInPureTermux();
|
|
103
|
+
}
|
package/dist/daemon/service.js
CHANGED
|
@@ -2,7 +2,8 @@ import { installLaunchAgent, isLaunchAgentLoaded, readLaunchAgentProgramArgument
|
|
|
2
2
|
import { installScheduledTask, isScheduledTaskInstalled, readScheduledTaskCommand, readScheduledTaskRuntime, restartScheduledTask, stopScheduledTask, uninstallScheduledTask, } from "./schtasks.js";
|
|
3
3
|
import { installSystemdService, isSystemdServiceEnabled, readSystemdServiceExecStart, readSystemdServiceRuntime, restartSystemdService, stopSystemdService, uninstallSystemdService, } from "./systemd.js";
|
|
4
4
|
import { installInitdService, uninstallInitdService, stopInitdService, restartInitdService, isInitdServiceEnabled, readInitdServiceCommand, readInitdServiceRuntime, } from "./initd.js";
|
|
5
|
-
import {
|
|
5
|
+
import { installTermuxSvService, uninstallTermuxSvService, stopTermuxSvService, restartTermuxSvService, isTermuxSvServiceEnabled, readTermuxSvServiceCommand, readTermuxSvServiceRuntime, } from "./termux-sv.js";
|
|
6
|
+
import { isRunningInPureTermux, isRunningInTermux } from "./proot.js";
|
|
6
7
|
export function resolveGatewayService() {
|
|
7
8
|
if (process.platform === "darwin") {
|
|
8
9
|
return {
|
|
@@ -33,6 +34,34 @@ export function resolveGatewayService() {
|
|
|
33
34
|
};
|
|
34
35
|
}
|
|
35
36
|
if (process.platform === "linux") {
|
|
37
|
+
if (isRunningInPureTermux()) {
|
|
38
|
+
return {
|
|
39
|
+
label: "termux-sv",
|
|
40
|
+
loadedText: "installed",
|
|
41
|
+
notLoadedText: "not installed",
|
|
42
|
+
install: async (args) => {
|
|
43
|
+
await installTermuxSvService(args);
|
|
44
|
+
},
|
|
45
|
+
uninstall: async (args) => {
|
|
46
|
+
await uninstallTermuxSvService(args);
|
|
47
|
+
},
|
|
48
|
+
stop: async (args) => {
|
|
49
|
+
await stopTermuxSvService({
|
|
50
|
+
stdout: args.stdout,
|
|
51
|
+
env: args.env,
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
restart: async (args) => {
|
|
55
|
+
await restartTermuxSvService({
|
|
56
|
+
stdout: args.stdout,
|
|
57
|
+
env: args.env,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
isLoaded: async (args) => isTermuxSvServiceEnabled(args),
|
|
61
|
+
readCommand: readTermuxSvServiceCommand,
|
|
62
|
+
readRuntime: async (env) => await readTermuxSvServiceRuntime(env),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
36
65
|
if (isRunningInTermux()) {
|
|
37
66
|
return {
|
|
38
67
|
label: "initd",
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import fs from "node:fs/promises";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { colorize, isRich, theme } from "../terminal/theme.js";
|
|
5
|
+
import { formatGatewayServiceDescription } from "./constants.js";
|
|
6
|
+
import { resolveHomeDir } from "./paths.js";
|
|
7
|
+
const toPosixPath = (value) => value.replace(/\\/g, "/");
|
|
8
|
+
const formatLine = (label, value) => {
|
|
9
|
+
const rich = isRich();
|
|
10
|
+
return `${colorize(rich, theme.muted, `${label}:`)} ${colorize(rich, theme.command, value)}`;
|
|
11
|
+
};
|
|
12
|
+
function resolveServiceDir(env) {
|
|
13
|
+
const home = toPosixPath(resolveHomeDir(env));
|
|
14
|
+
return path.posix.join(home, ".termux", "runit", "openclaw-gateway");
|
|
15
|
+
}
|
|
16
|
+
function resolveServiceLogDir(env) {
|
|
17
|
+
const home = toPosixPath(resolveHomeDir(env));
|
|
18
|
+
return path.posix.join(home, ".termux", "runit", "openclaw-gateway-log");
|
|
19
|
+
}
|
|
20
|
+
function buildServiceRunScript({ description, programArguments, workingDirectory, environment, }) {
|
|
21
|
+
const envVars = Object.entries(environment || {})
|
|
22
|
+
.map(([key, value]) => `export ${key}="${value || ""}"`)
|
|
23
|
+
.join("\n");
|
|
24
|
+
const cmd = programArguments.map((arg) => `"${arg}"`).join(" ");
|
|
25
|
+
const workDir = workingDirectory || "$(dirname \"$0\")";
|
|
26
|
+
return `#!/bin/sh
|
|
27
|
+
# OpenClaw Gateway Service (Termux runit)
|
|
28
|
+
# Description: ${description}
|
|
29
|
+
|
|
30
|
+
cd "${workDir}"
|
|
31
|
+
${envVars}
|
|
32
|
+
exec ${cmd}
|
|
33
|
+
`;
|
|
34
|
+
}
|
|
35
|
+
function buildServiceLogRunScript({ description, logFile, }) {
|
|
36
|
+
return `#!/bin/sh
|
|
37
|
+
# OpenClaw Gateway Service Log (Termux runit)
|
|
38
|
+
# Description: ${description}
|
|
39
|
+
|
|
40
|
+
exec svlogd -tt "${logFile}"
|
|
41
|
+
`;
|
|
42
|
+
}
|
|
43
|
+
async function executeCommand(command, args, options) {
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
execFile(command, args, options ?? { encoding: "utf-8" }, (error, stdout) => {
|
|
46
|
+
if (error) {
|
|
47
|
+
reject(error);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
resolve(stdout?.toString() ?? "");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
async function assertRunitAvailable() {
|
|
56
|
+
try {
|
|
57
|
+
await executeCommand("sv", ["--version"], {
|
|
58
|
+
encoding: "utf8",
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
throw new Error("Termux runit (sv) is not available. Please install termux-services package.");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export async function installTermuxSvService({ env, stdout, programArguments, workingDirectory, environment, description, }) {
|
|
66
|
+
await assertRunitAvailable();
|
|
67
|
+
const serviceDir = resolveServiceDir(env);
|
|
68
|
+
const logDir = resolveServiceLogDir(env);
|
|
69
|
+
const logFile = path.posix.join(serviceDir, "main", "log");
|
|
70
|
+
await fs.mkdir(serviceDir, { recursive: true });
|
|
71
|
+
await fs.mkdir(path.join(serviceDir, "main"), { recursive: true });
|
|
72
|
+
await fs.mkdir(logDir, { recursive: true });
|
|
73
|
+
const serviceDescription = description ??
|
|
74
|
+
formatGatewayServiceDescription({
|
|
75
|
+
profile: env.OPENCLAW_PROFILE,
|
|
76
|
+
version: environment?.OPENCLAW_SERVICE_VERSION ?? env.OPENCLAW_SERVICE_VERSION,
|
|
77
|
+
});
|
|
78
|
+
const runScript = buildServiceRunScript({
|
|
79
|
+
description: serviceDescription,
|
|
80
|
+
programArguments,
|
|
81
|
+
workingDirectory,
|
|
82
|
+
environment,
|
|
83
|
+
});
|
|
84
|
+
const logRunScript = buildServiceLogRunScript({
|
|
85
|
+
description: serviceDescription,
|
|
86
|
+
logFile,
|
|
87
|
+
});
|
|
88
|
+
await fs.writeFile(path.join(serviceDir, "run"), runScript, "utf8");
|
|
89
|
+
await fs.chmod(path.join(serviceDir, "run"), 0o755);
|
|
90
|
+
await fs.writeFile(path.join(logDir, "run"), logRunScript, "utf8");
|
|
91
|
+
await fs.chmod(path.join(logDir, "run"), 0o755);
|
|
92
|
+
stdout.write(`${formatLine("Installed Termux runit service", serviceDir)}\n`);
|
|
93
|
+
stdout.write(`${formatLine("Log directory", logDir)}\n`);
|
|
94
|
+
try {
|
|
95
|
+
await executeCommand("sv", ["enable", "openclaw-gateway"], {
|
|
96
|
+
encoding: "utf8",
|
|
97
|
+
});
|
|
98
|
+
stdout.write(`${formatLine("Enabled service", "openclaw-gateway")}\n`);
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
stdout.write(`Warning: Failed to enable service via sv: ${String(error)}\n`);
|
|
102
|
+
stdout.write("You may need to manually link the service to runsvdir\n");
|
|
103
|
+
}
|
|
104
|
+
try {
|
|
105
|
+
await executeCommand("sv", ["up", "openclaw-gateway"], {
|
|
106
|
+
encoding: "utf8",
|
|
107
|
+
});
|
|
108
|
+
stdout.write(`${formatLine("Started service", "openclaw-gateway")}\n`);
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
stdout.write(`Warning: Failed to start service via sv: ${String(error)}\n`);
|
|
112
|
+
}
|
|
113
|
+
return { serviceDir, logDir };
|
|
114
|
+
}
|
|
115
|
+
export async function uninstallTermuxSvService({ env, stdout, }) {
|
|
116
|
+
await assertRunitAvailable();
|
|
117
|
+
const serviceDir = resolveServiceDir(env);
|
|
118
|
+
const logDir = resolveServiceLogDir(env);
|
|
119
|
+
try {
|
|
120
|
+
await executeCommand("sv", ["down", "openclaw-gateway"], {
|
|
121
|
+
encoding: "utf8",
|
|
122
|
+
});
|
|
123
|
+
stdout.write(`${formatLine("Stopped service", "openclaw-gateway")}\n`);
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
stdout.write("Service not running\n");
|
|
127
|
+
}
|
|
128
|
+
try {
|
|
129
|
+
await executeCommand("sv", ["disable", "openclaw-gateway"], {
|
|
130
|
+
encoding: "utf8",
|
|
131
|
+
});
|
|
132
|
+
stdout.write(`${formatLine("Disabled service", "openclaw-gateway")}\n`);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
stdout.write("Service not enabled\n");
|
|
136
|
+
}
|
|
137
|
+
try {
|
|
138
|
+
await fs.unlink(path.join(serviceDir, "run"));
|
|
139
|
+
stdout.write(`${formatLine("Removed service script", path.join(serviceDir, "run"))}\n`);
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
stdout.write(`Service script not found at ${path.join(serviceDir, "run")}\n`);
|
|
143
|
+
}
|
|
144
|
+
try {
|
|
145
|
+
await fs.rm(serviceDir, { recursive: true });
|
|
146
|
+
stdout.write(`${formatLine("Removed service directory", serviceDir)}\n`);
|
|
147
|
+
}
|
|
148
|
+
catch {
|
|
149
|
+
stdout.write(`Service directory not found at ${serviceDir}\n`);
|
|
150
|
+
}
|
|
151
|
+
try {
|
|
152
|
+
await fs.unlink(path.join(logDir, "run"));
|
|
153
|
+
stdout.write(`${formatLine("Removed log script", path.join(logDir, "run"))}\n`);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
stdout.write(`Log script not found at ${path.join(logDir, "run")}\n`);
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
await fs.rm(logDir, { recursive: true });
|
|
160
|
+
stdout.write(`${formatLine("Removed log directory", logDir)}\n`);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
stdout.write(`Log directory not found at ${logDir}\n`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
export async function stopTermuxSvService({ stdout, env, }) {
|
|
167
|
+
await assertRunitAvailable();
|
|
168
|
+
try {
|
|
169
|
+
const result = await executeCommand("sv", ["down", "openclaw-gateway"], {
|
|
170
|
+
encoding: "utf8",
|
|
171
|
+
});
|
|
172
|
+
stdout.write(`${result}\n`);
|
|
173
|
+
stdout.write(`${formatLine("Stopped service", "openclaw-gateway")}\n`);
|
|
174
|
+
}
|
|
175
|
+
catch (error) {
|
|
176
|
+
stdout.write(`Failed to stop service: ${String(error)}\n`);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
export async function restartTermuxSvService({ stdout, env, }) {
|
|
180
|
+
await assertRunitAvailable();
|
|
181
|
+
try {
|
|
182
|
+
const result = await executeCommand("sv", ["restart", "openclaw-gateway"], {
|
|
183
|
+
encoding: "utf8",
|
|
184
|
+
});
|
|
185
|
+
stdout.write(`${result}\n`);
|
|
186
|
+
stdout.write(`${formatLine("Restarted service", "openclaw-gateway")}\n`);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
stdout.write(`Failed to restart service: ${String(error)}\n`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
export async function isTermuxSvServiceEnabled(args) {
|
|
193
|
+
const serviceDir = resolveServiceDir(args.env ?? {});
|
|
194
|
+
try {
|
|
195
|
+
await fs.access(path.join(serviceDir, "run"));
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
198
|
+
catch {
|
|
199
|
+
return false;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
export async function readTermuxSvServiceRuntime(env = process.env) {
|
|
203
|
+
const serviceDir = resolveServiceDir(env);
|
|
204
|
+
try {
|
|
205
|
+
await fs.access(path.join(serviceDir, "run"));
|
|
206
|
+
}
|
|
207
|
+
catch {
|
|
208
|
+
return {
|
|
209
|
+
status: "stopped",
|
|
210
|
+
detail: "Termux runit service not found",
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
try {
|
|
214
|
+
const result = await executeCommand("sv", ["status", "openclaw-gateway"], {
|
|
215
|
+
encoding: "utf8",
|
|
216
|
+
});
|
|
217
|
+
const match = result.match(/runsvdir: (.+): .+ \((\d+)\)/);
|
|
218
|
+
if (match) {
|
|
219
|
+
const status = match[1].trim();
|
|
220
|
+
const pid = parseInt(match[2], 10);
|
|
221
|
+
if (status === "up") {
|
|
222
|
+
return {
|
|
223
|
+
status: "running",
|
|
224
|
+
pid,
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
else if (status === "down") {
|
|
228
|
+
return {
|
|
229
|
+
status: "stopped",
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
status: "unknown",
|
|
235
|
+
detail: result.trim(),
|
|
236
|
+
};
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
return {
|
|
240
|
+
status: "unknown",
|
|
241
|
+
detail: String(error),
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
export async function readTermuxSvServiceCommand(env) {
|
|
246
|
+
const serviceDir = resolveServiceDir(env);
|
|
247
|
+
const runScriptPath = path.join(serviceDir, "run");
|
|
248
|
+
try {
|
|
249
|
+
const content = await fs.readFile(runScriptPath, "utf8");
|
|
250
|
+
let cmdLine = "";
|
|
251
|
+
let workingDirectory = "";
|
|
252
|
+
const environment = {};
|
|
253
|
+
for (const rawLine of content.split("\n")) {
|
|
254
|
+
const line = rawLine.trim();
|
|
255
|
+
if (line.startsWith("export ")) {
|
|
256
|
+
const match = line.match(/export (\w+)="(.*)"/);
|
|
257
|
+
if (match) {
|
|
258
|
+
environment[match[1]] = match[2];
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
else if (line.startsWith("exec ")) {
|
|
262
|
+
cmdLine = line.slice("exec ".length).trim();
|
|
263
|
+
}
|
|
264
|
+
else if (line.startsWith("cd ")) {
|
|
265
|
+
workingDirectory = line.slice("cd ".length).trim();
|
|
266
|
+
workingDirectory = workingDirectory.replace(/^"|"$/g, "");
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
if (!cmdLine)
|
|
270
|
+
return null;
|
|
271
|
+
const programArguments = [];
|
|
272
|
+
let currentArg = "";
|
|
273
|
+
let inQuotes = false;
|
|
274
|
+
for (let i = 0; i < cmdLine.length; i++) {
|
|
275
|
+
const char = cmdLine[i];
|
|
276
|
+
if (char === '"') {
|
|
277
|
+
inQuotes = !inQuotes;
|
|
278
|
+
}
|
|
279
|
+
else if (char === " " && !inQuotes) {
|
|
280
|
+
if (currentArg) {
|
|
281
|
+
programArguments.push(currentArg);
|
|
282
|
+
currentArg = "";
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
else {
|
|
286
|
+
currentArg += char;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
if (currentArg) {
|
|
290
|
+
programArguments.push(currentArg);
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
programArguments,
|
|
294
|
+
...(workingDirectory ? { workingDirectory } : {}),
|
|
295
|
+
...(Object.keys(environment).length > 0 ? { environment } : {}),
|
|
296
|
+
sourcePath: runScriptPath,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return null;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
@@ -351,6 +351,7 @@ export function createGatewayHttpServer(opts) {
|
|
|
351
351
|
* - Tailscale origins (*.ts.net domains, 100.64.0.0/10 IPv4, fd7a:115c:a1e0::/48 IPv6) are allowed
|
|
352
352
|
* - Null origin (file:// protocol, privacy mode) is allowed for local clients
|
|
353
353
|
* - Empty origin (non-browser clients) is allowed
|
|
354
|
+
* - Same Host as Origin (e.g. Control UI at http://PUBLIC_IP:18789) is allowed (CSWSH-safe)
|
|
354
355
|
* - All other origins are rejected
|
|
355
356
|
*/
|
|
356
357
|
function isValidWebSocketOrigin(req) {
|
|
@@ -383,6 +384,21 @@ function isValidWebSocketOrigin(req) {
|
|
|
383
384
|
return true;
|
|
384
385
|
if (isTailnetIPv6(hostname))
|
|
385
386
|
return true;
|
|
387
|
+
// Allow Origin matching the request Host header (e.g. Control UI served from
|
|
388
|
+
// the gateway itself at http://PUBLIC_IP:18789). This is safe against CSWSH
|
|
389
|
+
// because the browser sets both Origin and Host; an attacker page on a
|
|
390
|
+
// different origin cannot forge the Host header to match its own Origin.
|
|
391
|
+
// Note: behind a misconfigured reverse proxy the Host header could be
|
|
392
|
+
// attacker-controlled — but SECURITY.md already advises against public
|
|
393
|
+
// exposure without a trusted proxy + gateway auth.
|
|
394
|
+
const hostHeader = (req.headers.host ?? "").trim();
|
|
395
|
+
if (hostHeader) {
|
|
396
|
+
const ol = origin.toLowerCase();
|
|
397
|
+
const hh = hostHeader.toLowerCase();
|
|
398
|
+
if (ol === `http://${hh}` || ol === `https://${hh}`) {
|
|
399
|
+
return true;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
386
402
|
return false;
|
|
387
403
|
}
|
|
388
404
|
catch {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export const availableFormats = [];
|
|
2
|
+
export const getText = () => Promise.resolve("");
|
|
3
|
+
export const setText = () => Promise.resolve();
|
|
4
|
+
export const hasText = () => Promise.resolve(false);
|
|
5
|
+
export const getImageBinary = () => Promise.resolve(null);
|
|
6
|
+
export const getImageBase64 = () => Promise.resolve("");
|
|
7
|
+
export const setImageBinary = () => Promise.resolve();
|
|
8
|
+
export const setImageBase64 = () => Promise.resolve();
|
|
9
|
+
export const hasImage = () => Promise.resolve(false);
|
|
10
|
+
export const getHtml = () => Promise.resolve("");
|
|
11
|
+
export const setHtml = () => Promise.resolve();
|
|
12
|
+
export const hasHtml = () => Promise.resolve(false);
|
|
13
|
+
export const getRtf = () => Promise.resolve("");
|
|
14
|
+
export const setRtf = () => Promise.resolve();
|
|
15
|
+
export const hasRtf = () => Promise.resolve(false);
|
|
16
|
+
export const clear = () => Promise.resolve();
|
|
17
|
+
export const watch = () => Promise.resolve();
|
|
18
|
+
export const callThreadsafeFunction = () => Promise.resolve();
|
package/dist/infra/clipboard.js
CHANGED
package/dist/logging/logger.js
CHANGED
|
@@ -5,9 +5,15 @@ import { Logger as TsLogger } from "tslog";
|
|
|
5
5
|
import { levelToMinLevel, normalizeLogLevel } from "./levels.js";
|
|
6
6
|
import { readLoggingConfig } from "./config.js";
|
|
7
7
|
import { loggingState } from "./state.js";
|
|
8
|
+
import { isRunningInPureTermux } from "../daemon/proot.js";
|
|
9
|
+
import { resolveGatewayStateDir } from "../daemon/paths.js";
|
|
8
10
|
// Pin to /tmp so mac Debug UI and docs match; os.tmpdir() can be a per-user
|
|
9
|
-
// randomized path on macOS which made the
|
|
10
|
-
|
|
11
|
+
// randomized path on macOS which made the "Open log" button a no-op.
|
|
12
|
+
// For Termux native, use ~/.openclaw/logs/clawdbot.log instead of /tmp
|
|
13
|
+
const DEFAULT_LOG_DIR_VALUE = isRunningInPureTermux()
|
|
14
|
+
? path.join(resolveGatewayStateDir(process.env), "logs")
|
|
15
|
+
: "/tmp/clawdbot";
|
|
16
|
+
export const DEFAULT_LOG_DIR = DEFAULT_LOG_DIR_VALUE;
|
|
11
17
|
export const DEFAULT_LOG_FILE = path.join(DEFAULT_LOG_DIR, "clawdbot.log"); // legacy single-file path
|
|
12
18
|
const LOG_PREFIX = "clawdbot";
|
|
13
19
|
const LOG_SUFFIX = ".log";
|
|
@@ -24,16 +24,12 @@ export async function finalizeOnboardingWizard(options) {
|
|
|
24
24
|
progress.stop(options.doneMessage);
|
|
25
25
|
}
|
|
26
26
|
};
|
|
27
|
-
const systemdAvailable =
|
|
28
|
-
// process.platform === "linux" ? await isSystemdUserServiceAvailable() : true;
|
|
29
|
-
//if (process.platform === "linux" && !systemdAvailable) {
|
|
30
|
-
process.platform === "linux" && !isRunningInTermux()
|
|
27
|
+
const systemdAvailable = process.platform === "linux" && !isRunningInTermux()
|
|
31
28
|
? await isSystemdUserServiceAvailable()
|
|
32
29
|
: true;
|
|
33
30
|
if (process.platform === "linux" && !isRunningInTermux() && !systemdAvailable) {
|
|
34
31
|
await prompter.note("Systemd user services are unavailable. Skipping lingering checks and service install.", "Systemd");
|
|
35
32
|
}
|
|
36
|
-
// if (process.platform === "linux" && systemdAvailable) {
|
|
37
33
|
if (process.platform === "linux" && !isRunningInTermux() && systemdAvailable) {
|
|
38
34
|
const { ensureSystemdUserLingerInteractive } = await import("../commands/systemd-linger.js");
|
|
39
35
|
await ensureSystemdUserLingerInteractive({
|
|
@@ -50,7 +46,6 @@ export async function finalizeOnboardingWizard(options) {
|
|
|
50
46
|
let installDaemon;
|
|
51
47
|
if (explicitInstallDaemon !== undefined) {
|
|
52
48
|
installDaemon = explicitInstallDaemon;
|
|
53
|
-
// } else if (process.platform === "linux" && !systemdAvailable) {
|
|
54
49
|
}
|
|
55
50
|
else if (process.platform === "linux" && !isRunningInTermux() && !systemdAvailable) {
|
|
56
51
|
installDaemon = false;
|
|
@@ -64,7 +59,6 @@ export async function finalizeOnboardingWizard(options) {
|
|
|
64
59
|
initialValue: true,
|
|
65
60
|
});
|
|
66
61
|
}
|
|
67
|
-
// if (process.platform === "linux" && !systemdAvailable && installDaemon) {
|
|
68
62
|
if (process.platform === "linux" && !isRunningInTermux() && !systemdAvailable && installDaemon) {
|
|
69
63
|
await prompter.note("Systemd user services are unavailable; skipping service install. Use your container supervisor or `docker compose up -d`.", "Gateway service");
|
|
70
64
|
installDaemon = false;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openclaw-cn-termux",
|
|
3
|
-
"version": "0.2.0",
|
|
3
|
+
"version": "0.2.1-beta.0",
|
|
4
4
|
"description": "Openclaw Termux 版本 - WhatsApp gateway CLI (Baileys web) with Pi RPC agent",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "MIT",
|
|
@@ -270,7 +270,8 @@
|
|
|
270
270
|
"libsignal": "npm:@openclaw-cn/libsignal@2.0.1"
|
|
271
271
|
},
|
|
272
272
|
"patchedDependencies": {
|
|
273
|
-
"@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch"
|
|
273
|
+
"@mariozechner/pi-ai": "patches/@mariozechner__pi-ai.patch",
|
|
274
|
+
"@mariozechner/clipboard": "patches/@mariozechner__clipboard.patch"
|
|
274
275
|
}
|
|
275
276
|
},
|
|
276
277
|
"openclawVersion": "2026.3.24",
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* tslint:disable */
|
|
2
|
+
/* eslint-disable */
|
|
3
|
+
/* prettier-ignore */
|
|
4
|
+
|
|
5
|
+
/* auto-generated by NAPI-RS */
|
|
6
|
+
|
|
7
|
+
const { execSync } = require('child_process')
|
|
8
|
+
|
|
9
|
+
function termuxSetText(value) {
|
|
10
|
+
try {
|
|
11
|
+
execSync('termux-clipboard-set', {
|
|
12
|
+
input: value,
|
|
13
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
14
|
+
})
|
|
15
|
+
} catch {
|
|
16
|
+
throw new Error('Failed to set clipboard')
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function termuxGetText() {
|
|
21
|
+
try {
|
|
22
|
+
return execSync('termux-clipboard-get', {
|
|
23
|
+
encoding: 'utf8',
|
|
24
|
+
})
|
|
25
|
+
} catch {
|
|
26
|
+
return ''
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function termuxHasText() {
|
|
31
|
+
try {
|
|
32
|
+
const text = execSync('termux-clipboard-get', {
|
|
33
|
+
encoding: 'utf8',
|
|
34
|
+
})
|
|
35
|
+
return text.length > 0
|
|
36
|
+
} catch {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports.availableFormats = () => []
|
|
42
|
+
module.exports.getText = termuxGetText
|
|
43
|
+
module.exports.setText = termuxSetText
|
|
44
|
+
module.exports.hasText = termuxHasText
|
|
45
|
+
module.exports.getImageBinary = () => null
|
|
46
|
+
module.exports.getImageBase64 = () => ''
|
|
47
|
+
module.exports.setImageBinary = () => {}
|
|
48
|
+
module.exports.setImageBase64 = () => {}
|
|
49
|
+
module.exports.hasImage = () => false
|
|
50
|
+
module.exports.getHtml = () => ''
|
|
51
|
+
module.exports.setHtml = () => {}
|
|
52
|
+
module.exports.hasHtml = () => false
|
|
53
|
+
module.exports.getRtf = () => ''
|
|
54
|
+
module.exports.setRtf = () => {}
|
|
55
|
+
module.exports.hasRtf = () => false
|
|
56
|
+
module.exports.clear = () => {}
|
|
57
|
+
module.exports.watch = () => {}
|
|
58
|
+
module.exports.callThreadsafeFunction = () => {}
|