-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathstreaming-preview.js
More file actions
148 lines (132 loc) · 4.24 KB
/
streaming-preview.js
File metadata and controls
148 lines (132 loc) · 4.24 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
// Streaming Preview — 实时消息编辑显示 AI 流式输出
// 参考: Claude-to-IM bridge-manager.ts:600-659
//
// 工作原理:
// 1. 接管 progress tracker 的消息(或新建一条)
// 2. AI 流式输出时不断 editMessageText 原地更新
// 3. 双重节流:间隔节流 + 增量节流
// 4. 降级:连续编辑失败后停止预览
const DEFAULTS = {
intervalMs: 700, // 最小编辑间隔
minDeltaChars: 20, // 最少新增字符数才触发
maxChars: 3900, // 预览截断长度(TG 限制 4096)
activationChars: 50, // 积累多少字符后才激活预览
maxEditFailures: 3, // 连续失败次数后降级
};
/**
* @param {object} ctx - grammy context(需要 ctx.api)
* @param {number} chatId
* @param {object} config - 覆盖默认配置
* @param {object} config.replyMarkup - 可选的 InlineKeyboard(如 Stop 按钮)
*/
export function createStreamingPreview(ctx, chatId, config = {}) {
const cfg = { ...DEFAULTS, ...config };
const replyMarkup = config.replyMarkup || null;
let previewMsgId = null;
let lastSentText = "";
let lastSentAt = 0;
let degraded = false;
let consecutiveFailures = 0;
let throttleTimer = null;
let pendingText = "";
/**
* 启动预览
* @param {number} [existingMsgId] - 接管已有消息(如 progress tracker 的消息)
*/
async function start(existingMsgId) {
if (existingMsgId) {
previewMsgId = existingMsgId;
} else {
try {
const opts = replyMarkup ? { reply_markup: replyMarkup } : {};
const msg = await ctx.api.sendMessage(chatId, "⏳ 正在生成...", opts);
previewMsgId = msg.message_id;
} catch {
degraded = true;
}
}
}
/**
* 接收完整文本的增量更新
* @param {string} fullText - 到目前为止的全部文本
*/
function onText(fullText) {
if (degraded || !previewMsgId) return;
// 截断到 maxChars
pendingText = fullText.length > cfg.maxChars
? fullText.slice(0, cfg.maxChars) + "\n\n⏳ 正在生成..."
: fullText + "\n\n⏳ 正在生成...";
const delta = pendingText.length - lastSentText.length;
const elapsed = Date.now() - lastSentAt;
// 增量不够 → 调度尾部定时器
if (delta < cfg.minDeltaChars && lastSentAt > 0) {
if (!throttleTimer) {
throttleTimer = setTimeout(() => {
throttleTimer = null;
if (!degraded) doFlush();
}, cfg.intervalMs);
}
return;
}
// 间隔不够 → 调度尾部定时器
if (elapsed < cfg.intervalMs && lastSentAt > 0) {
if (!throttleTimer) {
throttleTimer = setTimeout(() => {
throttleTimer = null;
if (!degraded) doFlush();
}, cfg.intervalMs - elapsed);
}
return;
}
// 立即刷新
if (throttleTimer) {
clearTimeout(throttleTimer);
throttleTimer = null;
}
doFlush();
}
function doFlush() {
if (degraded || !previewMsgId || !pendingText) return;
if (pendingText === lastSentText) return;
const textToSend = pendingText;
const editOpts = replyMarkup ? { reply_markup: replyMarkup } : {};
ctx.api.editMessageText(chatId, previewMsgId, textToSend, editOpts)
.then(() => {
lastSentText = textToSend;
lastSentAt = Date.now();
consecutiveFailures = 0;
})
.catch((err) => {
consecutiveFailures++;
// "message is not modified" 不算真正失败
if (/not modified/i.test(err?.description || err?.message || "")) {
consecutiveFailures = 0;
return;
}
if (consecutiveFailures >= cfg.maxEditFailures) {
console.warn(`[streaming-preview] degraded after ${consecutiveFailures} consecutive failures`);
degraded = true;
}
});
}
/**
* 结束预览,返回消息 ID
* @returns {number|null}
*/
function finish() {
if (throttleTimer) {
clearTimeout(throttleTimer);
throttleTimer = null;
}
const msgId = previewMsgId;
previewMsgId = null;
return msgId;
}
function getMessageId() {
return previewMsgId;
}
function isDegraded() {
return degraded;
}
return { start, onText, finish, getMessageId, isDegraded };
}