@clawpilot-app/link 0.1.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/README.md +48 -0
- package/package.json +43 -0
- package/scripts/check-node-version.mjs +44 -0
- package/src/cli.js +599 -0
- package/src/constants.js +1 -0
- package/src/daemon.js +1661 -0
- package/src/i18n.js +182 -0
- package/src/network.js +71 -0
- package/src/openclaw.js +297 -0
- package/src/runtime.js +423 -0
- package/src/server-api.js +135 -0
package/src/i18n.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
export const SUPPORTED_LANGUAGES = Object.freeze(["en", "zh-Hans"]);
|
|
2
|
+
|
|
3
|
+
const copy = {
|
|
4
|
+
en: {
|
|
5
|
+
pairingCreated: "ClawPilot Link pairing session created.",
|
|
6
|
+
scanPrompt: "Scan this QR with ClawPilot App:",
|
|
7
|
+
manualCode: "Manual code",
|
|
8
|
+
waitingForScan: "Waiting for someone to scan the code...",
|
|
9
|
+
pairingSuccess: "ClawPilot Link is now paired.",
|
|
10
|
+
pairingExpired: "This pairing session expired. Run clawlink pair again.",
|
|
11
|
+
startDaemonAfterPair: "Starting the Link service in the background...",
|
|
12
|
+
daemonStarted: "ClawPilot Link is running in the background.",
|
|
13
|
+
daemonAlreadyRunning: "ClawPilot Link is already running.",
|
|
14
|
+
daemonStopped: "ClawPilot Link has been stopped.",
|
|
15
|
+
restartDone: "ClawPilot Link has been restarted.",
|
|
16
|
+
notPaired: "This computer is not paired yet. Run clawlink pair first.",
|
|
17
|
+
backendMissing: "OpenClaw was not found on this computer.",
|
|
18
|
+
backendUnsupported: "Your current OpenClaw setup is not supported yet.",
|
|
19
|
+
backendReachableOnPort: "OpenClaw is reachable on port {{port}}.",
|
|
20
|
+
localAccessReadyOnPort: "Local access is ready on port {{port}}.",
|
|
21
|
+
localAccessDaemonOffline: "Local access is off because ClawPilot Link is not running.",
|
|
22
|
+
localAccessPortInUse:
|
|
23
|
+
"Local access could not start because port {{port}} is already being used by another app on this computer. Close that app, then run clawlink restart.",
|
|
24
|
+
localAccessPermissionDenied:
|
|
25
|
+
"Local access could not start because this computer denied permission to open port {{port}}. Check your system settings or security software, then run clawlink restart.",
|
|
26
|
+
localAccessUnavailable:
|
|
27
|
+
"Local access is not available right now. Run clawlink doctor to see what needs attention.",
|
|
28
|
+
localAccessNeedsAttention:
|
|
29
|
+
"ClawPilot Link can still use Relay, but local network direct connection is not ready yet. Run clawlink doctor for help.",
|
|
30
|
+
doctorSummaryOk: "Everything looks good.",
|
|
31
|
+
doctorSummaryFix: "Something needs attention before Link can stay online.",
|
|
32
|
+
statusTitle: "ClawPilot Link status",
|
|
33
|
+
statusLinkIdLabel: "Link ID",
|
|
34
|
+
statusStateLabel: "Status",
|
|
35
|
+
statusDaemonLabel: "Daemon",
|
|
36
|
+
statusOpenClawLabel: "OpenClaw",
|
|
37
|
+
statusLocalAccessLabel: "Local access",
|
|
38
|
+
statusLastErrorLabel: "Last error",
|
|
39
|
+
statusBackendUrlLabel: "Backend URL",
|
|
40
|
+
statusNotPairedValue: "Not paired",
|
|
41
|
+
statusUnknownValue: "Unknown",
|
|
42
|
+
statusDaemonOnlineValue: "online",
|
|
43
|
+
statusDaemonOfflineValue: "offline",
|
|
44
|
+
qrJoinReady: "A new join QR code is ready.",
|
|
45
|
+
qrNeedsPair: "This computer is not paired yet, so we cannot create a join QR code.",
|
|
46
|
+
connectForeground: "Running ClawPilot Link in the foreground. Press Ctrl+C to stop.",
|
|
47
|
+
connectBackground: "ClawPilot Link has been started in the background.",
|
|
48
|
+
pairedOk: "Link is paired.",
|
|
49
|
+
daemonRunning: "Daemon is running.",
|
|
50
|
+
daemonNotRunning: "Daemon is not running.",
|
|
51
|
+
linkCredentialsMissing: "Link needs to be paired again on this computer.",
|
|
52
|
+
linkCredentialsExpired: "This computer's Link sign-in expired. Run clawlink pair again.",
|
|
53
|
+
linkRevoked: "This computer's Link access was removed. Run clawlink pair again to reconnect.",
|
|
54
|
+
relayConfigSecure: "Relay address uses HTTPS.",
|
|
55
|
+
relayConfigInsecure:
|
|
56
|
+
"The Relay address must start with https://. Open {{configFile}} and update relayBaseUrl.",
|
|
57
|
+
relayReachable: "Relay is reachable.",
|
|
58
|
+
relayUnreachable: "Relay is not reachable right now.",
|
|
59
|
+
relayCheckSkipped:
|
|
60
|
+
"Relay health was not checked because the Relay address is not using https://.",
|
|
61
|
+
apiReachable: "API server is reachable.",
|
|
62
|
+
apiUnreachable: "API server is not reachable right now.",
|
|
63
|
+
skillInstalled: "Skill file is installed.",
|
|
64
|
+
skillMissing: "Skill file is missing.",
|
|
65
|
+
unpairConfirm: "Run clawlink unpair --yes if you really want to clear this computer's Link credentials.",
|
|
66
|
+
startDaemonFailed: "ClawPilot Link did not start in time.",
|
|
67
|
+
restartFailed: "ClawPilot Link did not restart in time.",
|
|
68
|
+
unknownCommand: "Unknown command",
|
|
69
|
+
availableCommands: "Available commands: pair, qr, connect, status, doctor, restart, unpair",
|
|
70
|
+
},
|
|
71
|
+
"zh-Hans": {
|
|
72
|
+
pairingCreated: "已创建 ClawPilot Link 配对会话。",
|
|
73
|
+
scanPrompt: "请用 ClawPilot App 扫描下面的二维码:",
|
|
74
|
+
manualCode: "手动输入码",
|
|
75
|
+
waitingForScan: "正在等待有人扫码...",
|
|
76
|
+
pairingSuccess: "ClawPilot Link 已完成配对。",
|
|
77
|
+
pairingExpired: "这次配对已经过期,请重新执行 clawlink pair。",
|
|
78
|
+
startDaemonAfterPair: "正在后台启动 Link 服务...",
|
|
79
|
+
daemonStarted: "ClawPilot Link 已在后台运行。",
|
|
80
|
+
daemonAlreadyRunning: "ClawPilot Link 已经在运行。",
|
|
81
|
+
daemonStopped: "ClawPilot Link 已停止。",
|
|
82
|
+
restartDone: "ClawPilot Link 已重新启动。",
|
|
83
|
+
notPaired: "这台电脑还没有配对,请先执行 clawlink pair。",
|
|
84
|
+
backendMissing: "这台电脑上没有找到 OpenClaw。",
|
|
85
|
+
backendUnsupported: "当前 OpenClaw 配置暂时还不支持 Link。",
|
|
86
|
+
backendReachableOnPort: "已经在 {{port}} 端口找到 OpenClaw。",
|
|
87
|
+
localAccessReadyOnPort: "局域网直连已经在 {{port}} 端口就绪。",
|
|
88
|
+
localAccessDaemonOffline: "由于 ClawPilot Link 没有运行,局域网直连当前不可用。",
|
|
89
|
+
localAccessPortInUse:
|
|
90
|
+
"{{port}} 端口已经被这台电脑上的其他程序占用,所以局域网直连没能启动。先关闭占用它的程序,再执行 clawlink restart。",
|
|
91
|
+
localAccessPermissionDenied:
|
|
92
|
+
"这台电脑没有允许 ClawPilot Link 打开 {{port}} 端口,所以局域网直连没能启动。请检查系统设置或安全软件,然后再执行 clawlink restart。",
|
|
93
|
+
localAccessUnavailable: "局域网直连暂时还不可用。你可以执行 clawlink doctor 看看哪里需要处理。",
|
|
94
|
+
localAccessNeedsAttention:
|
|
95
|
+
"现在仍然可以走 Relay,但局域网直连还没有准备好。你可以执行 clawlink doctor 查看提示。",
|
|
96
|
+
doctorSummaryOk: "当前看起来一切正常。",
|
|
97
|
+
doctorSummaryFix: "还有问题需要先处理,Link 才能稳定在线。",
|
|
98
|
+
statusTitle: "ClawPilot Link 状态",
|
|
99
|
+
statusLinkIdLabel: "Link ID",
|
|
100
|
+
statusStateLabel: "当前状态",
|
|
101
|
+
statusDaemonLabel: "后台服务",
|
|
102
|
+
statusOpenClawLabel: "OpenClaw",
|
|
103
|
+
statusLocalAccessLabel: "局域网直连",
|
|
104
|
+
statusLastErrorLabel: "上次错误",
|
|
105
|
+
statusBackendUrlLabel: "本地地址",
|
|
106
|
+
statusNotPairedValue: "还没有配对",
|
|
107
|
+
statusUnknownValue: "未知",
|
|
108
|
+
statusDaemonOnlineValue: "在线",
|
|
109
|
+
statusDaemonOfflineValue: "离线",
|
|
110
|
+
qrJoinReady: "新的加入二维码已经生成。",
|
|
111
|
+
qrNeedsPair: "这台电脑还没有完成配对,所以现在不能生成加入二维码。",
|
|
112
|
+
connectForeground: "ClawPilot Link 正在前台运行。按 Ctrl+C 可停止。",
|
|
113
|
+
connectBackground: "ClawPilot Link 已在后台启动。",
|
|
114
|
+
pairedOk: "这台电脑已经完成 Link 配对。",
|
|
115
|
+
daemonRunning: "后台服务正在运行。",
|
|
116
|
+
daemonNotRunning: "后台服务还没有运行。",
|
|
117
|
+
linkCredentialsMissing: "这台电脑需要重新配对 Link。",
|
|
118
|
+
linkCredentialsExpired: "这台电脑上的 Link 登录已经失效,请重新执行 clawlink pair。",
|
|
119
|
+
linkRevoked: "这台电脑的 Link 访问权限已被移除,请重新执行 clawlink pair。",
|
|
120
|
+
relayConfigSecure: "Relay 地址已使用 HTTPS。",
|
|
121
|
+
relayConfigInsecure:
|
|
122
|
+
"Relay 地址必须以 https:// 开头。请打开 {{configFile}},把 relayBaseUrl 改成 https:// 地址。",
|
|
123
|
+
relayReachable: "Relay 服务可以连通。",
|
|
124
|
+
relayUnreachable: "现在还连不上 Relay 服务。",
|
|
125
|
+
relayCheckSkipped: "由于 Relay 地址不是 https://,所以这次没有继续检测 Relay 连通性。",
|
|
126
|
+
apiReachable: "API 服务可以连通。",
|
|
127
|
+
apiUnreachable: "现在还连不上 API 服务。",
|
|
128
|
+
skillInstalled: "SKILL 文件已经安装。",
|
|
129
|
+
skillMissing: "还没有找到 SKILL 文件。",
|
|
130
|
+
unpairConfirm: "如果你确定要清掉这台电脑上的 Link 凭据,请执行 clawlink unpair --yes。",
|
|
131
|
+
startDaemonFailed: "ClawPilot Link 没有在预期时间内启动。",
|
|
132
|
+
restartFailed: "ClawPilot Link 没有在预期时间内重新启动。",
|
|
133
|
+
unknownCommand: "不支持这个命令",
|
|
134
|
+
availableCommands: "可用命令:pair、qr、connect、status、doctor、restart、unpair",
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
export function detectLanguage() {
|
|
139
|
+
const envCandidates = [
|
|
140
|
+
process.env.CLAWPILOT_LINK_LANG,
|
|
141
|
+
process.env.LC_ALL,
|
|
142
|
+
process.env.LC_MESSAGES,
|
|
143
|
+
process.env.LANG,
|
|
144
|
+
]
|
|
145
|
+
.filter((value) => typeof value === "string" && value.trim())
|
|
146
|
+
.map((value) => value.toLowerCase());
|
|
147
|
+
|
|
148
|
+
for (const candidate of envCandidates) {
|
|
149
|
+
if (candidate.includes("zh")) {
|
|
150
|
+
return "zh-Hans";
|
|
151
|
+
}
|
|
152
|
+
if (candidate.includes("en")) {
|
|
153
|
+
return "en";
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return "en";
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
export function resolveLanguage(configLanguage) {
|
|
161
|
+
if (configLanguage === "zh-Hans" || configLanguage === "en") {
|
|
162
|
+
return configLanguage;
|
|
163
|
+
}
|
|
164
|
+
return detectLanguage();
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function createTranslator(language) {
|
|
168
|
+
const resolvedLanguage = language === "zh-Hans" ? "zh-Hans" : "en";
|
|
169
|
+
return {
|
|
170
|
+
language: resolvedLanguage,
|
|
171
|
+
t(key, vars = undefined) {
|
|
172
|
+
const template = copy[resolvedLanguage][key] ?? copy.en[key] ?? key;
|
|
173
|
+
if (!vars || typeof vars !== "object") {
|
|
174
|
+
return template;
|
|
175
|
+
}
|
|
176
|
+
return template.replace(/\{\{(\w+)\}\}/g, (_, name) => {
|
|
177
|
+
const value = vars[name];
|
|
178
|
+
return value === undefined || value === null ? "" : String(value);
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
package/src/network.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
|
|
3
|
+
export const LINK_DIRECT_PORT = 52378;
|
|
4
|
+
|
|
5
|
+
const VIRTUAL_INTERFACE_NAME_PATTERN =
|
|
6
|
+
/(docker|veth|vmnet|vbox|tailscale|utun|virbr|hyper-v|vethernet|loopback|\blo\b|^br-|^zt|^tun|^tap)/i;
|
|
7
|
+
|
|
8
|
+
function normalizeHost(value) {
|
|
9
|
+
if (typeof value !== "string") {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const normalized = value.trim();
|
|
13
|
+
return normalized || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function isPrivateIpv4(address) {
|
|
17
|
+
if (!/^\d{1,3}(?:\.\d{1,3}){3}$/.test(address)) {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const segments = address.split(".").map((segment) => Number.parseInt(segment, 10));
|
|
22
|
+
if (segments.some((segment) => Number.isNaN(segment) || segment < 0 || segment > 255)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const [first, second] = segments;
|
|
27
|
+
if (first === 10) {
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
if (first === 192 && second === 168) {
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
return first === 172 && second >= 16 && second <= 31;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shouldIgnoreInterface(name) {
|
|
37
|
+
const normalized = normalizeHost(name);
|
|
38
|
+
if (!normalized) {
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
return VIRTUAL_INTERFACE_NAME_PATTERN.test(normalized);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function listLanIpv4Addresses() {
|
|
45
|
+
const interfaces = os.networkInterfaces();
|
|
46
|
+
const addresses = [];
|
|
47
|
+
const seen = new Set();
|
|
48
|
+
|
|
49
|
+
for (const [name, entries] of Object.entries(interfaces)) {
|
|
50
|
+
if (shouldIgnoreInterface(name) || !Array.isArray(entries)) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
for (const entry of entries) {
|
|
55
|
+
if (!entry || entry.internal) {
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
if (entry.family !== "IPv4") {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const address = normalizeHost(entry.address);
|
|
62
|
+
if (!address || !isPrivateIpv4(address) || seen.has(address)) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
seen.add(address);
|
|
66
|
+
addresses.push(address);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return addresses.sort((left, right) => left.localeCompare(right));
|
|
71
|
+
}
|
package/src/openclaw.js
ADDED
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import JSON5 from "json5";
|
|
5
|
+
import {
|
|
6
|
+
fileExists,
|
|
7
|
+
normalizeNonEmptyString,
|
|
8
|
+
runtimePaths,
|
|
9
|
+
} from "./runtime.js";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_GATEWAY_PORT = 18789;
|
|
12
|
+
const FALLBACK_PORTS = [18789, 3400];
|
|
13
|
+
|
|
14
|
+
function resolveConfigPath() {
|
|
15
|
+
if (normalizeNonEmptyString(process.env.OPENCLAW_CONFIG_PATH)) {
|
|
16
|
+
return process.env.OPENCLAW_CONFIG_PATH.trim();
|
|
17
|
+
}
|
|
18
|
+
if (normalizeNonEmptyString(process.env.OPENCLAW_STATE_DIR)) {
|
|
19
|
+
return path.join(process.env.OPENCLAW_STATE_DIR.trim(), "openclaw.json");
|
|
20
|
+
}
|
|
21
|
+
return path.join(os.homedir(), ".openclaw", "openclaw.json");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function readOpenClawConfig() {
|
|
25
|
+
const configPath = resolveConfigPath();
|
|
26
|
+
if (!fileExists(configPath)) {
|
|
27
|
+
return {
|
|
28
|
+
exists: false,
|
|
29
|
+
configPath,
|
|
30
|
+
config: null,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const raw = await fsp.readFile(configPath, "utf8");
|
|
35
|
+
return {
|
|
36
|
+
exists: true,
|
|
37
|
+
configPath,
|
|
38
|
+
config: JSON5.parse(raw),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function resolveGatewayAuth(config) {
|
|
43
|
+
const gateway = config?.gateway ?? {};
|
|
44
|
+
const auth = gateway?.auth ?? {};
|
|
45
|
+
const mode = normalizeNonEmptyString(auth.mode);
|
|
46
|
+
const token = normalizeNonEmptyString(auth.token);
|
|
47
|
+
const password = normalizeNonEmptyString(auth.password);
|
|
48
|
+
|
|
49
|
+
if (mode === "token" || (!mode && token)) {
|
|
50
|
+
return token
|
|
51
|
+
? {
|
|
52
|
+
authType: "token",
|
|
53
|
+
secret: token,
|
|
54
|
+
supported: true,
|
|
55
|
+
message: null,
|
|
56
|
+
}
|
|
57
|
+
: {
|
|
58
|
+
authType: "token",
|
|
59
|
+
secret: null,
|
|
60
|
+
supported: false,
|
|
61
|
+
message: "gateway.auth.token is missing.",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (mode === "password" || (!mode && password)) {
|
|
66
|
+
return password
|
|
67
|
+
? {
|
|
68
|
+
authType: "password",
|
|
69
|
+
secret: password,
|
|
70
|
+
supported: true,
|
|
71
|
+
message: null,
|
|
72
|
+
}
|
|
73
|
+
: {
|
|
74
|
+
authType: "password",
|
|
75
|
+
secret: null,
|
|
76
|
+
supported: false,
|
|
77
|
+
message: "gateway.auth.password is missing.",
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return {
|
|
82
|
+
authType: mode ?? null,
|
|
83
|
+
secret: null,
|
|
84
|
+
supported: false,
|
|
85
|
+
message: "Link currently needs OpenClaw shared-secret auth (token or password).",
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function buildLocalAuthHeaders(authType, secret) {
|
|
90
|
+
const normalizedSecret = normalizeNonEmptyString(secret);
|
|
91
|
+
if (!normalizedSecret) {
|
|
92
|
+
return {};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (authType === "password") {
|
|
96
|
+
const basicValue = Buffer.from(`:${normalizedSecret}`, "utf8").toString("base64");
|
|
97
|
+
return {
|
|
98
|
+
authorization: `Basic ${basicValue}`,
|
|
99
|
+
"x-openclaw-password": normalizedSecret,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
authorization: `Bearer ${normalizedSecret}`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function probeHttpBaseUrl(candidate) {
|
|
109
|
+
const controller = new AbortController();
|
|
110
|
+
const timeoutId = setTimeout(() => {
|
|
111
|
+
controller.abort();
|
|
112
|
+
}, 4_000);
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const response = await fetch(`${candidate.httpBaseUrl}/v1/responses`, {
|
|
116
|
+
method: "POST",
|
|
117
|
+
headers: {
|
|
118
|
+
"content-type": "application/json",
|
|
119
|
+
"x-openclaw-scopes": "operator.write",
|
|
120
|
+
...buildLocalAuthHeaders(candidate.authType, candidate.secret),
|
|
121
|
+
},
|
|
122
|
+
body: JSON.stringify({}),
|
|
123
|
+
signal: controller.signal,
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
return {
|
|
127
|
+
reachable:
|
|
128
|
+
response.ok ||
|
|
129
|
+
[400, 401, 403, 404, 405, 422, 501].includes(response.status),
|
|
130
|
+
status: response.status,
|
|
131
|
+
message: `HTTP ${response.status}`,
|
|
132
|
+
};
|
|
133
|
+
} catch (error) {
|
|
134
|
+
return {
|
|
135
|
+
reachable: false,
|
|
136
|
+
status: 0,
|
|
137
|
+
message: error instanceof Error ? error.message : "probe_failed",
|
|
138
|
+
};
|
|
139
|
+
} finally {
|
|
140
|
+
clearTimeout(timeoutId);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function detectOpenClawBackend() {
|
|
145
|
+
const configResult = await readOpenClawConfig();
|
|
146
|
+
const candidates = [];
|
|
147
|
+
if (configResult.exists && configResult.config) {
|
|
148
|
+
const gateway = configResult.config.gateway ?? {};
|
|
149
|
+
const rawPort = Number(gateway.port ?? DEFAULT_GATEWAY_PORT);
|
|
150
|
+
const port = Number.isInteger(rawPort) && rawPort > 0 ? rawPort : DEFAULT_GATEWAY_PORT;
|
|
151
|
+
const auth = resolveGatewayAuth(configResult.config);
|
|
152
|
+
candidates.push({
|
|
153
|
+
source: "config",
|
|
154
|
+
configPath: configResult.configPath,
|
|
155
|
+
port,
|
|
156
|
+
httpBaseUrl: `http://127.0.0.1:${port}`,
|
|
157
|
+
wsEndpoint: `ws://127.0.0.1:${port}`,
|
|
158
|
+
authType: auth.authType,
|
|
159
|
+
secret: auth.secret,
|
|
160
|
+
supported: auth.supported,
|
|
161
|
+
supportMessage: auth.message,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
for (const port of FALLBACK_PORTS) {
|
|
166
|
+
if (candidates.some((candidate) => candidate.port === port)) {
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
candidates.push({
|
|
170
|
+
source: "probe",
|
|
171
|
+
configPath: configResult.configPath,
|
|
172
|
+
port,
|
|
173
|
+
httpBaseUrl: `http://127.0.0.1:${port}`,
|
|
174
|
+
wsEndpoint: `ws://127.0.0.1:${port}`,
|
|
175
|
+
authType: null,
|
|
176
|
+
secret: null,
|
|
177
|
+
supported: false,
|
|
178
|
+
supportMessage: "OpenClaw config was not found, so Link cannot read gateway auth.",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
for (const candidate of candidates) {
|
|
183
|
+
const probe = await probeHttpBaseUrl(candidate);
|
|
184
|
+
if (!probe.reachable) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
return {
|
|
189
|
+
detected: true,
|
|
190
|
+
healthy: candidate.supported,
|
|
191
|
+
supported: candidate.supported,
|
|
192
|
+
message:
|
|
193
|
+
candidate.supported
|
|
194
|
+
? `OpenClaw is reachable on port ${candidate.port}.`
|
|
195
|
+
: candidate.supportMessage,
|
|
196
|
+
configPath: candidate.configPath,
|
|
197
|
+
httpBaseUrl: candidate.httpBaseUrl,
|
|
198
|
+
wsEndpoint: candidate.wsEndpoint,
|
|
199
|
+
authType: candidate.authType,
|
|
200
|
+
secret: candidate.secret,
|
|
201
|
+
source: candidate.source,
|
|
202
|
+
port: candidate.port,
|
|
203
|
+
checkedAt: new Date().toISOString(),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return {
|
|
208
|
+
detected: false,
|
|
209
|
+
healthy: false,
|
|
210
|
+
supported: false,
|
|
211
|
+
message: "OpenClaw is not reachable on this computer.",
|
|
212
|
+
configPath: configResult.configPath,
|
|
213
|
+
httpBaseUrl: null,
|
|
214
|
+
wsEndpoint: null,
|
|
215
|
+
authType: null,
|
|
216
|
+
secret: null,
|
|
217
|
+
source: null,
|
|
218
|
+
port: null,
|
|
219
|
+
checkedAt: new Date().toISOString(),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export async function writeBackendCache(backend) {
|
|
224
|
+
await fsp.writeFile(
|
|
225
|
+
path.join(runtimePaths.cacheDir, "openclaw.json"),
|
|
226
|
+
`${JSON.stringify(backend, null, 2)}\n`,
|
|
227
|
+
"utf8",
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function buildSkillContent(language) {
|
|
232
|
+
if (language === "zh-Hans") {
|
|
233
|
+
return `# ClawPilot Link
|
|
234
|
+
|
|
235
|
+
这台电脑已经安装了 ClawPilot Link。
|
|
236
|
+
|
|
237
|
+
你可以优先使用这些命令:
|
|
238
|
+
|
|
239
|
+
- \`clawlink status --json\`
|
|
240
|
+
- \`clawlink doctor --json\`
|
|
241
|
+
- \`clawlink qr\`
|
|
242
|
+
- \`clawlink restart\`
|
|
243
|
+
|
|
244
|
+
高风险命令必须先征求用户确认:
|
|
245
|
+
|
|
246
|
+
- \`clawlink unpair\`
|
|
247
|
+
|
|
248
|
+
如果用户只是说“连不上”或“掉线了”,先运行:
|
|
249
|
+
|
|
250
|
+
1. \`clawlink status --json\`
|
|
251
|
+
2. \`clawlink doctor --json\`
|
|
252
|
+
|
|
253
|
+
然后把发生了什么和建议怎么修复,用普通人能看懂的话告诉用户。
|
|
254
|
+
`;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return `# ClawPilot Link
|
|
258
|
+
|
|
259
|
+
ClawPilot Link is installed on this computer.
|
|
260
|
+
|
|
261
|
+
Start with these commands:
|
|
262
|
+
|
|
263
|
+
- \`clawlink status --json\`
|
|
264
|
+
- \`clawlink doctor --json\`
|
|
265
|
+
- \`clawlink qr\`
|
|
266
|
+
- \`clawlink restart\`
|
|
267
|
+
|
|
268
|
+
Ask for confirmation before running anything risky:
|
|
269
|
+
|
|
270
|
+
- \`clawlink unpair\`
|
|
271
|
+
|
|
272
|
+
If the user says Link is offline or not connecting, check:
|
|
273
|
+
|
|
274
|
+
1. \`clawlink status --json\`
|
|
275
|
+
2. \`clawlink doctor --json\`
|
|
276
|
+
|
|
277
|
+
Then explain what happened in plain language and tell the user what to do next.
|
|
278
|
+
`;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export async function installSkill(language) {
|
|
282
|
+
const configPath = resolveConfigPath();
|
|
283
|
+
const stateDir = path.dirname(configPath);
|
|
284
|
+
const skillDir = path.join(stateDir, "skills", "clawpilot-link");
|
|
285
|
+
await fsp.mkdir(skillDir, { recursive: true });
|
|
286
|
+
const skillPath = path.join(skillDir, "SKILL.md");
|
|
287
|
+
await fsp.writeFile(skillPath, buildSkillContent(language), "utf8");
|
|
288
|
+
return {
|
|
289
|
+
skillPath,
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function buildLocalGatewayRequestHeaders(backend) {
|
|
294
|
+
return {
|
|
295
|
+
...buildLocalAuthHeaders(backend.authType, backend.secret),
|
|
296
|
+
};
|
|
297
|
+
}
|