@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 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
- ? `<b>${escapeTelegramHtml(OBSERVED_CLI_USER_LABEL)}</b>\n<pre>${escapeTelegramHtml(chunk)}</pre>`
3377
- : `<pre>${escapeTelegramHtml(chunk)}</pre>`;
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.sendMessage(scopeId, formatDiffMessage(locale, diff.diff));
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
- `<b>${escapeTelegramHtml(heading)}</b>`,
7996
- `<blockquote expandable>${detailLines.join('\n')}</blockquote>`,
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('&', '&amp;')
8148
- .replaceAll('<', '&lt;')
8149
- .replaceAll('>', '&gt;');
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 `${t(locale, 'diff_title')}\n${clipped}`;
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('&', '&amp;')
648
- .replaceAll('<', '&lt;')
649
- .replaceAll('>', '&gt;');
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;
@@ -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, '&amp;')
52
+ .replace(/</g, '&lt;')
53
+ .replace(/>/g, '&gt;')
54
+ .replace(/"/g, '&quot;')
55
+ .replace(/'/g, '&apos;');
56
+ }
57
+ function xmlUnescape(value) {
58
+ return value
59
+ .replace(/&apos;/g, "'")
60
+ .replace(/&quot;/g, '"')
61
+ .replace(/&gt;/g, '>')
62
+ .replace(/&lt;/g, '<')
63
+ .replace(/&amp;/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
+ }