@foxden-app/foxclaw 0.5.31 → 0.5.33
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/CHANGELOG.md +22 -0
- package/README.md +9 -1
- package/README_EN.md +8 -0
- package/dist/channels/bridge_messaging_router.d.ts +2 -0
- package/dist/channels/bridge_messaging_router.js +12 -0
- package/dist/channels/telegram/telegram_messaging_port.d.ts +2 -0
- package/dist/channels/telegram/telegram_messaging_port.js +9 -0
- package/dist/controller/controller.d.ts +3 -0
- package/dist/controller/controller.js +124 -13
- package/dist/controller/presentation.js +1 -6
- package/dist/i18n.d.ts +2 -0
- package/dist/i18n.js +3 -0
- package/dist/launchd.d.ts +18 -0
- package/dist/launchd.js +75 -0
- package/dist/main.js +78 -58
- package/dist/telegram/gateway.d.ts +10 -0
- package/dist/telegram/gateway.js +34 -0
- package/dist/telegram/html.d.ts +8 -0
- package/dist/telegram/html.js +31 -0
- package/dist/telegram/rich.d.ts +13 -0
- package/dist/telegram/rich.js +12 -0
- package/docs/agent-assisted-install.md +1 -0
- package/docs/install-for-beginners.md +13 -2
- package/docs/telegram-rich-messages.md +99 -0
- package/docs/troubleshooting.md +32 -2
- package/docs/user-manual.md +12 -1
- package/docs/zh/agent-assisted-install.md +1 -0
- package/docs/zh/install-for-beginners.md +12 -1
- package/docs/zh/telegram-rich-messages.md +125 -0
- package/docs/zh/troubleshooting.md +32 -2
- package/docs/zh/user-manual.md +12 -1
- package/package.json +2 -1
- package/skills/foxclaw/SKILL.md +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
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.33 - 2026-06-16
|
|
6
|
+
|
|
7
|
+
### 中文
|
|
8
|
+
- 新增 Telegram Rich Message 适配专项盘点,明确现有 HTML 通道、Bot API 10.1 rich message 能力、FoxClaw 可用功能面和分阶段接入路线。
|
|
9
|
+
- 接入 `sendRichMessage` / rich HTML 发送链路,新增 `/rich` 诊断命令用于在 Telegram 客户端直接查看 heading、table、details、pre/code、list 的 RichMessage 渲染效果。
|
|
10
|
+
- 集中 Telegram HTML 转义与常用标签 helper,并把 `/diff` 改为优先使用 RichMessage details + diff code block;发送失败时回退到原 Telegram HTML 折叠展示。
|
|
11
|
+
|
|
12
|
+
### English
|
|
13
|
+
- Added a Telegram Rich Message adaptation check covering the current HTML path, Bot API 10.1 rich message capabilities, FoxClaw candidate surfaces, and a phased rollout plan.
|
|
14
|
+
- Wired the `sendRichMessage` / rich HTML send path and added `/rich` as a diagnostic command for checking heading, table, details, pre/code, and list rendering in Telegram clients.
|
|
15
|
+
- Centralized Telegram HTML escaping/tag helpers and changed `/diff` to prefer RichMessage details plus a diff code block, falling back to the previous Telegram HTML collapsible rendering if rich sending fails.
|
|
16
|
+
|
|
17
|
+
## 0.5.32 - 2026-06-11
|
|
18
|
+
|
|
19
|
+
### 中文
|
|
20
|
+
- 补齐 macOS launchd 适配检查:`doctor` 现在会检查已安装 plist 中记录的 Node 路径是否存在且为 Node 24+,并新增 `uninstall-launchd` 命令。
|
|
21
|
+
- 新增 launchd plist 生成/解析的单元测试,并补充 macOS 服务状态、日志、代理和 Node 路径排障文档。
|
|
22
|
+
|
|
23
|
+
### English
|
|
24
|
+
- Filled macOS launchd adaptation gaps: `doctor` now checks that the Node path recorded in the installed plist exists and is Node 24+, and `uninstall-launchd` is available.
|
|
25
|
+
- Added unit coverage for launchd plist generation/parsing and expanded macOS service status, log, proxy, and Node-path troubleshooting docs.
|
|
26
|
+
|
|
5
27
|
## 0.5.31 - 2026-06-09
|
|
6
28
|
|
|
7
29
|
### 中文
|
package/README.md
CHANGED
|
@@ -148,13 +148,20 @@ FoxClaw 的一大特色是自动多账号切换。当一个账号触发用量限
|
|
|
148
148
|
foxclaw start
|
|
149
149
|
```
|
|
150
150
|
|
|
151
|
-
Linux 上会安装/重启用户级 systemd 服务,macOS 上安装/重载 launchd
|
|
151
|
+
Linux 上会安装/重启用户级 systemd 服务,macOS 上安装/重载 launchd。Linux 查看状态:
|
|
152
152
|
|
|
153
153
|
```bash
|
|
154
154
|
systemctl --user status foxclaw.service
|
|
155
155
|
journalctl --user -u foxclaw.service -f
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
macOS 查看状态和启动日志:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
162
|
+
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
163
|
+
```
|
|
164
|
+
|
|
158
165
|
也可以直接用包装命令:
|
|
159
166
|
|
|
160
167
|
```bash
|
|
@@ -319,6 +326,7 @@ foxclaw restart
|
|
|
319
326
|
foxclaw update
|
|
320
327
|
foxclaw stop
|
|
321
328
|
foxclaw uninstall-systemd
|
|
329
|
+
foxclaw uninstall-launchd
|
|
322
330
|
```
|
|
323
331
|
|
|
324
332
|
## 贡献
|
package/README_EN.md
CHANGED
|
@@ -155,6 +155,13 @@ systemctl --user status foxclaw.service
|
|
|
155
155
|
journalctl --user -u foxclaw.service -f
|
|
156
156
|
```
|
|
157
157
|
|
|
158
|
+
To inspect macOS launchd state and startup logs:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
launchctl print "gui/$(id -u)/app.foxden.foxclaw"
|
|
162
|
+
tail -f ~/.foxclaw/logs/launchd.err.log ~/.foxclaw/logs/service.log
|
|
163
|
+
```
|
|
164
|
+
|
|
158
165
|
You can also use the wrapper commands:
|
|
159
166
|
|
|
160
167
|
```bash
|
|
@@ -319,6 +326,7 @@ foxclaw restart
|
|
|
319
326
|
foxclaw update
|
|
320
327
|
foxclaw stop
|
|
321
328
|
foxclaw uninstall-systemd
|
|
329
|
+
foxclaw uninstall-launchd
|
|
322
330
|
```
|
|
323
331
|
|
|
324
332
|
## Contributing
|
|
@@ -15,8 +15,10 @@ export declare class BridgeMessagingRouter {
|
|
|
15
15
|
private requireWeixinTransport;
|
|
16
16
|
sendPlain(scopeId: string, text: string, keyboard?: InlineKeyboard): Promise<number>;
|
|
17
17
|
sendHtml(scopeId: string, text: string, keyboard?: InlineKeyboard): Promise<number>;
|
|
18
|
+
sendRichHtml(scopeId: string, html: string, fallbackHtml: string, keyboard?: InlineKeyboard): Promise<number>;
|
|
18
19
|
editPlain(scopeId: string, messageId: number, text: string, keyboard?: InlineKeyboard): Promise<void>;
|
|
19
20
|
editHtml(scopeId: string, messageId: number, text: string, keyboard?: InlineKeyboard): Promise<void>;
|
|
21
|
+
editRichHtml(scopeId: string, messageId: number, html: string, fallbackHtml: string, keyboard?: InlineKeyboard): Promise<void>;
|
|
20
22
|
deleteMessage(scopeId: string, messageId: number): Promise<void>;
|
|
21
23
|
sendTypingInScope(scopeId: string): Promise<void>;
|
|
22
24
|
clearInlineKeyboard(scopeId: string, messageId: number): Promise<void>;
|
|
@@ -37,6 +37,12 @@ export class BridgeMessagingRouter {
|
|
|
37
37
|
}
|
|
38
38
|
return this.telegram.sendHtml(scopeId, text, keyboard);
|
|
39
39
|
}
|
|
40
|
+
sendRichHtml(scopeId, html, fallbackHtml, keyboard) {
|
|
41
|
+
if (this.isWeixinScope(scopeId)) {
|
|
42
|
+
return this.requireWeixinTransport(scopeId).sendHtml(scopeId, fallbackHtml, keyboard);
|
|
43
|
+
}
|
|
44
|
+
return this.telegram.sendRichHtml(scopeId, html, keyboard);
|
|
45
|
+
}
|
|
40
46
|
editPlain(scopeId, messageId, text, keyboard) {
|
|
41
47
|
if (this.isWeixinScope(scopeId)) {
|
|
42
48
|
return this.requireWeixinTransport(scopeId).editPlain(scopeId, messageId, text, keyboard);
|
|
@@ -49,6 +55,12 @@ export class BridgeMessagingRouter {
|
|
|
49
55
|
}
|
|
50
56
|
return this.telegram.editHtml(scopeId, messageId, text, keyboard);
|
|
51
57
|
}
|
|
58
|
+
editRichHtml(scopeId, messageId, html, fallbackHtml, keyboard) {
|
|
59
|
+
if (this.isWeixinScope(scopeId)) {
|
|
60
|
+
return this.requireWeixinTransport(scopeId).editHtml(scopeId, messageId, fallbackHtml, keyboard);
|
|
61
|
+
}
|
|
62
|
+
return this.telegram.editRichHtml(scopeId, messageId, html, keyboard);
|
|
63
|
+
}
|
|
52
64
|
deleteMessage(scopeId, messageId) {
|
|
53
65
|
if (this.isWeixinScope(scopeId)) {
|
|
54
66
|
return this.requireWeixinTransport(scopeId).deleteMessage(scopeId, messageId);
|
|
@@ -13,8 +13,10 @@ export declare class TelegramMessagingPort implements ChannelPort {
|
|
|
13
13
|
constructor(gateway: TelegramGateway);
|
|
14
14
|
sendPlain(bridgeScopeId: string, text: string, inlineKeyboard?: InlineKeyboard): Promise<number>;
|
|
15
15
|
sendHtml(bridgeScopeId: string, text: string, inlineKeyboard?: InlineKeyboard): Promise<number>;
|
|
16
|
+
sendRichHtml(bridgeScopeId: string, html: string, inlineKeyboard?: InlineKeyboard): Promise<number>;
|
|
16
17
|
editPlain(bridgeScopeId: string, messageId: number, text: string, inlineKeyboard?: InlineKeyboard): Promise<void>;
|
|
17
18
|
editHtml(bridgeScopeId: string, messageId: number, text: string, inlineKeyboard?: InlineKeyboard): Promise<void>;
|
|
19
|
+
editRichHtml(bridgeScopeId: string, messageId: number, html: string, inlineKeyboard?: InlineKeyboard): Promise<void>;
|
|
18
20
|
deleteMessage(bridgeScopeId: string, messageId: number): Promise<void>;
|
|
19
21
|
sendTypingInScope(bridgeScopeId: string): Promise<void>;
|
|
20
22
|
clearInlineKeyboard(bridgeScopeId: string, messageId: number): Promise<void>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { parseTelegramTargetFromBridgeScope } from '../../core/bridge_scope.js';
|
|
2
|
+
import { telegramRichHtml } from '../../telegram/rich.js';
|
|
2
3
|
/**
|
|
3
4
|
* Telegram outbound operations addressed by bridge scope id (`telegram:…`).
|
|
4
5
|
*/
|
|
@@ -15,6 +16,10 @@ export class TelegramMessagingPort {
|
|
|
15
16
|
const target = parseTelegramTargetFromBridgeScope(bridgeScopeId);
|
|
16
17
|
return this.gateway.sendHtmlMessage(target.chatId, text, inlineKeyboard, target.topicId);
|
|
17
18
|
}
|
|
19
|
+
async sendRichHtml(bridgeScopeId, html, inlineKeyboard) {
|
|
20
|
+
const target = parseTelegramTargetFromBridgeScope(bridgeScopeId);
|
|
21
|
+
return this.gateway.sendRichMessage(target.chatId, telegramRichHtml(html, { skipEntityDetection: true }), inlineKeyboard, target.topicId);
|
|
22
|
+
}
|
|
18
23
|
async editPlain(bridgeScopeId, messageId, text, inlineKeyboard) {
|
|
19
24
|
const target = parseTelegramTargetFromBridgeScope(bridgeScopeId);
|
|
20
25
|
await this.gateway.editMessage(target.chatId, messageId, text, inlineKeyboard);
|
|
@@ -23,6 +28,10 @@ export class TelegramMessagingPort {
|
|
|
23
28
|
const target = parseTelegramTargetFromBridgeScope(bridgeScopeId);
|
|
24
29
|
await this.gateway.editHtmlMessage(target.chatId, messageId, text, inlineKeyboard);
|
|
25
30
|
}
|
|
31
|
+
async editRichHtml(bridgeScopeId, messageId, html, inlineKeyboard) {
|
|
32
|
+
const target = parseTelegramTargetFromBridgeScope(bridgeScopeId);
|
|
33
|
+
await this.gateway.editRichMessage(target.chatId, messageId, telegramRichHtml(html, { skipEntityDetection: true }), inlineKeyboard);
|
|
34
|
+
}
|
|
26
35
|
async deleteMessage(bridgeScopeId, messageId) {
|
|
27
36
|
const target = parseTelegramTargetFromBridgeScope(bridgeScopeId);
|
|
28
37
|
await this.gateway.deleteMessage(target.chatId, messageId);
|
|
@@ -227,8 +227,10 @@ export declare class BridgeSessionCore {
|
|
|
227
227
|
private updateStatus;
|
|
228
228
|
private sendMessage;
|
|
229
229
|
private sendHtmlMessage;
|
|
230
|
+
private sendRichHtmlMessage;
|
|
230
231
|
private editMessage;
|
|
231
232
|
private editHtmlMessage;
|
|
233
|
+
private editRichHtmlMessage;
|
|
232
234
|
private deleteMessage;
|
|
233
235
|
private sendTyping;
|
|
234
236
|
private sendObservedCliUserMessage;
|
|
@@ -318,6 +320,7 @@ export declare class BridgeSessionCore {
|
|
|
318
320
|
private handleArchiveCommand;
|
|
319
321
|
private handleUnarchiveCommand;
|
|
320
322
|
private handleReviewCommand;
|
|
323
|
+
private handleRichCommand;
|
|
321
324
|
private handleDiffCommand;
|
|
322
325
|
private handleLoadedCommand;
|
|
323
326
|
private handleSkillsCommand;
|
|
@@ -9,6 +9,8 @@ import { buildAccessSettingsKeyboard, buildModelSettingsKeyboard, buildSetupPane
|
|
|
9
9
|
import { clampServiceTierToModel, resolveFastTierForModel } from './service_tier.js';
|
|
10
10
|
import { TELEGRAM_BOT_API_DOWNLOAD_LIMIT_BYTES, buildAttachmentPrompt, isNativeImageAttachment, planAttachmentStoragePath, summarizeTelegramInput, } from '../telegram/media.js';
|
|
11
11
|
import { TELEGRAM_MESSAGE_LIMIT, chunkTelegramMessage, chunkTelegramStreamMessage, clipTelegramDraftMessage, } from '../telegram/text.js';
|
|
12
|
+
import { escapeTelegramHtml, telegramBold, telegramDetails, telegramExpandableBlockquote, telegramPre, telegramPreCode, } from '../telegram/html.js';
|
|
13
|
+
import { TELEGRAM_RICH_MESSAGE_TEXT_LIMIT } from '../telegram/rich.js';
|
|
12
14
|
import { isDefaultTelegramScope, resolveTelegramAddressing } from '../telegram/addressing.js';
|
|
13
15
|
import { BRIDGE_SCOPE_WEIXIN_PREFIX, parseTelegramTargetFromBridgeScope, parseWeixinBridgeScope } from '../core/bridge_scope.js';
|
|
14
16
|
import { resolveTelegramRenderRoute } from '../telegram/rendering.js';
|
|
@@ -50,6 +52,7 @@ const PINNED_HELP_COMMANDS = [
|
|
|
50
52
|
const DYNAMIC_HELP_COMMANDS = [
|
|
51
53
|
{ key: 'fast', line: '/fast <on|off|toggle>' },
|
|
52
54
|
{ key: 'active', line: '/active <steer|queue>' },
|
|
55
|
+
{ key: 'rich', line: '/rich' },
|
|
53
56
|
{ key: 'account', line: '/account' },
|
|
54
57
|
{ key: 'quota', line: '/quota' },
|
|
55
58
|
{ key: 'update', line: '/update' },
|
|
@@ -717,6 +720,10 @@ export class BridgeSessionCore {
|
|
|
717
720
|
await this.handleReviewCommand(event, locale, args);
|
|
718
721
|
return;
|
|
719
722
|
}
|
|
723
|
+
case 'rich': {
|
|
724
|
+
await this.handleRichCommand(scopeId, locale);
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
720
727
|
case 'diff': {
|
|
721
728
|
await this.handleDiffCommand(scopeId, locale);
|
|
722
729
|
return;
|
|
@@ -3356,12 +3363,30 @@ export class BridgeSessionCore {
|
|
|
3356
3363
|
async sendHtmlMessage(scopeId, text, inlineKeyboard) {
|
|
3357
3364
|
return this.messaging.sendHtml(scopeId, text, inlineKeyboard);
|
|
3358
3365
|
}
|
|
3366
|
+
async sendRichHtmlMessage(scopeId, html, fallbackHtml, inlineKeyboard) {
|
|
3367
|
+
try {
|
|
3368
|
+
return await this.messaging.sendRichHtml(scopeId, html, fallbackHtml, inlineKeyboard);
|
|
3369
|
+
}
|
|
3370
|
+
catch (error) {
|
|
3371
|
+
this.logger.warn('telegram.rich_message_send_failed', { scopeId, error: toErrorMeta(error) });
|
|
3372
|
+
return this.sendHtmlMessage(scopeId, fallbackHtml, inlineKeyboard);
|
|
3373
|
+
}
|
|
3374
|
+
}
|
|
3359
3375
|
async editMessage(scopeId, messageId, text, inlineKeyboard) {
|
|
3360
3376
|
await this.messaging.editPlain(scopeId, messageId, text, inlineKeyboard);
|
|
3361
3377
|
}
|
|
3362
3378
|
async editHtmlMessage(scopeId, messageId, text, inlineKeyboard) {
|
|
3363
3379
|
await this.messaging.editHtml(scopeId, messageId, text, inlineKeyboard);
|
|
3364
3380
|
}
|
|
3381
|
+
async editRichHtmlMessage(scopeId, messageId, html, fallbackHtml, inlineKeyboard) {
|
|
3382
|
+
try {
|
|
3383
|
+
await this.messaging.editRichHtml(scopeId, messageId, html, fallbackHtml, inlineKeyboard);
|
|
3384
|
+
}
|
|
3385
|
+
catch (error) {
|
|
3386
|
+
this.logger.warn('telegram.rich_message_edit_failed', { scopeId, messageId, error: toErrorMeta(error) });
|
|
3387
|
+
await this.editHtmlMessage(scopeId, messageId, fallbackHtml, inlineKeyboard);
|
|
3388
|
+
}
|
|
3389
|
+
}
|
|
3365
3390
|
async deleteMessage(scopeId, messageId) {
|
|
3366
3391
|
await this.messaging.deleteMessage(scopeId, messageId);
|
|
3367
3392
|
}
|
|
@@ -3373,8 +3398,8 @@ export class BridgeSessionCore {
|
|
|
3373
3398
|
for (let index = 0; index < chunks.length; index += 1) {
|
|
3374
3399
|
const chunk = chunks[index];
|
|
3375
3400
|
const body = index === 0
|
|
3376
|
-
?
|
|
3377
|
-
:
|
|
3401
|
+
? `${telegramBold(OBSERVED_CLI_USER_LABEL)}\n${telegramPre(chunk)}`
|
|
3402
|
+
: telegramPre(chunk);
|
|
3378
3403
|
await this.sendHtmlMessage(scopeId, body);
|
|
3379
3404
|
}
|
|
3380
3405
|
}
|
|
@@ -5052,13 +5077,16 @@ export class BridgeSessionCore {
|
|
|
5052
5077
|
await this.registerActiveTurn(event.scopeId, event.chatId, event.chatType, event.topicId, result.reviewThreadId, result.turnId, 0);
|
|
5053
5078
|
}
|
|
5054
5079
|
}
|
|
5080
|
+
async handleRichCommand(scopeId, locale) {
|
|
5081
|
+
await this.sendRichHtmlMessage(scopeId, formatRichDemoMessage(locale), formatRichDemoFallbackMessage(locale));
|
|
5082
|
+
}
|
|
5055
5083
|
async handleDiffCommand(scopeId, locale) {
|
|
5056
5084
|
const diff = this.latestTurnDiffs.get(scopeId);
|
|
5057
5085
|
if (!diff?.diff.trim()) {
|
|
5058
5086
|
await this.sendMessage(scopeId, t(locale, 'diff_unavailable'));
|
|
5059
5087
|
return;
|
|
5060
5088
|
}
|
|
5061
|
-
await this.
|
|
5089
|
+
await this.sendRichHtmlMessage(scopeId, formatRichDiffMessage(locale, diff.diff), formatDiffMessage(locale, diff.diff));
|
|
5062
5090
|
}
|
|
5063
5091
|
async handleLoadedCommand(scopeId, locale) {
|
|
5064
5092
|
const threadIds = await this.app.listLoadedThreads();
|
|
@@ -7990,10 +8018,9 @@ function renderArchivedToolBatchStatus(locale, counts, actionLines) {
|
|
|
7990
8018
|
return { text, html: null };
|
|
7991
8019
|
}
|
|
7992
8020
|
const heading = formatToolBatchHeading(locale, counts, false);
|
|
7993
|
-
const detailLines = actionLines.slice(0, 12).map(line => escapeTelegramHtml(line));
|
|
7994
8021
|
const html = [
|
|
7995
|
-
|
|
7996
|
-
|
|
8022
|
+
telegramBold(heading),
|
|
8023
|
+
telegramExpandableBlockquote(actionLines.slice(0, 12).join('\n')),
|
|
7997
8024
|
].join('\n');
|
|
7998
8025
|
return { text, html };
|
|
7999
8026
|
}
|
|
@@ -8142,12 +8169,6 @@ function truncateInline(value, limit) {
|
|
|
8142
8169
|
}
|
|
8143
8170
|
return `${value.slice(0, Math.max(0, limit - 1))}…`;
|
|
8144
8171
|
}
|
|
8145
|
-
function escapeTelegramHtml(value) {
|
|
8146
|
-
return value
|
|
8147
|
-
.replaceAll('&', '&')
|
|
8148
|
-
.replaceAll('<', '<')
|
|
8149
|
-
.replaceAll('>', '>');
|
|
8150
|
-
}
|
|
8151
8172
|
function parseReviewTarget(args) {
|
|
8152
8173
|
if (args.length === 0) {
|
|
8153
8174
|
return { type: 'uncommittedChanges' };
|
|
@@ -8260,7 +8281,97 @@ function formatMcpResourceMessage(locale, server, uri, contents) {
|
|
|
8260
8281
|
}
|
|
8261
8282
|
function formatDiffMessage(locale, diff) {
|
|
8262
8283
|
const clipped = diff.length > 3500 ? `${diff.slice(0, 3500)}\n...` : diff;
|
|
8263
|
-
return
|
|
8284
|
+
return [
|
|
8285
|
+
telegramBold(t(locale, 'diff_title')),
|
|
8286
|
+
telegramExpandableBlockquote(clipped),
|
|
8287
|
+
].join('\n');
|
|
8288
|
+
}
|
|
8289
|
+
function formatRichDiffMessage(locale, diff) {
|
|
8290
|
+
const clipped = clipRichMessageText(diff, Math.min(24_000, TELEGRAM_RICH_MESSAGE_TEXT_LIMIT - 1024));
|
|
8291
|
+
const summary = locale === 'zh' ? '展开 diff' : 'Expand diff';
|
|
8292
|
+
const footer = locale === 'zh'
|
|
8293
|
+
? 'FoxClaw · sendRichMessage · details/pre/code'
|
|
8294
|
+
: 'FoxClaw · sendRichMessage · details/pre/code';
|
|
8295
|
+
return [
|
|
8296
|
+
`<h3>${escapeTelegramHtml(t(locale, 'diff_title'))}</h3>`,
|
|
8297
|
+
telegramDetails(summary, telegramPreCode(clipped, 'diff')),
|
|
8298
|
+
`<footer>${escapeTelegramHtml(footer)}</footer>`,
|
|
8299
|
+
].join('\n');
|
|
8300
|
+
}
|
|
8301
|
+
function formatRichDemoMessage(locale) {
|
|
8302
|
+
const title = 'FoxClaw RichMessage';
|
|
8303
|
+
const intro = locale === 'zh'
|
|
8304
|
+
? '这条消息通过 Telegram Bot API sendRichMessage 发送,用来验证 Rich Message 在真实客户端里的渲染。'
|
|
8305
|
+
: 'This message is sent through Telegram Bot API sendRichMessage to verify Rich Message rendering in a real client.';
|
|
8306
|
+
const detailsSummary = locale === 'zh' ? '展开 details + pre 示例' : 'Open details + pre sample';
|
|
8307
|
+
const diffSample = [
|
|
8308
|
+
'diff --git a/src/telegram/rich.ts b/src/telegram/rich.ts',
|
|
8309
|
+
'+ sendRichMessage({ rich_message: { html } })',
|
|
8310
|
+
'+ <details><summary>Expandable</summary>...</details>',
|
|
8311
|
+
'+ <table bordered striped>...</table>',
|
|
8312
|
+
].join('\n');
|
|
8313
|
+
const tableCaption = locale === 'zh' ? 'FoxClaw 可用 rich 面' : 'FoxClaw rich surfaces';
|
|
8314
|
+
const surfaceHeader = locale === 'zh' ? '功能面' : 'Surface';
|
|
8315
|
+
const richHeader = locale === 'zh' ? 'Rich 用法' : 'Rich usage';
|
|
8316
|
+
const tableRows = locale === 'zh'
|
|
8317
|
+
? [
|
|
8318
|
+
['`/diff`', 'details + pre/code'],
|
|
8319
|
+
['工具状态', 'details/list'],
|
|
8320
|
+
['`/status`', 'table'],
|
|
8321
|
+
]
|
|
8322
|
+
: [
|
|
8323
|
+
['`/diff`', 'details + pre/code'],
|
|
8324
|
+
['Tool status', 'details/list'],
|
|
8325
|
+
['`/status`', 'table'],
|
|
8326
|
+
];
|
|
8327
|
+
const listItems = locale === 'zh'
|
|
8328
|
+
? ['Gateway 已接入 sendRichMessage', '/diff 已优先使用 RichMessage', '失败会回退到 Telegram HTML']
|
|
8329
|
+
: ['Gateway now supports sendRichMessage', '/diff prefers RichMessage', 'Failures fall back to Telegram HTML'];
|
|
8330
|
+
const detailBody = [
|
|
8331
|
+
`<p>${escapeTelegramHtml(locale === 'zh' ? '下面是 rich pre/code block,客户端支持时会按代码块渲染。' : 'This is a rich pre/code block rendered as code by supported clients.')}</p>`,
|
|
8332
|
+
telegramPreCode(diffSample, 'diff'),
|
|
8333
|
+
].join('\n');
|
|
8334
|
+
return [
|
|
8335
|
+
`<h2>${escapeTelegramHtml(title)}</h2>`,
|
|
8336
|
+
`<p><b>Bot API 10.1</b> ${escapeTelegramHtml(intro)}</p>`,
|
|
8337
|
+
'<hr/>',
|
|
8338
|
+
'<table bordered striped>',
|
|
8339
|
+
`<caption>${escapeTelegramHtml(tableCaption)}</caption>`,
|
|
8340
|
+
`<tr><th>${escapeTelegramHtml(surfaceHeader)}</th><th>${escapeTelegramHtml(richHeader)}</th></tr>`,
|
|
8341
|
+
...tableRows.map(([surface, usage]) => `<tr><td>${escapeTelegramHtml(surface)}</td><td>${escapeTelegramHtml(usage)}</td></tr>`),
|
|
8342
|
+
'</table>',
|
|
8343
|
+
telegramDetails(detailsSummary, detailBody, true),
|
|
8344
|
+
'<ul>',
|
|
8345
|
+
...listItems.map(item => `<li>${escapeTelegramHtml(item)}</li>`),
|
|
8346
|
+
'</ul>',
|
|
8347
|
+
'<footer>FoxClaw · sendRichMessage</footer>',
|
|
8348
|
+
].join('\n');
|
|
8349
|
+
}
|
|
8350
|
+
function formatRichDemoFallbackMessage(locale) {
|
|
8351
|
+
const lines = locale === 'zh'
|
|
8352
|
+
? [
|
|
8353
|
+
'RichMessage fallback 预览',
|
|
8354
|
+
'Gateway 已接入 sendRichMessage',
|
|
8355
|
+
'/diff 已优先使用 RichMessage',
|
|
8356
|
+
'如果你看到这条 HTML fallback,说明 Telegram rich API 返回了失败,正常功能仍可用。',
|
|
8357
|
+
]
|
|
8358
|
+
: [
|
|
8359
|
+
'RichMessage fallback preview',
|
|
8360
|
+
'Gateway now supports sendRichMessage',
|
|
8361
|
+
'/diff prefers RichMessage',
|
|
8362
|
+
'If you see this HTML fallback, Telegram rich API failed but normal messaging still works.',
|
|
8363
|
+
];
|
|
8364
|
+
return [
|
|
8365
|
+
telegramBold('FoxClaw RichMessage'),
|
|
8366
|
+
telegramExpandableBlockquote(lines.join('\n')),
|
|
8367
|
+
telegramPreCode('sendRichMessage({ rich_message: { html } })', 'typescript'),
|
|
8368
|
+
].join('\n');
|
|
8369
|
+
}
|
|
8370
|
+
function clipRichMessageText(value, limit) {
|
|
8371
|
+
if (value.length <= limit) {
|
|
8372
|
+
return value;
|
|
8373
|
+
}
|
|
8374
|
+
return `${value.slice(0, Math.max(0, limit - 4))}\n...`;
|
|
8264
8375
|
}
|
|
8265
8376
|
function formatLoadedThreadsMessage(locale, threadIds) {
|
|
8266
8377
|
const lines = [t(locale, 'loaded_title')];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { t } from '../i18n.js';
|
|
3
|
+
import { escapeTelegramHtml } from '../telegram/html.js';
|
|
3
4
|
import { resolveFastTierForModel } from './service_tier.js';
|
|
4
5
|
export function formatThreadsMessage(locale, threads, currentThreadId, searchTerm, listState) {
|
|
5
6
|
if (threads.length === 0) {
|
|
@@ -642,12 +643,6 @@ function formatIsoTime(locale, unixSeconds) {
|
|
|
642
643
|
return t(locale, 'unknown');
|
|
643
644
|
return new Date(unixSeconds * 1000).toISOString();
|
|
644
645
|
}
|
|
645
|
-
function escapeTelegramHtml(value) {
|
|
646
|
-
return value
|
|
647
|
-
.replaceAll('&', '&')
|
|
648
|
-
.replaceAll('<', '<')
|
|
649
|
-
.replaceAll('>', '>');
|
|
650
|
-
}
|
|
651
646
|
function chunkButtons(buttons, width) {
|
|
652
647
|
const rows = [];
|
|
653
648
|
for (let index = 0; index < buttons.length; index += width) {
|
package/dist/i18n.d.ts
CHANGED
|
@@ -45,6 +45,7 @@ declare const MESSAGES: {
|
|
|
45
45
|
readonly cmd_desc_setup: "Unified preference panel";
|
|
46
46
|
readonly cmd_desc_fast: "Toggle Fast mode";
|
|
47
47
|
readonly cmd_desc_active: "Active-turn message behavior";
|
|
48
|
+
readonly cmd_desc_rich: "Telegram RichMessage demo";
|
|
48
49
|
readonly cmd_desc_status: "Bridge status";
|
|
49
50
|
readonly cmd_desc_update: "Update and restart FoxClaw";
|
|
50
51
|
readonly cmd_desc_account: "Codex account";
|
|
@@ -742,6 +743,7 @@ declare const MESSAGES: {
|
|
|
742
743
|
readonly cmd_desc_setup: "统一偏好面板";
|
|
743
744
|
readonly cmd_desc_fast: "切换 Fast 模式";
|
|
744
745
|
readonly cmd_desc_active: "运行中新消息处理方式";
|
|
746
|
+
readonly cmd_desc_rich: "Telegram RichMessage 演示";
|
|
745
747
|
readonly cmd_desc_status: "查看桥接状态";
|
|
746
748
|
readonly cmd_desc_update: "升级并重启 FoxClaw";
|
|
747
749
|
readonly cmd_desc_account: "Codex 账号";
|
package/dist/i18n.js
CHANGED
|
@@ -43,6 +43,7 @@ const MESSAGES = {
|
|
|
43
43
|
cmd_desc_setup: 'Unified preference panel',
|
|
44
44
|
cmd_desc_fast: 'Toggle Fast mode',
|
|
45
45
|
cmd_desc_active: 'Active-turn message behavior',
|
|
46
|
+
cmd_desc_rich: 'Telegram RichMessage demo',
|
|
46
47
|
cmd_desc_status: 'Bridge status',
|
|
47
48
|
cmd_desc_update: 'Update and restart FoxClaw',
|
|
48
49
|
cmd_desc_account: 'Codex account',
|
|
@@ -740,6 +741,7 @@ const MESSAGES = {
|
|
|
740
741
|
cmd_desc_setup: '统一偏好面板',
|
|
741
742
|
cmd_desc_fast: '切换 Fast 模式',
|
|
742
743
|
cmd_desc_active: '运行中新消息处理方式',
|
|
744
|
+
cmd_desc_rich: 'Telegram RichMessage 演示',
|
|
743
745
|
cmd_desc_status: '查看桥接状态',
|
|
744
746
|
cmd_desc_update: '升级并重启 FoxClaw',
|
|
745
747
|
cmd_desc_account: 'Codex 账号',
|
|
@@ -1412,6 +1414,7 @@ export function getTelegramCommands(locale) {
|
|
|
1412
1414
|
{ command: 'auth', description: t(locale, 'cmd_desc_auth') },
|
|
1413
1415
|
{ command: 'fast', description: t(locale, 'cmd_desc_fast') },
|
|
1414
1416
|
{ command: 'active', description: t(locale, 'cmd_desc_active') },
|
|
1417
|
+
{ command: 'rich', description: t(locale, 'cmd_desc_rich') },
|
|
1415
1418
|
{ command: 'account', description: t(locale, 'cmd_desc_account') },
|
|
1416
1419
|
{ command: 'quota', description: t(locale, 'cmd_desc_quota') },
|
|
1417
1420
|
{ command: 'login_device', description: t(locale, 'cmd_desc_login_device') },
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export interface FoxclawLaunchdPlistOptions {
|
|
2
|
+
label: string;
|
|
3
|
+
nodePath: string;
|
|
4
|
+
nodeArgs: string[];
|
|
5
|
+
entryPoint: string;
|
|
6
|
+
workingDirectory: string;
|
|
7
|
+
pathValue: string;
|
|
8
|
+
home: string;
|
|
9
|
+
user: string;
|
|
10
|
+
logname: string;
|
|
11
|
+
envPath: string;
|
|
12
|
+
proxyEnv: Record<string, string>;
|
|
13
|
+
stdoutPath: string;
|
|
14
|
+
stderrPath: string;
|
|
15
|
+
}
|
|
16
|
+
export declare function buildFoxclawLaunchdPlistText(options: FoxclawLaunchdPlistOptions): string;
|
|
17
|
+
export declare function extractNodePathFromLaunchdPlist(plistText: string): string;
|
|
18
|
+
export declare function xmlEscape(value: string): string;
|
package/dist/launchd.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export function buildFoxclawLaunchdPlistText(options) {
|
|
2
|
+
const nodeArgXml = options.nodeArgs.map((arg) => ` <string>${xmlEscape(arg)}</string>`).join('\n');
|
|
3
|
+
const proxyEnvXml = buildEnvironmentVariablesXml(options.proxyEnv, 4);
|
|
4
|
+
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
5
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
6
|
+
<plist version="1.0">
|
|
7
|
+
<dict>
|
|
8
|
+
<key>Label</key>
|
|
9
|
+
<string>${xmlEscape(options.label)}</string>
|
|
10
|
+
<key>ProgramArguments</key>
|
|
11
|
+
<array>
|
|
12
|
+
<string>${xmlEscape(options.nodePath)}</string>
|
|
13
|
+
${nodeArgXml ? `${nodeArgXml}\n` : ''} <string>${xmlEscape(options.entryPoint)}</string>
|
|
14
|
+
<string>serve</string>
|
|
15
|
+
</array>
|
|
16
|
+
<key>WorkingDirectory</key>
|
|
17
|
+
<string>${xmlEscape(options.workingDirectory)}</string>
|
|
18
|
+
<key>EnvironmentVariables</key>
|
|
19
|
+
<dict>
|
|
20
|
+
<key>PATH</key>
|
|
21
|
+
<string>${xmlEscape(options.pathValue)}</string>
|
|
22
|
+
<key>HOME</key>
|
|
23
|
+
<string>${xmlEscape(options.home)}</string>
|
|
24
|
+
<key>USER</key>
|
|
25
|
+
<string>${xmlEscape(options.user)}</string>
|
|
26
|
+
<key>LOGNAME</key>
|
|
27
|
+
<string>${xmlEscape(options.logname)}</string>
|
|
28
|
+
<key>FOXCLAW_ENV</key>
|
|
29
|
+
<string>${xmlEscape(options.envPath)}</string>
|
|
30
|
+
${proxyEnvXml}
|
|
31
|
+
</dict>
|
|
32
|
+
<key>RunAtLoad</key>
|
|
33
|
+
<true/>
|
|
34
|
+
<key>KeepAlive</key>
|
|
35
|
+
<true/>
|
|
36
|
+
<key>StandardOutPath</key>
|
|
37
|
+
<string>${xmlEscape(options.stdoutPath)}</string>
|
|
38
|
+
<key>StandardErrorPath</key>
|
|
39
|
+
<string>${xmlEscape(options.stderrPath)}</string>
|
|
40
|
+
</dict>
|
|
41
|
+
</plist>
|
|
42
|
+
`;
|
|
43
|
+
}
|
|
44
|
+
export function extractNodePathFromLaunchdPlist(plistText) {
|
|
45
|
+
const programArguments = plistText.match(/<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/);
|
|
46
|
+
const firstArgument = programArguments?.[1]?.match(/<string>([\s\S]*?)<\/string>/)?.[1] ?? '';
|
|
47
|
+
return firstArgument ? xmlUnescape(firstArgument.trim()) : '';
|
|
48
|
+
}
|
|
49
|
+
export function xmlEscape(value) {
|
|
50
|
+
return value
|
|
51
|
+
.replace(/&/g, '&')
|
|
52
|
+
.replace(/</g, '<')
|
|
53
|
+
.replace(/>/g, '>')
|
|
54
|
+
.replace(/"/g, '"')
|
|
55
|
+
.replace(/'/g, ''');
|
|
56
|
+
}
|
|
57
|
+
function xmlUnescape(value) {
|
|
58
|
+
return value
|
|
59
|
+
.replace(/'/g, "'")
|
|
60
|
+
.replace(/"/g, '"')
|
|
61
|
+
.replace(/>/g, '>')
|
|
62
|
+
.replace(/</g, '<')
|
|
63
|
+
.replace(/&/g, '&');
|
|
64
|
+
}
|
|
65
|
+
function buildEnvironmentVariablesXml(values, indent) {
|
|
66
|
+
const prefix = ' '.repeat(indent);
|
|
67
|
+
const entries = [];
|
|
68
|
+
for (const [key, value] of Object.entries(values)) {
|
|
69
|
+
if (!value)
|
|
70
|
+
continue;
|
|
71
|
+
entries.push(`${prefix}<key>${xmlEscape(key)}</key>`);
|
|
72
|
+
entries.push(`${prefix}<string>${xmlEscape(value)}</string>`);
|
|
73
|
+
}
|
|
74
|
+
return entries.length > 0 ? `${entries.join('\n')}\n` : '';
|
|
75
|
+
}
|