@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 +5 -0
- package/CHANGELOG.md +20 -0
- package/README.md +1 -1
- package/README_EN.md +1 -1
- package/dist/codex_skills.js +1 -1
- package/dist/main.js +106 -0
- package/dist/telegram/api.js +49 -20
- package/dist/update.d.ts +1 -0
- package/dist/update.js +12 -3
- package/docs/user-manual.md +11 -0
- package/docs/zh/foxclaw-skill.md +1 -1
- package/docs/zh/user-manual.md +11 -0
- package/package.json +1 -1
- package/skills/telegram-media-delivery/SKILL.md +37 -0
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-
|
|
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
|
|
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
|
|
package/dist/codex_skills.js
CHANGED
|
@@ -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);
|
package/dist/telegram/api.js
CHANGED
|
@@ -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
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
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(
|
|
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 =
|
|
488
|
-
if (!
|
|
489
|
-
throw new Error(`Updated FoxClaw entry point was not found
|
|
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
|
}
|
package/docs/user-manual.md
CHANGED
|
@@ -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.
|
package/docs/zh/foxclaw-skill.md
CHANGED
|
@@ -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
|
|
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
|
|
package/docs/zh/user-manual.md
CHANGED
|
@@ -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
|
@@ -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。
|