mirror of
https://github.com/NanmiCoder/cc-haha
synced 2026-07-16 13:03:31 +08:00
Telegram and desktop can attach to the same session while a turn is streaming. The WebSocket handler now tracks output callbacks per client and broadcasts session messages instead of replacing the previous subscriber. Telegram thinking deltas are accumulated before editing the placeholder so the preview does not collapse to the latest tiny chunk. Constraint: Desktop and IM adapters may observe the same active session concurrently Rejected: Keep a single session output callback | a later desktop view can steal Telegram's live content and completion events Confidence: high Scope-risk: moderate Directive: Do not collapse session output callbacks back to one callback without a multi-client streaming regression Tested: bun run check:server (828 pass); bun run check:adapters (358 pass); git diff --check Not-tested: Live Telegram Bot API smoke against the packaged app
93 lines
2.7 KiB
TypeScript
93 lines
2.7 KiB
TypeScript
import {
|
|
convertMarkdownTablesToBullets,
|
|
splitMessage,
|
|
} from '../common/format.js'
|
|
|
|
export function formatTelegramOutboundText(text: string): string {
|
|
return convertMarkdownTablesToBullets(text)
|
|
}
|
|
|
|
export function formatTelegramStreamingText(text: string): string {
|
|
return `${formatTelegramOutboundText(text)} ▍`
|
|
}
|
|
|
|
const DEFAULT_THINKING_PREVIEW_LIMIT = 1000
|
|
|
|
export type TelegramThinkingUpdate = {
|
|
fullText: string
|
|
messageText: string
|
|
}
|
|
|
|
export function buildTelegramThinkingUpdate(
|
|
currentText: string,
|
|
deltaText: string,
|
|
previewLimit = DEFAULT_THINKING_PREVIEW_LIMIT,
|
|
): TelegramThinkingUpdate {
|
|
const fullText = currentText + deltaText
|
|
const preview = fullText.slice(0, Math.max(0, previewLimit)).trimStart()
|
|
return {
|
|
fullText,
|
|
messageText: preview ? `💭 ${preview}...` : '💭 思考中...',
|
|
}
|
|
}
|
|
|
|
export type TelegramStreamingUpdate = {
|
|
sealedChunks: string[]
|
|
activeChunk: string
|
|
}
|
|
|
|
export function planTelegramStreamingUpdate(
|
|
currentText: string,
|
|
deltaText: string,
|
|
limit: number,
|
|
): TelegramStreamingUpdate {
|
|
const fullText = currentText + deltaText
|
|
if (formatTelegramOutboundText(fullText).length <= limit) {
|
|
return { sealedChunks: [], activeChunk: fullText }
|
|
}
|
|
|
|
const sealedChunks: string[] = []
|
|
let remaining = fullText
|
|
|
|
while (formatTelegramOutboundText(remaining).length > limit) {
|
|
const [sealed, rest] = splitOneStreamingChunk(remaining, limit)
|
|
sealedChunks.push(sealed)
|
|
remaining = rest
|
|
|
|
if (!remaining) break
|
|
}
|
|
|
|
return { sealedChunks, activeChunk: remaining }
|
|
}
|
|
|
|
function splitOneStreamingChunk(text: string, limit: number): [string, string] {
|
|
const roughLimit = Math.min(limit, text.length)
|
|
const candidates = [
|
|
text.lastIndexOf('\n\n', roughLimit),
|
|
text.lastIndexOf('\n', roughLimit),
|
|
text.lastIndexOf('. ', roughLimit),
|
|
text.lastIndexOf(' ', roughLimit),
|
|
].filter((index) => index > 0)
|
|
|
|
for (const candidate of candidates) {
|
|
const splitAt = includeDelimiter(text, candidate)
|
|
const sealed = text.slice(0, splitAt).trimEnd()
|
|
if (sealed && formatTelegramOutboundText(sealed).length <= limit) {
|
|
return [sealed, text.slice(splitAt).trimStart()]
|
|
}
|
|
}
|
|
|
|
const chunks = splitMessage(formatTelegramOutboundText(text), limit)
|
|
const firstFormattedChunk = chunks[0] ?? text.slice(0, limit)
|
|
if (firstFormattedChunk.length < text.length && text.startsWith(firstFormattedChunk)) {
|
|
return [firstFormattedChunk.trimEnd(), text.slice(firstFormattedChunk.length).trimStart()]
|
|
}
|
|
|
|
const splitAt = Math.max(1, Math.min(limit, text.length))
|
|
return [text.slice(0, splitAt).trimEnd(), text.slice(splitAt).trimStart()]
|
|
}
|
|
|
|
function includeDelimiter(text: string, splitAt: number): number {
|
|
return text[splitAt] === '\n' || text[splitAt] === '.' ? splitAt + 1 : splitAt
|
|
}
|