@foxden-app/foxclaw 0.5.67 → 0.5.69

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/.env.example CHANGED
@@ -45,6 +45,11 @@ DEFAULT_APPROVAL_POLICY=on-request
45
45
  DEFAULT_SANDBOX_MODE=workspace-write
46
46
  TELEGRAM_POLL_INTERVAL_MS=1200
47
47
  TELEGRAM_PREVIEW_THROTTLE_MS=800
48
+ # Optional: route Telegram Bot API calls through a Local Bot API Server.
49
+ # Useful for large video/media delivery; official local server supports much
50
+ # larger uploads than the public https://api.telegram.org endpoint.
51
+ # TELEGRAM_BOT_API_BASE_URL=http://127.0.0.1:8081
52
+ # TELEGRAM_BOT_API_TIMEOUT_MS=1800000
48
53
  # Delete folded operation-detail messages after the final answer is sent.
49
54
  # Set to false if you prefer to keep "read files / edited files / ran command"
50
55
  # detail cards in the chat history.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,26 @@
2
2
 
3
3
  All notable FoxClaw changes are listed here. Each release note is bilingual so GitHub Releases and the npm package are useful to both Chinese and English readers.
4
4
 
5
+ ## 0.5.69 - 2026-07-11
6
+
7
+ ### 中文
8
+ - 修复 pnpm 11 的自更新路径解析:`pnpm root -g` 在 pnpm 11 返回全局目录本身(例如 `.../global/v11`),而不是旧版返回的 `node_modules` 目录。FoxClaw 现在同时识别两种布局,升级后可以正确执行自检和服务重启。
9
+
10
+ ### English
11
+ - Fixed self-update path resolution for pnpm 11. `pnpm root -g` now returns the global directory itself (for example `.../global/v11`) rather than the `node_modules` directory returned by earlier pnpm versions. FoxClaw now recognizes both layouts and can correctly run post-update checks and restart the service.
12
+
13
+ ## 0.5.68 - 2026-07-01
14
+
15
+ ### 中文
16
+ - 新增 `foxclaw send-media <path> [caption]`,Codex 生成图片、封面、截图、GIF、视频或其他文件后,可以直接投递到当前 Telegram 私聊;FoxClaw 会按文件类型选择 `sendPhoto`、`sendVideo`、`sendAnimation` 或 `sendDocument`。
17
+ - 新增内置 `telegram-media-delivery` skill,并随 Telegram runtime 自动同步到 `CODEX_HOME`,让 Codex 像发送语音一样发送视觉产物;`sendVideo` 默认启用 `supports_streaming=true`,提升 MP4 在 Telegram 客户端中的在线播放体验。
18
+ - Telegram Bot API endpoint 现在可通过 `TELEGRAM_BOT_API_BASE_URL` 配置为 Local Bot API Server,并可用 `TELEGRAM_BOT_API_TIMEOUT_MS` 调大上传超时,适合交付 25 分钟这类大视频。
19
+
20
+ ### English
21
+ - Added `foxclaw send-media <path> [caption]`, allowing Codex-generated images, covers, screenshots, GIFs, videos, and other files to be delivered directly to the current Telegram private chat. FoxClaw selects `sendPhoto`, `sendVideo`, `sendAnimation`, or `sendDocument` by file type.
22
+ - Added the bundled `telegram-media-delivery` skill and sync it into each Telegram runtime's `CODEX_HOME`, so Codex can deliver visual artifacts like it already delivers voice files. `sendVideo` enables `supports_streaming=true` for better inline MP4 playback in Telegram clients.
23
+ - Telegram Bot API endpoint calls can now use `TELEGRAM_BOT_API_BASE_URL` for a Local Bot API Server, with `TELEGRAM_BOT_API_TIMEOUT_MS` for longer uploads, making 25-minute video delivery practical when the file size exceeds public Bot API limits.
24
+
5
25
  ## 0.5.67 - 2026-07-01
6
26
 
7
27
  ### 中文
package/README.md CHANGED
@@ -310,7 +310,7 @@ foxclaw weixin-login
310
310
 
311
311
  ## Codex Skill
312
312
 
313
- 仓库自带 FoxClaw 安装维护 Skill 和 `telegram-voice-delivery` 语音投递 Skill。用法看 [FoxClaw Skill 中文说明](./docs/zh/foxclaw-skill.md)。FoxClaw 启动时会把语音投递 Skill 自动同步到每个 Telegram runtime 的 `CODEX_HOME`;Codex 生成音频后可直接执行 `foxclaw send-voice`,自动把文件送回当前 Telegram 私聊,无需用户再输入 `/voice file`。
313
+ 仓库自带 FoxClaw 安装维护 Skill、`telegram-voice-delivery` 语音投递 Skill 和 `telegram-media-delivery` 媒体投递 Skill。用法看 [FoxClaw Skill 中文说明](./docs/zh/foxclaw-skill.md)。FoxClaw 启动时会把投递 Skill 自动同步到每个 Telegram runtime 的 `CODEX_HOME`;Codex 生成音频、图片、封面、截图或视频后可直接执行 `foxclaw send-voice` / `foxclaw send-media`,自动把文件送回当前 Telegram 私聊。
314
314
 
315
315
  ## 故障排查
316
316
 
package/README_EN.md CHANGED
@@ -310,7 +310,7 @@ Weixin runtime files default to `~/.foxclaw/weixin`. When `TG_BOT_TOKENS` is ena
310
310
 
311
311
  ## Codex Skill
312
312
 
313
- This repo ships the FoxClaw deployment skill and a [`telegram-voice-delivery`](./skills/telegram-voice-delivery) skill. FoxClaw automatically syncs the delivery skill into every Telegram runtime's `CODEX_HOME` at startup. Codex can then run `foxclaw send-voice` after generating audio and deliver the file to the current Telegram private chat without asking the user to enter `/voice file`.
313
+ This repo ships the FoxClaw deployment skill, [`telegram-voice-delivery`](./skills/telegram-voice-delivery), and [`telegram-media-delivery`](./skills/telegram-media-delivery). FoxClaw automatically syncs the delivery skills into every Telegram runtime's `CODEX_HOME` at startup. Codex can then run `foxclaw send-voice` or `foxclaw send-media` after generating audio, images, covers, screenshots, or videos and deliver the file to the current Telegram private chat.
314
314
 
315
315
  ## Troubleshooting
316
316
 
@@ -1,6 +1,6 @@
1
1
  import fs from 'node:fs';
2
2
  import path from 'node:path';
3
- const BUNDLED_CODEX_SKILLS = ['telegram-voice-delivery'];
3
+ const BUNDLED_CODEX_SKILLS = ['telegram-voice-delivery', 'telegram-media-delivery'];
4
4
  export function installBundledCodexSkills(packageRoot, codexHome) {
5
5
  const installed = [];
6
6
  for (const skillName of BUNDLED_CODEX_SKILLS) {
package/dist/main.js CHANGED
@@ -36,6 +36,7 @@ const PROXY_ENV_KEYS = [
36
36
  const STANDARD_NODE_PROXY_ENV_KEYS = ['HTTP_PROXY', 'HTTPS_PROXY', 'http_proxy', 'https_proxy'];
37
37
  const LOCAL_AUTH_REFRESH_LEASE_TTL_MS = 10 * 60_000;
38
38
  const CLUSTER_UPDATE_BROADCAST_PATH = path.join(APP_HOME, 'runtime', 'pending-cluster-update-broadcast.json');
39
+ const TELEGRAM_PUBLIC_BOT_API_UPLOAD_HINT_BYTES = 50 * 1024 * 1024;
39
40
  function createClusterUpdateScheduler(options) {
40
41
  let pending = null;
41
42
  let timer = null;
@@ -236,6 +237,11 @@ async function main() {
236
237
  await runSendVoiceCli();
237
238
  return;
238
239
  }
240
+ if (command === 'send-media' || command === 'media-file') {
241
+ requireNode24(command);
242
+ await runSendMediaCli();
243
+ return;
244
+ }
239
245
  if (command !== 'serve') {
240
246
  console.error(`Unknown command: ${command}`);
241
247
  printUsage();
@@ -273,6 +279,7 @@ Usage:
273
279
  foxclaw start|restart|stop
274
280
  foxclaw update
275
281
  foxclaw send-voice <path> [caption]
282
+ foxclaw send-media <path> [caption]
276
283
  foxclaw install-systemd|uninstall-systemd
277
284
  foxclaw install-launchd|uninstall-launchd
278
285
  foxclaw weixin-login [account-id]
@@ -330,6 +337,59 @@ async function runSendVoiceCli() {
330
337
  }
331
338
  console.log(`Sent Telegram voice message ${result.result.message_id}: ${filePath}`);
332
339
  }
340
+ async function runSendMediaCli() {
341
+ const parsed = parseSendVoiceCliArgs(process.argv.slice(3));
342
+ if (!parsed.fileArg) {
343
+ console.error('Usage: foxclaw send-media <path> [caption] [--bot-id <bot-id>] [--chat-id <chat-id>]');
344
+ process.exitCode = 1;
345
+ return;
346
+ }
347
+ const config = loadConfig();
348
+ const filePath = path.resolve(config.defaultCwd, parsed.fileArg);
349
+ const stat = await fs.promises.stat(filePath).catch(() => null);
350
+ if (!stat?.isFile()) {
351
+ throw new Error(`Media file not found: ${filePath}`);
352
+ }
353
+ const plan = planTelegramOutboundMedia(filePath);
354
+ const inferredBotId = parsed.botId ?? inferTelegramBotId(process.env.CODEX_HOME) ?? inferTelegramBotId(config.codexHome);
355
+ const { botId, botToken } = resolveTelegramVoiceTarget(config.tgBotTokens, inferredBotId);
356
+ const { BridgeStore } = await import('./store/database.js');
357
+ const store = new BridgeStore(config.storePath);
358
+ let chatId = parsed.chatId;
359
+ try {
360
+ chatId ??= botId ? store.getTelegramPrivateScope(botId)?.chatId ?? null : null;
361
+ }
362
+ finally {
363
+ store.close();
364
+ }
365
+ if (!chatId) {
366
+ const target = botId ? ` for ${botId}` : '';
367
+ throw new Error(`No remembered Telegram private chat${target}. Send /status to the bot once, or pass --chat-id <chat-id>.`);
368
+ }
369
+ if (stat.size > TELEGRAM_PUBLIC_BOT_API_UPLOAD_HINT_BYTES && !isLocalTelegramBotApiConfigured()) {
370
+ console.warn([
371
+ `Media is ${formatBytes(stat.size)}. Public Telegram Bot API uploads may reject large files.`,
372
+ 'For long videos, configure TELEGRAM_BOT_API_BASE_URL to a Local Bot API Server.',
373
+ ].join(' '));
374
+ }
375
+ const contents = await fs.promises.readFile(filePath);
376
+ const { callTelegramMultipartApi } = await import('./telegram/api.js');
377
+ const fields = {
378
+ chat_id: chatId,
379
+ ...(parsed.caption ? { caption: parsed.caption } : {}),
380
+ ...(plan.kind === 'video' ? { supports_streaming: 'true' } : {}),
381
+ };
382
+ const result = await callTelegramMultipartApi(botToken, plan.method, fields, [{
383
+ fieldName: plan.fieldName,
384
+ filename: path.basename(filePath),
385
+ contents,
386
+ contentType: plan.contentType,
387
+ }]);
388
+ if (!result.ok || !result.result) {
389
+ throw new Error(result.description || `Telegram ${plan.method} failed.`);
390
+ }
391
+ console.log(`Sent Telegram ${plan.kind} message ${result.result.message_id}: ${filePath}`);
392
+ }
333
393
  function parseSendVoiceCliArgs(args) {
334
394
  const positional = [];
335
395
  let botId = null;
@@ -357,6 +417,52 @@ function parseSendVoiceCliArgs(args) {
357
417
  chatId,
358
418
  };
359
419
  }
420
+ function planTelegramOutboundMedia(filePath) {
421
+ const extension = path.extname(filePath).toLowerCase();
422
+ switch (extension) {
423
+ case '.jpg':
424
+ case '.jpeg':
425
+ return { kind: 'photo', method: 'sendPhoto', fieldName: 'photo', contentType: 'image/jpeg' };
426
+ case '.png':
427
+ return { kind: 'photo', method: 'sendPhoto', fieldName: 'photo', contentType: 'image/png' };
428
+ case '.webp':
429
+ return { kind: 'photo', method: 'sendPhoto', fieldName: 'photo', contentType: 'image/webp' };
430
+ case '.gif':
431
+ return { kind: 'animation', method: 'sendAnimation', fieldName: 'animation', contentType: 'image/gif' };
432
+ case '.mp4':
433
+ case '.m4v':
434
+ return { kind: 'video', method: 'sendVideo', fieldName: 'video', contentType: 'video/mp4' };
435
+ case '.mov':
436
+ return { kind: 'video', method: 'sendVideo', fieldName: 'video', contentType: 'video/quicktime' };
437
+ case '.pdf':
438
+ return { kind: 'document', method: 'sendDocument', fieldName: 'document', contentType: 'application/pdf' };
439
+ case '.json':
440
+ return { kind: 'document', method: 'sendDocument', fieldName: 'document', contentType: 'application/json' };
441
+ case '.md':
442
+ return { kind: 'document', method: 'sendDocument', fieldName: 'document', contentType: 'text/markdown' };
443
+ case '.txt':
444
+ return { kind: 'document', method: 'sendDocument', fieldName: 'document', contentType: 'text/plain' };
445
+ case '.zip':
446
+ return { kind: 'document', method: 'sendDocument', fieldName: 'document', contentType: 'application/zip' };
447
+ default:
448
+ return { kind: 'document', method: 'sendDocument', fieldName: 'document', contentType: 'application/octet-stream' };
449
+ }
450
+ }
451
+ function isLocalTelegramBotApiConfigured() {
452
+ const value = process.env.TELEGRAM_BOT_API_BASE_URL?.trim();
453
+ return Boolean(value && !/^https:\/\/api\.telegram\.org\/?$/i.test(value));
454
+ }
455
+ function formatBytes(bytes) {
456
+ if (bytes < 1024)
457
+ return `${bytes}B`;
458
+ const kib = bytes / 1024;
459
+ if (kib < 1024)
460
+ return `${kib.toFixed(1)}KiB`;
461
+ const mib = kib / 1024;
462
+ if (mib < 1024)
463
+ return `${mib.toFixed(1)}MiB`;
464
+ return `${(mib / 1024).toFixed(2)}GiB`;
465
+ }
360
466
  function formatRuntimeStatusSummary(status) {
361
467
  const lines = [];
362
468
  const age = formatAge(status.updatedAt);
@@ -1,17 +1,15 @@
1
1
  import fs from 'node:fs';
2
+ import http from 'node:http';
2
3
  import https from 'node:https';
3
4
  import path from 'node:path';
4
5
  import { pipeline } from 'node:stream/promises';
5
- const API_HOST = 'api.telegram.org';
6
+ const DEFAULT_API_BASE_URL = 'https://api.telegram.org';
6
7
  export async function callTelegramApi(botToken, method, body) {
7
8
  const payload = JSON.stringify(body);
9
+ const endpoint = telegramApiUrl(botToken, method);
8
10
  return new Promise((resolve, reject) => {
9
- const request = https.request({
10
- host: API_HOST,
11
- port: 443,
12
- path: `/bot${botToken}/${method}`,
11
+ const request = requestForUrl(endpoint, {
13
12
  method: 'POST',
14
- family: 4,
15
13
  headers: {
16
14
  'content-type': 'application/json',
17
15
  'content-length': Buffer.byteLength(payload),
@@ -32,7 +30,7 @@ export async function callTelegramApi(botToken, method, body) {
32
30
  });
33
31
  });
34
32
  request.on('error', reject);
35
- request.setTimeout(20_000, () => {
33
+ request.setTimeout(telegramApiTimeoutMs(false), () => {
36
34
  request.destroy(new Error(`Telegram API request timed out for ${method}`));
37
35
  });
38
36
  request.write(payload);
@@ -52,13 +50,10 @@ export async function callTelegramMultipartApi(botToken, method, fields, files)
52
50
  }
53
51
  parts.push(Buffer.from(`--${boundary}--\r\n`, 'utf8'));
54
52
  const payload = Buffer.concat(parts);
53
+ const endpoint = telegramApiUrl(botToken, method);
55
54
  return new Promise((resolve, reject) => {
56
- const request = https.request({
57
- host: API_HOST,
58
- port: 443,
59
- path: `/bot${botToken}/${method}`,
55
+ const request = requestForUrl(endpoint, {
60
56
  method: 'POST',
61
- family: 4,
62
57
  headers: {
63
58
  'content-type': `multipart/form-data; boundary=${boundary}`,
64
59
  'content-length': payload.length,
@@ -79,7 +74,7 @@ export async function callTelegramMultipartApi(botToken, method, fields, files)
79
74
  });
80
75
  });
81
76
  request.on('error', reject);
82
- request.setTimeout(20_000, () => {
77
+ request.setTimeout(telegramApiTimeoutMs(true), () => {
83
78
  request.destroy(new Error(`Telegram API request timed out for ${method}`));
84
79
  });
85
80
  request.write(payload);
@@ -98,13 +93,9 @@ export async function downloadTelegramFile(botToken, remoteFilePath, destination
98
93
  const tempPath = `${destinationPath}.tmp-${process.pid}-${Date.now()}`;
99
94
  let response = null;
100
95
  try {
96
+ const endpoint = telegramFileUrl(botToken, remoteFilePath);
101
97
  response = await new Promise((resolve, reject) => {
102
- const request = https.get({
103
- host: API_HOST,
104
- port: 443,
105
- path: `/file/bot${botToken}/${remoteFilePath}`,
106
- family: 4,
107
- }, (incoming) => {
98
+ const request = getForUrl(endpoint, (incoming) => {
108
99
  const statusCode = incoming.statusCode ?? 500;
109
100
  if (statusCode >= 400) {
110
101
  incoming.resume();
@@ -114,7 +105,7 @@ export async function downloadTelegramFile(botToken, remoteFilePath, destination
114
105
  resolve(incoming);
115
106
  });
116
107
  request.on('error', reject);
117
- request.setTimeout(20_000, () => {
108
+ request.setTimeout(telegramApiTimeoutMs(true), () => {
118
109
  request.destroy(new Error(`Telegram file download timed out for ${remoteFilePath}`));
119
110
  });
120
111
  });
@@ -137,3 +128,41 @@ export async function downloadTelegramFile(botToken, remoteFilePath, destination
137
128
  function escapeMultipartName(value) {
138
129
  return value.replaceAll('\\', '\\\\').replaceAll('"', '\\"').replaceAll('\r', '').replaceAll('\n', '');
139
130
  }
131
+ function telegramApiBaseUrl() {
132
+ const raw = process.env.TELEGRAM_BOT_API_BASE_URL?.trim() || DEFAULT_API_BASE_URL;
133
+ return raw.replace(/\/+$/g, '');
134
+ }
135
+ function telegramApiUrl(botToken, method) {
136
+ return new URL(`${telegramApiBaseUrl()}/bot${botToken}/${method}`);
137
+ }
138
+ function telegramFileUrl(botToken, remoteFilePath) {
139
+ return new URL(`${telegramApiBaseUrl()}/file/bot${botToken}/${remoteFilePath}`);
140
+ }
141
+ function telegramApiTimeoutMs(multipart) {
142
+ const raw = Number.parseInt(process.env.TELEGRAM_BOT_API_TIMEOUT_MS ?? '', 10);
143
+ if (Number.isFinite(raw) && raw > 0) {
144
+ return raw;
145
+ }
146
+ return multipart ? 300_000 : 20_000;
147
+ }
148
+ function requestForUrl(url, options, callback) {
149
+ const client = url.protocol === 'http:' ? http : https;
150
+ return client.request({
151
+ ...options,
152
+ protocol: url.protocol,
153
+ hostname: url.hostname,
154
+ port: url.port || undefined,
155
+ path: `${url.pathname}${url.search}`,
156
+ family: url.hostname === 'localhost' ? undefined : 4,
157
+ }, callback);
158
+ }
159
+ function getForUrl(url, callback) {
160
+ const client = url.protocol === 'http:' ? http : https;
161
+ return client.get({
162
+ protocol: url.protocol,
163
+ hostname: url.hostname,
164
+ port: url.port || undefined,
165
+ path: `${url.pathname}${url.search}`,
166
+ family: url.hostname === 'localhost' ? undefined : 4,
167
+ }, callback);
168
+ }
package/dist/update.d.ts CHANGED
@@ -85,5 +85,6 @@ export declare function performSelfUpdate(options: PerformSelfUpdateOptions): Se
85
85
  export declare function readPendingClusterUpdateBroadcast(filePath: string): PendingClusterUpdateBroadcast | null;
86
86
  export declare function writePendingClusterUpdateBroadcast(filePath: string, value: PendingClusterUpdateBroadcast): void;
87
87
  export declare function clearPendingClusterUpdateBroadcast(filePath: string): void;
88
+ export declare function resolveFoxclawEntryPointFromGlobalRoot(globalRoot: string, exists?: (target: string) => boolean): string | null;
88
89
  export declare function extractReleaseNotes(changelog: string, version: string, locale: AppLocale): string[] | null;
89
90
  export {};
package/dist/update.js CHANGED
@@ -472,6 +472,15 @@ function runInherited(command, args, env) {
472
472
  throw new Error(`${command} ${args.join(' ')} exited with status ${result.status ?? 'unknown'}.`);
473
473
  }
474
474
  }
475
+ export function resolveFoxclawEntryPointFromGlobalRoot(globalRoot, exists = fs.existsSync) {
476
+ // pnpm <= 10 prints the global node_modules directory; pnpm >= 11 prints
477
+ // the global directory itself. Accept both documented layouts.
478
+ const candidates = [
479
+ path.join(globalRoot, '@foxden-app', 'foxclaw', 'dist', 'main.js'),
480
+ path.join(globalRoot, 'node_modules', '@foxden-app', 'foxclaw', 'dist', 'main.js'),
481
+ ];
482
+ return candidates.find(candidate => exists(candidate)) ?? null;
483
+ }
475
484
  function resolveUpdatedEntryPoint(installer, env) {
476
485
  const result = spawnSync(installer.command, installer.rootArgs, { encoding: 'utf8', env });
477
486
  if (result.error) {
@@ -484,9 +493,9 @@ function resolveUpdatedEntryPoint(installer, env) {
484
493
  if (!globalRoot) {
485
494
  throw new Error(`Could not locate the updated global package root using ${installer.manager}.`);
486
495
  }
487
- const updatedEntryPoint = path.join(globalRoot, '@foxden-app', 'foxclaw', 'dist', 'main.js');
488
- if (!fs.existsSync(updatedEntryPoint)) {
489
- throw new Error(`Updated FoxClaw entry point was not found at ${updatedEntryPoint}.`);
496
+ const updatedEntryPoint = resolveFoxclawEntryPointFromGlobalRoot(globalRoot);
497
+ if (!updatedEntryPoint) {
498
+ throw new Error(`Updated FoxClaw entry point was not found below ${globalRoot} (checked direct and node_modules layouts).`);
490
499
  }
491
500
  return updatedEntryPoint;
492
501
  }
@@ -250,6 +250,17 @@ Later commands are sorted by recent usage. Plain text, photos, and files continu
250
250
 
251
251
  These are useful when the phone-side model, permission, or provider behavior does not match what you expected.
252
252
 
253
+ ### 3.4 Media delivery CLI
254
+
255
+ Codex can deliver generated files back to the current Telegram private chat without asking you to run a Telegram command manually:
256
+
257
+ ```bash
258
+ foxclaw send-voice /absolute/path/audio.ogg "caption"
259
+ foxclaw send-media /absolute/path/output.mp4 "caption"
260
+ ```
261
+
262
+ `send-media` auto-selects Telegram `sendPhoto`, `sendVideo`, `sendAnimation`, or `sendDocument` based on the file extension. MP4 videos are sent with `supports_streaming=true`, so Telegram clients can play them inline when the file is encoded compatibly. For long videos, prefer H.264/AAC MP4. Public Bot API uploads can reject large files; configure a Local Bot API Server with `TELEGRAM_BOT_API_BASE_URL` and a longer `TELEGRAM_BOT_API_TIMEOUT_MS` when delivering large media.
263
+
253
264
  ## 4. The `/setup` Panel
254
265
 
255
266
  `/setup` is one of the main mobile panels. Settings are scoped to the chat, so private chat, group, and topic can use different settings.
@@ -2,7 +2,7 @@
2
2
 
3
3
  仓库内置的 `skills/foxclaw` 是给 Codex 使用的安装技能。它的用途是让 Codex 在本机或远程 Mac 上自动完成 FoxClaw bootstrap,包括写 `.env`、安装依赖、构建、跑 `doctor`、安装 launchd 服务,并引导你完成第一次 Telegram 消息验证。
4
4
 
5
- 仓库还内置 `skills/telegram-voice-delivery`。FoxClaw 启动时会把它自动同步到每个 Telegram runtime 的 `CODEX_HOME`。当 Codex 已经生成音频时,它会直接运行 `foxclaw send-voice <path> [caption]`,从当前 `CODEX_HOME` 识别 Telegram bot,并将音频投递到该 bot 最近记录的私聊,不需要用户再输入命令。
5
+ 仓库还内置 `skills/telegram-voice-delivery` 和 `skills/telegram-media-delivery`。FoxClaw 启动时会把它们自动同步到每个 Telegram runtime 的 `CODEX_HOME`。当 Codex 已经生成音频、图片、封面、截图或视频时,它会直接运行 `foxclaw send-voice <path> [caption]` 或 `foxclaw send-media <path> [caption]`,从当前 `CODEX_HOME` 识别 Telegram bot,并将文件投递到该 bot 最近记录的私聊,不需要用户再输入命令。
6
6
 
7
7
  ## 适合什么时候用
8
8
 
@@ -250,6 +250,17 @@ TG_ALLOWED_TOPIC_ID=42
250
250
 
251
251
  这些命令适合排查“为什么手机上跑出来的权限、模型或 provider 和预期不同”。
252
252
 
253
+ ### 3.4 媒体投递 CLI
254
+
255
+ Codex 可以把已经生成的文件直接送回当前 Telegram 私聊,不需要你手动输入 Telegram 命令:
256
+
257
+ ```bash
258
+ foxclaw send-voice /absolute/path/audio.ogg "说明"
259
+ foxclaw send-media /absolute/path/output.mp4 "说明"
260
+ ```
261
+
262
+ `send-media` 会按文件扩展名自动选择 Telegram `sendPhoto`、`sendVideo`、`sendAnimation` 或 `sendDocument`。MP4 视频会带 `supports_streaming=true`,只要编码兼容,Telegram 客户端就可以内联播放。长视频建议使用 H.264/AAC MP4。公网 Bot API 上传大文件可能被拒绝;需要交付大视频时,建议配置 Local Bot API Server,并设置 `TELEGRAM_BOT_API_BASE_URL` 和更长的 `TELEGRAM_BOT_API_TIMEOUT_MS`。
263
+
253
264
  ## 4. `/setup` 会话设置面板
254
265
 
255
266
  `/setup` 是手机端最重要的面板之一。它按聊天范围保存设置,私聊、群组、topic 可以有不同设置。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@foxden-app/foxclaw",
3
- "version": "0.5.67",
3
+ "version": "0.5.69",
4
4
  "description": "Foxden local execution claw for controlling Codex from trusted chat interfaces.",
5
5
  "type": "module",
6
6
  "main": "dist/main.js",
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: telegram-media-delivery
3
+ description: 通过 FoxClaw 将 Codex 已生成或找到的图片、封面、截图、视频、GIF 或其他媒体文件直接发送到当前 Telegram 会话。需要把视觉产物交付给用户且不应要求用户手动执行 FoxClaw 命令时使用。
4
+ ---
5
+
6
+ # Telegram 媒体投递
7
+
8
+ 把已经完成的图片、视频或其他媒体文件直接发送到当前 FoxClaw Telegram 私聊,不要让用户手动下载或操作 `/media`。
9
+
10
+ ## 操作流程
11
+
12
+ 1. 确认文件已经完整生成,并优先使用绝对路径。
13
+ 2. 图片优先使用 `.jpg`、`.jpeg`、`.png` 或 `.webp`。
14
+ 3. 视频优先使用可流式播放的 `.mp4`,编码建议 H.264/AAC;`.mov` 可尝试发送,但客户端兼容性不如 MP4。
15
+ 4. GIF 使用 `.gif`,FoxClaw 会按 Telegram animation 发送。
16
+ 5. 执行:
17
+
18
+ ```bash
19
+ foxclaw send-media "/文件的绝对路径/output.mp4" "简短说明"
20
+ ```
21
+
22
+ 6. 命令成功即表示 Telegram 已接收媒体;随后简短告知用户已经发送。
23
+
24
+ FoxClaw 会从 `CODEX_HOME` 推断当前 Telegram bot;单 bot 默认 runtime 会从唯一配置的 token 补全 bot ID。随后从本地数据库读取该 bot 最近记录的私聊,并调用 Telegram `sendPhoto`、`sendVideo`、`sendAnimation` 或 `sendDocument`。不要读取、打印或泄露 Telegram bot token。
25
+
26
+ 文件已经存在时直接发送,不要重新生成。只有 FoxClaw 明确报告没有记录私聊时,才请用户对当前 bot 发送一次 `/status`;仅在自动会话推断确实不可用时使用 `--bot-id <bot-id>` 或 `--chat-id <chat-id>`,不要凭猜测指定其他 bot。
27
+
28
+ ## 大视频
29
+
30
+ 公网 Telegram Bot API 对大文件上传限制更严格。25 分钟视频通常应先压成可流式播放 MP4,并尽量控制体积;如果文件明显超过几十 MB,优先建议 FoxClaw 主机配置 Local Bot API Server,并设置:
31
+
32
+ ```bash
33
+ TELEGRAM_BOT_API_BASE_URL=http://127.0.0.1:8081
34
+ TELEGRAM_BOT_API_TIMEOUT_MS=1800000
35
+ ```
36
+
37
+ Local Bot API Server 支持更大的上传体积;公网 Bot API 失败时,不要反复重试同一个大文件,先压缩或改用本地 Bot API Server。