@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.
Files changed (43) hide show
  1. package/dist/commands/install.js +17 -18
  2. package/dist/commands/install.js.map +1 -1
  3. package/dist/commands/openclaw-cli.js +23 -3
  4. package/dist/commands/openclaw-cli.js.map +1 -1
  5. package/dist/commands/pair.js +8 -1
  6. package/dist/commands/pair.js.map +1 -1
  7. package/dist/commands/status.js +3 -0
  8. package/dist/commands/status.js.map +1 -1
  9. package/dist/i18n/index.js +6 -0
  10. package/dist/i18n/index.js.map +1 -1
  11. package/dist/index.js +1 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/platform/service-manager.d.ts +6 -0
  14. package/dist/platform/service-manager.js +384 -42
  15. package/dist/platform/service-manager.js.map +1 -1
  16. package/package.json +8 -1
  17. package/src/commands/assistant-send.ts +0 -91
  18. package/src/commands/install.ts +0 -128
  19. package/src/commands/local-handlers.ts +0 -533
  20. package/src/commands/openclaw-cli.ts +0 -329
  21. package/src/commands/pair.ts +0 -120
  22. package/src/commands/provider-config.ts +0 -275
  23. package/src/commands/provider-handlers.ts +0 -184
  24. package/src/commands/provider-registry.ts +0 -138
  25. package/src/commands/run.ts +0 -34
  26. package/src/commands/send.ts +0 -42
  27. package/src/commands/session-key.ts +0 -77
  28. package/src/commands/set-token.ts +0 -45
  29. package/src/commands/status.ts +0 -154
  30. package/src/commands/upload.ts +0 -141
  31. package/src/config/config.ts +0 -137
  32. package/src/generated/build-config.ts +0 -1
  33. package/src/i18n/index.ts +0 -179
  34. package/src/index.ts +0 -166
  35. package/src/media/assistant-attachments.ts +0 -205
  36. package/src/media/oss-uploader.ts +0 -306
  37. package/src/platform/service-manager.ts +0 -570
  38. package/src/relay/gateway-client.ts +0 -359
  39. package/src/relay/reconnect.ts +0 -37
  40. package/src/relay/relay-manager.ts +0 -328
  41. package/test-chat.mjs +0 -64
  42. package/test-direct.mjs +0 -171
  43. 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
- }