From 6d00225cc46d79c6a15b209b3accf4555062b786 Mon Sep 17 00:00:00 2001 From: root Date: Thu, 17 Sep 2026 11:36:15 +0800 Subject: [PATCH] =?UTF-8?q?feat(tg):=20=E6=80=9D=E8=80=83=E5=9D=97?= =?UTF-8?q?=E6=94=B9=E4=B8=BA=E5=8F=AF=E6=8A=98=E5=8F=A0=E5=BC=95=E7=94=A8?= =?UTF-8?q?=E5=9D=97=EF=BC=88=E4=B8=8E=E5=B7=A5=E5=85=B7=E8=B0=83=E7=94=A8?= =?UTF-8?q?=E5=8C=BA=E5=88=86=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TG 不支持按消息自定义引用块颜色(竖线颜色跟随客户端主题),按用户 选择采用官方可折叠引用块:默认收起为「💭 思考过程 ▾」,点击展开, 与工具调用的普通蓝色竖线引用块在交互上明确区分。 - rich 模式:Telegram 原生 markdown 可折叠引用语法(**> 开头 + 行尾 ||) - HTML 模式:markdownToHtml 转换为
- 思考文本内的 || 全角化,避免被解析成 spoiler 实体 --- lib/bridge.js | 14 +++++++++++--- lib/format.js | 42 +++++++++++++++++++++++++++++++++++++----- src/bridge.ts | 14 +++++++++++--- src/format.ts | 40 +++++++++++++++++++++++++++++++++++----- 4 files changed, 94 insertions(+), 16 deletions(-) diff --git a/lib/bridge.js b/lib/bridge.js index 01c2e17..d976c1d 100644 --- a/lib/bridge.js +++ b/lib/bridge.js @@ -75,8 +75,10 @@ const defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); const REASONING_QUOTE_MAX_CHARS = 800; /** * Assistant message → markdown for delivery. Reasoning (thinking) blocks are - * kept, one `> ` quote line each — Telegram renders them as the accent-bar - * quote style the phone user expects for bash/thinking-style chatter. + * kept as EXPANDABLE quotes (`**>`…`||` per Telegram's native markdown spec, + * `
` in HTML mode) so they stay collapsed on the phone + * and visually distinct from tool-call quotes; ordinary quotes in assistant + * text remain plain blockquotes. */ function contentToText(content) { const parts = []; @@ -90,7 +92,13 @@ function contentToText(content) { continue; if (thinking.length > REASONING_QUOTE_MAX_CHARS) thinking = `${thinking.slice(0, REASONING_QUOTE_MAX_CHARS)}…`; - parts.push(thinking.split('\n').map((line) => `> ${line}`).join('\n')); + // `||` would open/close spoiler entities in native markdown — neutralize. + const lines = thinking.replace(/\|\|/g, '||').split('\n'); + const last = lines.length - 1; + parts.push([ + '**>💭 思考过程', + ...lines.map((line, i) => `> ${line}${i === last ? '||' : ''}`), + ].join('\n')); } } return parts.join('\n\n'); diff --git a/lib/format.js b/lib/format.js index 4ac6ba8..729bbcb 100644 --- a/lib/format.js +++ b/lib/format.js @@ -12,22 +12,54 @@ function inlineToHtml(text) { html = html.replace(/\*\*([^*\n]+)\*\*/g, '$1'); return html; } -/** Convert `> …` line groups into `
` (Telegram renders it with the accent-colored left bar). */ +/** + * Convert `> …` line groups into `
` (Telegram renders it with the + * accent-colored left bar). The native-markdown expandable-quote form + * (`**>first line` … `last line||`) becomes `
` — used + * for thinking blocks so they stay collapsed and distinct from tool-call + * quotes (Telegram has no per-message quote color). + */ +const EXPANDABLE_OPEN = '**>'; function blocksToHtml(text) { const lines = text.split('\n'); const out = []; let quote = []; + let expandable = false; const flush = () => { if (quote.length === 0) return; - out.push(`
${inlineToHtml(quote.join('\n'))}
`); + const open = expandable ? '
' : '
'; + out.push(`${open}${inlineToHtml(quote.join('\n'))}
`); quote = []; + expandable = false; }; for (const line of lines) { - if (line.startsWith('> ')) - quote.push(line.slice(2)); - else if (line === '>') + if (line.startsWith(EXPANDABLE_OPEN)) { + flush(); + expandable = true; + let content = line.slice(EXPANDABLE_OPEN.length); + if (content.endsWith('||')) { + quote.push(content.slice(0, -2)); + flush(); + } + else { + quote.push(content); + } + } + else if (line.startsWith('> ')) { + let content = line.slice(2); + let closed = false; + if (expandable && content.endsWith('||')) { + content = content.slice(0, -2); + closed = true; + } + quote.push(content); + if (closed) + flush(); + } + else if (line === '>') { quote.push(''); + } else { flush(); out.push(inlineToHtml(line)); diff --git a/src/bridge.ts b/src/bridge.ts index 1427c5d..4a927e4 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -210,8 +210,10 @@ const REASONING_QUOTE_MAX_CHARS = 800 /** * Assistant message → markdown for delivery. Reasoning (thinking) blocks are - * kept, one `> ` quote line each — Telegram renders them as the accent-bar - * quote style the phone user expects for bash/thinking-style chatter. + * kept as EXPANDABLE quotes (`**>`…`||` per Telegram's native markdown spec, + * `
` in HTML mode) so they stay collapsed on the phone + * and visually distinct from tool-call quotes; ordinary quotes in assistant + * text remain plain blockquotes. */ function contentToText(content: readonly ContentBlock[]): string { const parts: string[] = [] @@ -223,7 +225,13 @@ function contentToText(content: readonly ContentBlock[]): string { if (!thinking) continue if (thinking.length > REASONING_QUOTE_MAX_CHARS) thinking = `${thinking.slice(0, REASONING_QUOTE_MAX_CHARS)}…` - parts.push(thinking.split('\n').map((line) => `> ${line}`).join('\n')) + // `||` would open/close spoiler entities in native markdown — neutralize. + const lines = thinking.replace(/\|\|/g, '||').split('\n') + const last = lines.length - 1 + parts.push([ + '**>💭 思考过程', + ...lines.map((line, i) => `> ${line}${i === last ? '||' : ''}`), + ].join('\n')) } } return parts.join('\n\n') diff --git a/src/format.ts b/src/format.ts index 0b0fa22..b2b160d 100644 --- a/src/format.ts +++ b/src/format.ts @@ -14,20 +14,50 @@ function inlineToHtml(text: string): string { return html } -/** Convert `> …` line groups into `
` (Telegram renders it with the accent-colored left bar). */ +/** + * Convert `> …` line groups into `
` (Telegram renders it with the + * accent-colored left bar). The native-markdown expandable-quote form + * (`**>first line` … `last line||`) becomes `
` — used + * for thinking blocks so they stay collapsed and distinct from tool-call + * quotes (Telegram has no per-message quote color). + */ +const EXPANDABLE_OPEN = '**>' + function blocksToHtml(text: string): string { const lines = text.split('\n') const out: string[] = [] let quote: string[] = [] + let expandable = false const flush = (): void => { if (quote.length === 0) return - out.push(`
${inlineToHtml(quote.join('\n'))}
`) + const open = expandable ? '
' : '
' + out.push(`${open}${inlineToHtml(quote.join('\n'))}
`) quote = [] + expandable = false } for (const line of lines) { - if (line.startsWith('> ')) quote.push(line.slice(2)) - else if (line === '>') quote.push('') - else { + if (line.startsWith(EXPANDABLE_OPEN)) { + flush() + expandable = true + let content = line.slice(EXPANDABLE_OPEN.length) + if (content.endsWith('||')) { + quote.push(content.slice(0, -2)) + flush() + } else { + quote.push(content) + } + } else if (line.startsWith('> ')) { + let content = line.slice(2) + let closed = false + if (expandable && content.endsWith('||')) { + content = content.slice(0, -2) + closed = true + } + quote.push(content) + if (closed) flush() + } else if (line === '>') { + quote.push('') + } else { flush() out.push(inlineToHtml(line)) }