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)) }