@rethinkingstudio/clawpilot 1.1.17 → 2.0.0-beta.1
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/dist/commands/install.js +17 -18
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/openclaw-cli.js +23 -3
- package/dist/commands/openclaw-cli.js.map +1 -1
- package/dist/commands/pair.js +8 -1
- package/dist/commands/pair.js.map +1 -1
- package/dist/commands/status.js +3 -0
- package/dist/commands/status.js.map +1 -1
- package/dist/i18n/index.js +6 -0
- package/dist/i18n/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/platform/service-manager.d.ts +6 -0
- package/dist/platform/service-manager.js +384 -42
- package/dist/platform/service-manager.js.map +1 -1
- package/package.json +8 -1
- package/src/commands/assistant-send.ts +0 -91
- package/src/commands/install.ts +0 -128
- package/src/commands/local-handlers.ts +0 -533
- package/src/commands/openclaw-cli.ts +0 -329
- package/src/commands/pair.ts +0 -120
- package/src/commands/provider-config.ts +0 -275
- package/src/commands/provider-handlers.ts +0 -184
- package/src/commands/provider-registry.ts +0 -138
- package/src/commands/run.ts +0 -34
- package/src/commands/send.ts +0 -42
- package/src/commands/session-key.ts +0 -77
- package/src/commands/set-token.ts +0 -45
- package/src/commands/status.ts +0 -154
- package/src/commands/upload.ts +0 -141
- package/src/config/config.ts +0 -137
- package/src/generated/build-config.ts +0 -1
- package/src/i18n/index.ts +0 -179
- package/src/index.ts +0 -166
- package/src/media/assistant-attachments.ts +0 -205
- package/src/media/oss-uploader.ts +0 -306
- package/src/platform/service-manager.ts +0 -570
- package/src/relay/gateway-client.ts +0 -359
- package/src/relay/reconnect.ts +0 -37
- package/src/relay/relay-manager.ts +0 -328
- package/test-chat.mjs +0 -64
- package/test-direct.mjs +0 -171
- package/tsconfig.json +0 -16
package/src/i18n/index.ts
DELETED
|
@@ -1,179 +0,0 @@
|
|
|
1
|
-
type Locale = "en" | "zh";
|
|
2
|
-
type MsgFn = (...args: string[]) => string;
|
|
3
|
-
type MsgValue = string | MsgFn;
|
|
4
|
-
|
|
5
|
-
const en: Record<string, MsgValue> = {
|
|
6
|
-
// pair
|
|
7
|
-
"pair.alreadyRegistered": (id) => `Gateway already registered (id=${id}). Refreshing access code…`,
|
|
8
|
-
"pair.invalidCredentials": "Invalid credentials (401). The server doesn't recognize this gateway.\nRun `clawpilot reset` to clear config and re-register.",
|
|
9
|
-
"pair.refreshFailed": (status, body) => `Failed to refresh access code: ${status} ${body}`,
|
|
10
|
-
"pair.registering": "Registering with relay server…",
|
|
11
|
-
"pair.registrationFailed": (status, body) => `Registration failed: ${status} ${body}`,
|
|
12
|
-
"pair.registered": (id) => `Registered! Gateway ID: ${id}`,
|
|
13
|
-
"pair.scanQR": "\nScan this QR code with the Clawai iOS app:\n",
|
|
14
|
-
"pair.accessCode": (code) => `\nAccess code (one-time use): ${code}`,
|
|
15
|
-
"pair.installingService": "\nInstalling/updating relay background service…",
|
|
16
|
-
|
|
17
|
-
// run
|
|
18
|
-
"run.starting": "Starting ClawAI relay client…",
|
|
19
|
-
"run.gatewayId": (id) => ` Gateway ID: ${id}`,
|
|
20
|
-
"run.relayServer": (url) => ` Relay Server: ${url}`,
|
|
21
|
-
"run.gatewayUrl": (url) => ` Gateway URL: ${url}`,
|
|
22
|
-
"run.connected": "Relay connected.",
|
|
23
|
-
"run.disconnected": "Relay disconnected. Reconnecting…",
|
|
24
|
-
"run.retry": (attempt, delay) => `Retry attempt ${attempt}, waiting ${delay}ms…`,
|
|
25
|
-
|
|
26
|
-
// install
|
|
27
|
-
"install.serviceStarted": (manager) => `Service installed and started via ${manager}.`,
|
|
28
|
-
"install.installFailed": (manager) => `Failed to activate service via ${manager}.`,
|
|
29
|
-
"install.serviceFileWritten": (path) => `Service file written to: ${path}`,
|
|
30
|
-
"install.startManually": (command) => `Start manually: ${command}`,
|
|
31
|
-
"install.restarting": "Restarting relay service…",
|
|
32
|
-
"install.serviceRestarted": (manager) => `Service restarted via ${manager}.`,
|
|
33
|
-
"install.restartFailed": (manager) => `Failed to restart service via ${manager}.`,
|
|
34
|
-
"install.stopped": (manager) => `Relay client stopped via ${manager}.`,
|
|
35
|
-
"install.stoppedAndRemoved": (manager) => `Relay client stopped and removed from ${manager}.`,
|
|
36
|
-
"install.noService": "No running relay service found.",
|
|
37
|
-
"install.unsupported": (platform) => `Background service management is not supported on platform: ${platform}`,
|
|
38
|
-
"install.runForeground": "Run `clawpilot run` manually in the foreground instead.",
|
|
39
|
-
"install.configRemoved": (path) => `Config removed: ${path}`,
|
|
40
|
-
"install.removeConfigFailed": "Failed to remove config:",
|
|
41
|
-
"install.noConfig": "No config file found.",
|
|
42
|
-
"install.resetComplete": "\nReset complete. Run `clawpilot pair` to re-register.",
|
|
43
|
-
|
|
44
|
-
// status
|
|
45
|
-
"status.title": "── ClawPilot Relay Client Status ──\n",
|
|
46
|
-
"status.notPaired": "Config: ✗ Not paired — run 'clawpilot pair' first",
|
|
47
|
-
"status.paired": "Config: ✓ Paired",
|
|
48
|
-
"status.displayName": (name) => ` Display name : ${name}`,
|
|
49
|
-
"status.gatewayId": (id) => ` Gateway ID : ${id}`,
|
|
50
|
-
"status.relayServer": (url) => ` Relay server : ${url}`,
|
|
51
|
-
"status.configCorrupted": "Config: ✗ File exists but is corrupted",
|
|
52
|
-
"status.gateway": (url) => `\nGateway: ${url}`,
|
|
53
|
-
"status.servicePlatform": (manager) => `\nService Manager: ${manager}`,
|
|
54
|
-
"status.serviceNotInstalled": "\nService: ✗ Not installed — run 'clawpilot install'",
|
|
55
|
-
"status.serviceRunning": (manager) => `\nService: ✓ Running (${manager})`,
|
|
56
|
-
"status.serviceLog": (path) => ` Log : ${path}`,
|
|
57
|
-
"status.relayHealth": (icon, detail) => ` Relay : ${icon} ${detail}`,
|
|
58
|
-
"status.gatewayHealth": (icon, detail) => ` Gateway : ${icon} ${detail}`,
|
|
59
|
-
"status.serviceNotRunning": "\nService: ⚠ Installed but not running",
|
|
60
|
-
"status.serviceFile": (path) => ` Service file : ${path}`,
|
|
61
|
-
"status.serviceStart": (command) => ` Start : ${command}`,
|
|
62
|
-
"status.serviceUnsupported": (platform) => `\nService: - Unsupported on platform ${platform}`,
|
|
63
|
-
|
|
64
|
-
// set-token
|
|
65
|
-
"setToken.noPairing": "No pairing config found. Run 'clawpilot pair' first.",
|
|
66
|
-
"setToken.whereToFind": "\nWhere to find your Gateway Token:",
|
|
67
|
-
"setToken.option1": " Option 1 — OpenClaw desktop app: Settings → Advanced → Gateway Token",
|
|
68
|
-
"setToken.option2": " Option 2 — Terminal:",
|
|
69
|
-
"setToken.option2cmd": " cat ~/.openclaw/openclaw.json | grep -A2 'auth'",
|
|
70
|
-
"setToken.option3": " Option 3 — If set via environment variable: echo $OPENCLAW_GATEWAY_TOKEN\n",
|
|
71
|
-
"setToken.prompt": "Gateway Token (leave blank to clear): ",
|
|
72
|
-
"setToken.saved": "\nToken saved to ~/.clawai/config.json.",
|
|
73
|
-
"setToken.cleared": "\nToken cleared from ~/.clawai/config.json.",
|
|
74
|
-
"setToken.restart": "Run 'clawpilot restart' to apply the change.\n",
|
|
75
|
-
|
|
76
|
-
// upload
|
|
77
|
-
"upload.pathRequired": "Please provide a file path.",
|
|
78
|
-
"upload.fileMissing": (path) => `File not found: ${path}`,
|
|
79
|
-
"upload.notAFile": (path) => `Path is not a file: ${path}`,
|
|
80
|
-
"upload.starting": (name, mimeType) => `Uploading ${name} (${mimeType})…`,
|
|
81
|
-
"upload.completed": (url) => `Upload complete: ${url}`,
|
|
82
|
-
"send.preparing": (path) => `Sending file via PocketClaw: ${path}`,
|
|
83
|
-
"send.completed": (name, sessionKey) => `Sent ${name} to session ${sessionKey}`,
|
|
84
|
-
};
|
|
85
|
-
|
|
86
|
-
const zh: Record<string, MsgValue> = {
|
|
87
|
-
// pair
|
|
88
|
-
"pair.alreadyRegistered": (id) => `网关已注册 (id=${id}),正在刷新访问码…`,
|
|
89
|
-
"pair.invalidCredentials": "凭证无效 (401),服务器无法识别此网关。\n请运行 `clawpilot reset` 清除配置后重新注册。",
|
|
90
|
-
"pair.refreshFailed": (status, body) => `刷新访问码失败:${status} ${body}`,
|
|
91
|
-
"pair.registering": "正在向中继服务器注册…",
|
|
92
|
-
"pair.registrationFailed": (status, body) => `注册失败:${status} ${body}`,
|
|
93
|
-
"pair.registered": (id) => `注册成功!网关 ID:${id}`,
|
|
94
|
-
"pair.scanQR": "\n请用 Clawai iOS 应用扫描此二维码:\n",
|
|
95
|
-
"pair.accessCode": (code) => `\n访问码(一次性使用):${code}`,
|
|
96
|
-
"pair.installingService": "\n正在安装/更新中继后台服务…",
|
|
97
|
-
|
|
98
|
-
// run
|
|
99
|
-
"run.starting": "正在启动 ClawAI 中继客户端…",
|
|
100
|
-
"run.gatewayId": (id) => ` 网关 ID: ${id}`,
|
|
101
|
-
"run.relayServer": (url) => ` 中继服务器:${url}`,
|
|
102
|
-
"run.gatewayUrl": (url) => ` 网关地址: ${url}`,
|
|
103
|
-
"run.connected": "中继已连接。",
|
|
104
|
-
"run.disconnected": "中继已断开,正在重连…",
|
|
105
|
-
"run.retry": (attempt, delay) => `第 ${attempt} 次重试,等待 ${delay}ms…`,
|
|
106
|
-
|
|
107
|
-
// install
|
|
108
|
-
"install.serviceStarted": (manager) => `服务已通过 ${manager} 安装并启动。`,
|
|
109
|
-
"install.installFailed": (manager) => `通过 ${manager} 启动服务失败。`,
|
|
110
|
-
"install.serviceFileWritten": (path) => `服务文件已写入:${path}`,
|
|
111
|
-
"install.startManually": (command) => `请手动运行:${command}`,
|
|
112
|
-
"install.restarting": "正在重启中继服务…",
|
|
113
|
-
"install.serviceRestarted": (manager) => `服务已通过 ${manager} 重启。`,
|
|
114
|
-
"install.restartFailed": (manager) => `通过 ${manager} 重启服务失败。`,
|
|
115
|
-
"install.stopped": (manager) => `已通过 ${manager} 停止中继客户端。`,
|
|
116
|
-
"install.stoppedAndRemoved": (manager) => `中继客户端已停止并从 ${manager} 移除。`,
|
|
117
|
-
"install.noService": "未找到正在运行的中继服务。",
|
|
118
|
-
"install.unsupported": (platform) => `当前平台 ${platform} 暂不支持后台服务管理`,
|
|
119
|
-
"install.runForeground": "请改用 `clawpilot run` 前台运行。",
|
|
120
|
-
"install.configRemoved": (path) => `配置文件已删除:${path}`,
|
|
121
|
-
"install.removeConfigFailed": "删除配置文件失败:",
|
|
122
|
-
"install.noConfig": "未找到配置文件。",
|
|
123
|
-
"install.resetComplete": "\n重置完成。请运行 `clawpilot pair` 重新注册。",
|
|
124
|
-
|
|
125
|
-
// status
|
|
126
|
-
"status.title": "── ClawPilot 中继客户端状态 ──\n",
|
|
127
|
-
"status.notPaired": "配置:✗ 未配对 — 请先运行 'clawpilot pair'",
|
|
128
|
-
"status.paired": "配置:✓ 已配对",
|
|
129
|
-
"status.displayName": (name) => ` 显示名称:${name}`,
|
|
130
|
-
"status.gatewayId": (id) => ` 网关 ID: ${id}`,
|
|
131
|
-
"status.relayServer": (url) => ` 中继服务器:${url}`,
|
|
132
|
-
"status.configCorrupted": "配置:✗ 文件存在但已损坏",
|
|
133
|
-
"status.gateway": (url) => `\n网关地址:${url}`,
|
|
134
|
-
"status.servicePlatform": (manager) => `\n服务管理器:${manager}`,
|
|
135
|
-
"status.serviceNotInstalled": "\n服务:✗ 未安装 — 请运行 'clawpilot install'",
|
|
136
|
-
"status.serviceRunning": (manager) => `\n服务:✓ 运行中 (${manager})`,
|
|
137
|
-
"status.serviceLog": (path) => ` 日志:${path}`,
|
|
138
|
-
"status.relayHealth": (icon, detail) => ` 中继连接:${icon} ${detail}`,
|
|
139
|
-
"status.gatewayHealth": (icon, detail) => ` 网关连接:${icon} ${detail}`,
|
|
140
|
-
"status.serviceNotRunning": "\n服务:⚠ 已安装但未运行",
|
|
141
|
-
"status.serviceFile": (path) => ` 服务文件:${path}`,
|
|
142
|
-
"status.serviceStart": (command) => ` 启动命令:${command}`,
|
|
143
|
-
"status.serviceUnsupported": (platform) => `\n服务:- 平台 ${platform} 暂不支持`,
|
|
144
|
-
|
|
145
|
-
// set-token
|
|
146
|
-
"setToken.noPairing": "未找到配对配置,请先运行 'clawpilot pair'。",
|
|
147
|
-
"setToken.whereToFind": "\n如何查找 Gateway Token:",
|
|
148
|
-
"setToken.option1": " 方式一 — OpenClaw 桌面应用:设置 → 高级 → Gateway Token",
|
|
149
|
-
"setToken.option2": " 方式二 — 终端:",
|
|
150
|
-
"setToken.option2cmd": " cat ~/.openclaw/openclaw.json | grep -A2 'auth'",
|
|
151
|
-
"setToken.option3": " 方式三 — 若通过环境变量设置:echo $OPENCLAW_GATEWAY_TOKEN\n",
|
|
152
|
-
"setToken.prompt": "Gateway Token(留空则清除):",
|
|
153
|
-
"setToken.saved": "\nToken 已保存到 ~/.clawai/config.json。",
|
|
154
|
-
"setToken.cleared": "\nToken 已从 ~/.clawai/config.json 清除。",
|
|
155
|
-
"setToken.restart": "请运行 'clawpilot restart' 使修改生效。\n",
|
|
156
|
-
|
|
157
|
-
// upload
|
|
158
|
-
"upload.pathRequired": "请提供文件路径。",
|
|
159
|
-
"upload.fileMissing": (path) => `文件不存在:${path}`,
|
|
160
|
-
"upload.notAFile": (path) => `路径不是文件:${path}`,
|
|
161
|
-
"upload.starting": (name, mimeType) => `正在上传 ${name} (${mimeType})…`,
|
|
162
|
-
"upload.completed": (url) => `上传完成:${url}`,
|
|
163
|
-
"send.preparing": (path) => `正在通过 PocketClaw 发送文件:${path}`,
|
|
164
|
-
"send.completed": (name, sessionKey) => `已将 ${name} 发送到会话 ${sessionKey}`,
|
|
165
|
-
};
|
|
166
|
-
|
|
167
|
-
function detectLocale(): Locale {
|
|
168
|
-
const lang = process.env.LANG ?? process.env.LC_ALL ?? process.env.LANGUAGE ?? "";
|
|
169
|
-
return lang.toLowerCase().startsWith("zh") ? "zh" : "en";
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
const locale: Locale = detectLocale();
|
|
173
|
-
const msgs = locale === "zh" ? zh : en;
|
|
174
|
-
|
|
175
|
-
export function t(key: string, ...args: string[]): string {
|
|
176
|
-
const val = msgs[key] ?? en[key] ?? key;
|
|
177
|
-
if (typeof val === "function") return (val as MsgFn)(...args);
|
|
178
|
-
return val as string;
|
|
179
|
-
}
|
package/src/index.ts
DELETED
|
@@ -1,166 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import { Command } from "commander";
|
|
3
|
-
import { createRequire } from "module";
|
|
4
|
-
import { pairCommand } from "./commands/pair.js";
|
|
5
|
-
import { runCommand } from "./commands/run.js";
|
|
6
|
-
import { installCommand, uninstallCommand, stopCommand, restartCommand, resetCommand } from "./commands/install.js";
|
|
7
|
-
import { statusCommand } from "./commands/status.js";
|
|
8
|
-
import { setTokenCommand } from "./commands/set-token.js";
|
|
9
|
-
import { uploadCommand } from "./commands/upload.js";
|
|
10
|
-
import { assistantSendCommand } from "./commands/assistant-send.js";
|
|
11
|
-
import { sendCommand } from "./commands/send.js";
|
|
12
|
-
import { DEFAULT_RELAY_SERVER } from "./generated/build-config.js";
|
|
13
|
-
|
|
14
|
-
const require = createRequire(import.meta.url);
|
|
15
|
-
const { version } = require("../package.json") as { version: string };
|
|
16
|
-
|
|
17
|
-
const program = new Command();
|
|
18
|
-
|
|
19
|
-
program
|
|
20
|
-
.name("clawpilot")
|
|
21
|
-
.description("ClawPilot relay client — connects OpenClaw gateway hosts to the cloud relay server")
|
|
22
|
-
.version(version);
|
|
23
|
-
|
|
24
|
-
program
|
|
25
|
-
.command("pair")
|
|
26
|
-
.description("Register with relay server and display QR code for iOS pairing")
|
|
27
|
-
.option("-s, --server <url>", "Relay server URL", DEFAULT_RELAY_SERVER)
|
|
28
|
-
.option("-n, --name <name>", "Display name for this host")
|
|
29
|
-
.option("--code-only", "Print only the access code and skip QR code output", false)
|
|
30
|
-
.action(async (opts: { server: string; name: string; codeOnly?: boolean }) => {
|
|
31
|
-
try {
|
|
32
|
-
await pairCommand(opts);
|
|
33
|
-
} catch (err) {
|
|
34
|
-
console.error("Error:", err instanceof Error ? err.message : err);
|
|
35
|
-
process.exit(1);
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
program
|
|
40
|
-
.command("run")
|
|
41
|
-
.description("Run relay client in foreground (used by the background service manager)")
|
|
42
|
-
.action(async () => {
|
|
43
|
-
try {
|
|
44
|
-
await runCommand();
|
|
45
|
-
} catch (err) {
|
|
46
|
-
console.error("Error:", err instanceof Error ? err.message : err);
|
|
47
|
-
process.exit(1);
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
program
|
|
52
|
-
.command("stop")
|
|
53
|
-
.description("Stop relay client background service")
|
|
54
|
-
.action(() => {
|
|
55
|
-
stopCommand();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
program
|
|
59
|
-
.command("status")
|
|
60
|
-
.description("Show pairing config, gateway URL, and background service status")
|
|
61
|
-
.action(() => {
|
|
62
|
-
statusCommand();
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
program
|
|
66
|
-
.command("install")
|
|
67
|
-
.description("Register as a background service (launchd on macOS, systemd --user on Linux)")
|
|
68
|
-
.action(() => {
|
|
69
|
-
installCommand();
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
program
|
|
73
|
-
.command("restart")
|
|
74
|
-
.description("Restart the relay background service")
|
|
75
|
-
.action(() => {
|
|
76
|
-
restartCommand();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
program
|
|
80
|
-
.command("uninstall")
|
|
81
|
-
.description("Remove background service")
|
|
82
|
-
.action(() => {
|
|
83
|
-
uninstallCommand();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
program
|
|
87
|
-
.command("set-token")
|
|
88
|
-
.description("Set the local OpenClaw gateway token (needed when using token auth)")
|
|
89
|
-
.action(async () => {
|
|
90
|
-
try {
|
|
91
|
-
await setTokenCommand();
|
|
92
|
-
} catch (err) {
|
|
93
|
-
console.error("Error:", err instanceof Error ? err.message : err);
|
|
94
|
-
process.exit(1);
|
|
95
|
-
}
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
program
|
|
99
|
-
.command("upload <file>")
|
|
100
|
-
.description("Upload a local file to OSS and print the public URL")
|
|
101
|
-
.option("--json", "Print machine-readable JSON output", false)
|
|
102
|
-
.action(async (file: string, opts: { json?: boolean }) => {
|
|
103
|
-
try {
|
|
104
|
-
await uploadCommand(file, { json: opts.json });
|
|
105
|
-
} catch (err) {
|
|
106
|
-
console.error("Error:", err instanceof Error ? err.message : err);
|
|
107
|
-
process.exit(1);
|
|
108
|
-
}
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
program
|
|
112
|
-
.command("send <file>")
|
|
113
|
-
.description("Upload a local file and send it back to PocketClaw as an assistant message")
|
|
114
|
-
.option("--json", "Print machine-readable JSON output", false)
|
|
115
|
-
.action(async (file: string, opts: { json?: boolean }) => {
|
|
116
|
-
try {
|
|
117
|
-
await sendCommand(file, { json: opts.json });
|
|
118
|
-
} catch (err) {
|
|
119
|
-
console.error("Error:", err instanceof Error ? err.message : err);
|
|
120
|
-
process.exit(1);
|
|
121
|
-
}
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
program
|
|
125
|
-
.command("assistant-send")
|
|
126
|
-
.description("Send a synthetic assistant message to the current PocketClaw session")
|
|
127
|
-
.option("--session-key <key>", "Explicit session key to target")
|
|
128
|
-
.option("--url <url>", "Attachment URL to include")
|
|
129
|
-
.option("--mime-type <mimeType>", "Attachment MIME type")
|
|
130
|
-
.option("--file-name <fileName>", "Attachment file name")
|
|
131
|
-
.option("--message <text>", "Assistant text content", "")
|
|
132
|
-
.option("--thumbnail-url <url>", "Optional thumbnail URL for video attachments")
|
|
133
|
-
.option("--width <px>", "Attachment width")
|
|
134
|
-
.option("--height <px>", "Attachment height")
|
|
135
|
-
.option("--size <bytes>", "Attachment size in bytes")
|
|
136
|
-
.option("--duration-ms <ms>", "Attachment duration in milliseconds")
|
|
137
|
-
.option("--json", "Print machine-readable JSON output", false)
|
|
138
|
-
.action(async (opts: {
|
|
139
|
-
sessionKey?: string;
|
|
140
|
-
url?: string;
|
|
141
|
-
mimeType?: string;
|
|
142
|
-
fileName?: string;
|
|
143
|
-
message?: string;
|
|
144
|
-
thumbnailUrl?: string;
|
|
145
|
-
width?: string;
|
|
146
|
-
height?: string;
|
|
147
|
-
size?: string;
|
|
148
|
-
durationMs?: string;
|
|
149
|
-
json?: boolean;
|
|
150
|
-
}) => {
|
|
151
|
-
try {
|
|
152
|
-
await assistantSendCommand(opts);
|
|
153
|
-
} catch (err) {
|
|
154
|
-
console.error("Error:", err instanceof Error ? err.message : err);
|
|
155
|
-
process.exit(1);
|
|
156
|
-
}
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
program
|
|
160
|
-
.command("reset")
|
|
161
|
-
.description("Clear saved config and stop service — use when switching servers or on auth errors")
|
|
162
|
-
.action(() => {
|
|
163
|
-
resetCommand();
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
program.parse(process.argv);
|
|
@@ -1,205 +0,0 @@
|
|
|
1
|
-
// ---------------------------------------------------------------------------
|
|
2
|
-
// Extract media (image / video) blocks from an OpenClaw gateway chat history
|
|
3
|
-
// response, and upload them to OSS via STS credentials from the relay server.
|
|
4
|
-
// ---------------------------------------------------------------------------
|
|
5
|
-
|
|
6
|
-
import { uploadMedia, type StsCredentials, type UploadResult } from "./oss-uploader.js";
|
|
7
|
-
import { readFile } from "fs/promises";
|
|
8
|
-
|
|
9
|
-
// Max video size accepted for upload (200 MB)
|
|
10
|
-
const MAX_VIDEO_BYTES = 200 * 1024 * 1024;
|
|
11
|
-
|
|
12
|
-
export interface MediaBlock {
|
|
13
|
-
mimeType: string;
|
|
14
|
-
/** Buffer content (decoded from base64 or read from local path) */
|
|
15
|
-
data: Buffer;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface AssistantAttachment extends UploadResult {
|
|
19
|
-
width?: number;
|
|
20
|
-
height?: number;
|
|
21
|
-
durationMs?: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
// ---------------------------------------------------------------------------
|
|
25
|
-
// Extract media blocks from the last assistant message in a history response
|
|
26
|
-
// ---------------------------------------------------------------------------
|
|
27
|
-
|
|
28
|
-
type ContentBlock = {
|
|
29
|
-
type: string;
|
|
30
|
-
text?: string;
|
|
31
|
-
source?: {
|
|
32
|
-
type?: string; // "base64" | "url" | "file" | null
|
|
33
|
-
media_type?: string; // "image/png" etc.
|
|
34
|
-
data?: string; // base64 payload
|
|
35
|
-
url?: string; // external URL (not handled in Phase 1)
|
|
36
|
-
path?: string; // local file path
|
|
37
|
-
};
|
|
38
|
-
// Some gateways embed media_type / data directly on the block
|
|
39
|
-
media_type?: string;
|
|
40
|
-
data?: string;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
type HistoryMessage = {
|
|
44
|
-
role: string;
|
|
45
|
-
content?: ContentBlock[] | string;
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
export function extractMediaBlocks(history: { messages?: HistoryMessage[] }): MediaBlock[] {
|
|
49
|
-
const msgs = history.messages ?? [];
|
|
50
|
-
const last = [...msgs].reverse().find((m) => m.role === "assistant");
|
|
51
|
-
if (!last || !last.content) return [];
|
|
52
|
-
|
|
53
|
-
const content: ContentBlock[] =
|
|
54
|
-
typeof last.content === "string" ? [] : last.content;
|
|
55
|
-
|
|
56
|
-
const blocks: MediaBlock[] = [];
|
|
57
|
-
|
|
58
|
-
for (const block of content) {
|
|
59
|
-
if (block.type !== "image" && block.type !== "video") continue;
|
|
60
|
-
|
|
61
|
-
const source = block.source;
|
|
62
|
-
const mimeType =
|
|
63
|
-
source?.media_type ?? block.media_type ?? "image/jpeg";
|
|
64
|
-
|
|
65
|
-
// Only handle base64 and local file paths in Phase 1
|
|
66
|
-
if (source?.type === "base64" || (!source?.type && source?.data)) {
|
|
67
|
-
const raw = source?.data ?? block.data ?? "";
|
|
68
|
-
if (!raw) continue;
|
|
69
|
-
try {
|
|
70
|
-
const buf = Buffer.from(raw.replace(/^data:[^;]+;base64,/, ""), "base64");
|
|
71
|
-
blocks.push({ mimeType, data: buf });
|
|
72
|
-
} catch {
|
|
73
|
-
console.warn("[media] failed to decode base64 block, skipping");
|
|
74
|
-
}
|
|
75
|
-
} else if (source?.type === "file" && source.path) {
|
|
76
|
-
// Local file reference — will be read asynchronously below
|
|
77
|
-
blocks.push({ mimeType, data: Buffer.alloc(0), _localPath: source.path } as MediaBlock & { _localPath: string });
|
|
78
|
-
}
|
|
79
|
-
// URL type is not handled in Phase 1
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return blocks;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ---------------------------------------------------------------------------
|
|
86
|
-
// Load local-path blocks and filter oversized videos
|
|
87
|
-
// ---------------------------------------------------------------------------
|
|
88
|
-
|
|
89
|
-
async function resolveLocalPaths(blocks: (MediaBlock & { _localPath?: string })[]): Promise<MediaBlock[]> {
|
|
90
|
-
const resolved: MediaBlock[] = [];
|
|
91
|
-
for (const b of blocks) {
|
|
92
|
-
if ((b as any)._localPath) {
|
|
93
|
-
try {
|
|
94
|
-
const buf = await readFile((b as any)._localPath as string);
|
|
95
|
-
resolved.push({ mimeType: b.mimeType, data: buf });
|
|
96
|
-
} catch (err) {
|
|
97
|
-
console.warn(`[media] failed to read local file ${(b as any)._localPath}: ${err}`);
|
|
98
|
-
}
|
|
99
|
-
} else {
|
|
100
|
-
resolved.push(b);
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
return resolved.filter((b) => {
|
|
104
|
-
if (b.mimeType.startsWith("video/") && b.data.length > MAX_VIDEO_BYTES) {
|
|
105
|
-
console.warn(`[media] video too large (${b.data.length} bytes), skipping`);
|
|
106
|
-
return false;
|
|
107
|
-
}
|
|
108
|
-
return b.data.length > 0;
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ---------------------------------------------------------------------------
|
|
113
|
-
// Fetch STS credentials from the relay server
|
|
114
|
-
// ---------------------------------------------------------------------------
|
|
115
|
-
|
|
116
|
-
async function fetchSts(
|
|
117
|
-
relayServerUrl: string,
|
|
118
|
-
gatewayId: string,
|
|
119
|
-
relaySecret: string,
|
|
120
|
-
mimeTypes: string[],
|
|
121
|
-
hasVideo: boolean
|
|
122
|
-
): Promise<StsCredentials> {
|
|
123
|
-
const url = `${relayServerUrl.replace(/\/$/, "")}/api/media/sts`;
|
|
124
|
-
const res = await fetch(url, {
|
|
125
|
-
method: "POST",
|
|
126
|
-
headers: { "Content-Type": "application/json" },
|
|
127
|
-
body: JSON.stringify({
|
|
128
|
-
gatewayId,
|
|
129
|
-
relaySecret,
|
|
130
|
-
count: mimeTypes.length,
|
|
131
|
-
mimeTypes,
|
|
132
|
-
durationSeconds: hasVideo ? 1800 : 900,
|
|
133
|
-
}),
|
|
134
|
-
});
|
|
135
|
-
if (!res.ok) {
|
|
136
|
-
const text = await res.text().catch(() => "");
|
|
137
|
-
throw new Error(`STS fetch failed: ${res.status} ${text}`);
|
|
138
|
-
}
|
|
139
|
-
return res.json() as Promise<StsCredentials>;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// ---------------------------------------------------------------------------
|
|
143
|
-
// Main: extract + upload → return attachments[]
|
|
144
|
-
// ---------------------------------------------------------------------------
|
|
145
|
-
|
|
146
|
-
export async function uploadAssistantAttachments(
|
|
147
|
-
history: { messages?: HistoryMessage[] },
|
|
148
|
-
relayServerUrl: string,
|
|
149
|
-
gatewayId: string,
|
|
150
|
-
relaySecret: string
|
|
151
|
-
): Promise<AssistantAttachment[]> {
|
|
152
|
-
const rawBlocks = extractMediaBlocks(history);
|
|
153
|
-
if (rawBlocks.length === 0) return [];
|
|
154
|
-
|
|
155
|
-
const blocks = await resolveLocalPaths(rawBlocks as (MediaBlock & { _localPath?: string })[]);
|
|
156
|
-
if (blocks.length === 0) return [];
|
|
157
|
-
|
|
158
|
-
const mimeTypes = blocks.map((b) => b.mimeType);
|
|
159
|
-
const hasVideo = mimeTypes.some((m) => m.startsWith("video/"));
|
|
160
|
-
|
|
161
|
-
let sts: StsCredentials;
|
|
162
|
-
try {
|
|
163
|
-
sts = await fetchSts(relayServerUrl, gatewayId, relaySecret, mimeTypes, hasVideo);
|
|
164
|
-
} catch (err) {
|
|
165
|
-
console.error("[media] failed to fetch STS:", err);
|
|
166
|
-
return [];
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
const results: AssistantAttachment[] = [];
|
|
170
|
-
|
|
171
|
-
// Upload images concurrently (max 3), videos serially
|
|
172
|
-
const imageBlocks = blocks.filter((b) => !b.mimeType.startsWith("video/"));
|
|
173
|
-
const videoBlocks = blocks.filter((b) => b.mimeType.startsWith("video/"));
|
|
174
|
-
|
|
175
|
-
// Images: up to 3 concurrent
|
|
176
|
-
const imageBatches: MediaBlock[][] = [];
|
|
177
|
-
for (let i = 0; i < imageBlocks.length; i += 3) {
|
|
178
|
-
imageBatches.push(imageBlocks.slice(i, i + 3));
|
|
179
|
-
}
|
|
180
|
-
for (const batch of imageBatches) {
|
|
181
|
-
const settled = await Promise.allSettled(
|
|
182
|
-
batch.map((b) => uploadMedia(sts, b.data, b.mimeType))
|
|
183
|
-
);
|
|
184
|
-
for (const r of settled) {
|
|
185
|
-
if (r.status === "fulfilled") {
|
|
186
|
-
results.push(r.value);
|
|
187
|
-
} else {
|
|
188
|
-
console.warn("[media] image upload failed:", r.reason);
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
// Videos: serial
|
|
194
|
-
for (const b of videoBlocks) {
|
|
195
|
-
try {
|
|
196
|
-
const r = await uploadMedia(sts, b.data, b.mimeType);
|
|
197
|
-
results.push(r);
|
|
198
|
-
} catch (err) {
|
|
199
|
-
console.warn("[media] video upload failed:", err);
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
console.log(`[media] uploaded ${results.length}/${blocks.length} attachments`);
|
|
204
|
-
return results;
|
|
205
|
-
}
|