bingocode 1.0.40 → 1.1.42

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.
Files changed (63) hide show
  1. package/bin/bingo-win.cjs +2 -1
  2. package/bin/bingocode-win.cjs +2 -1
  3. package/bin/claude-win.cjs +2 -1
  4. package/bun.lock +1716 -0
  5. package/package.json +14 -2
  6. package/src/server/config/providers.yaml +1 -1
  7. package/src/server/proxy/transform/anthropicToOpenaiChat.ts +23 -9
  8. package/adapters/README.md +0 -87
  9. package/adapters/common/__tests__/chat-queue.test.ts +0 -61
  10. package/adapters/common/__tests__/format.test.ts +0 -148
  11. package/adapters/common/__tests__/http-client.test.ts +0 -105
  12. package/adapters/common/__tests__/message-buffer.test.ts +0 -84
  13. package/adapters/common/__tests__/message-dedup.test.ts +0 -57
  14. package/adapters/common/__tests__/session-store.test.ts +0 -62
  15. package/adapters/common/__tests__/ws-bridge.test.ts +0 -177
  16. package/adapters/common/attachment/__tests__/attachment-limits.test.ts +0 -52
  17. package/adapters/common/attachment/__tests__/attachment-store.test.ts +0 -108
  18. package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +0 -115
  19. package/adapters/common/attachment/attachment-limits.ts +0 -58
  20. package/adapters/common/attachment/attachment-store.ts +0 -121
  21. package/adapters/common/attachment/attachment-types.ts +0 -29
  22. package/adapters/common/attachment/image-block-watcher.ts +0 -94
  23. package/adapters/common/chat-queue.ts +0 -24
  24. package/adapters/common/config.ts +0 -96
  25. package/adapters/common/format.ts +0 -229
  26. package/adapters/common/http-client.ts +0 -107
  27. package/adapters/common/message-buffer.ts +0 -91
  28. package/adapters/common/message-dedup.ts +0 -57
  29. package/adapters/common/pairing.ts +0 -149
  30. package/adapters/common/session-store.ts +0 -60
  31. package/adapters/common/ws-bridge.ts +0 -282
  32. package/adapters/feishu/__tests__/card-errors.test.ts +0 -194
  33. package/adapters/feishu/__tests__/cardkit.test.ts +0 -295
  34. package/adapters/feishu/__tests__/extract-payload.test.ts +0 -77
  35. package/adapters/feishu/__tests__/feishu.test.ts +0 -907
  36. package/adapters/feishu/__tests__/flush-controller.test.ts +0 -290
  37. package/adapters/feishu/__tests__/markdown-style.test.ts +0 -353
  38. package/adapters/feishu/__tests__/media.test.ts +0 -120
  39. package/adapters/feishu/__tests__/streaming-card.test.ts +0 -914
  40. package/adapters/feishu/card-errors.ts +0 -151
  41. package/adapters/feishu/cardkit.ts +0 -294
  42. package/adapters/feishu/extract-payload.ts +0 -95
  43. package/adapters/feishu/flush-controller.ts +0 -149
  44. package/adapters/feishu/index.ts +0 -1275
  45. package/adapters/feishu/markdown-style.ts +0 -212
  46. package/adapters/feishu/media.ts +0 -176
  47. package/adapters/feishu/streaming-card.ts +0 -612
  48. package/adapters/package.json +0 -23
  49. package/adapters/telegram/__tests__/media.test.ts +0 -86
  50. package/adapters/telegram/__tests__/telegram.test.ts +0 -115
  51. package/adapters/telegram/index.ts +0 -754
  52. package/adapters/telegram/media.ts +0 -89
  53. package/adapters/tsconfig.json +0 -18
  54. package/runtime/mac_helper.py +0 -775
  55. package/runtime/requirements-win.txt +0 -7
  56. package/runtime/requirements.txt +0 -6
  57. package/runtime/test_helpers.py +0 -322
  58. package/runtime/win_helper.py +0 -723
  59. package/scripts/count-app-loc.ts +0 -256
  60. package/scripts/release.ts +0 -130
  61. package/start-cli.bat +0 -7
  62. package/stubs/ant-claude-for-chrome-mcp.ts +0 -24
  63. package/stubs/color-diff-napi.ts +0 -45
@@ -1,612 +0,0 @@
1
- /**
2
- * 单个 chat 的流式卡片生命周期状态机
3
- *
4
- * 负责把 LLM 的流式文本增量渲染成一张随着内容生长的飞书 CardKit 卡片。
5
- * 封装了:
6
- * - CardKit API 的 5 步调用(create → send → stream × N → settings → update)
7
- * - 节流 + 并发保护(FlushController)
8
- * - Markdown 预处理(optimizeMarkdownForFeishu + sanitizeTextForCard)
9
- * - 错误降级:CardKit 挂了自动切到 im.message.patch + Schema 2.0 卡
10
- * - 速率限制:230020 跳帧,下次重试;230099 表格超限禁用 CardKit 流式
11
- *
12
- * 每个 chatId 一个实例,由 index.ts 的 handleServerMessage 协调 lifecycle。
13
- */
14
-
15
- import type * as Lark from '@larksuiteoapi/node-sdk'
16
- import { FlushController, THROTTLE } from './flush-controller.js'
17
- import {
18
- createCardEntity,
19
- sendCardAsMessage,
20
- streamCardContent,
21
- setCardStreamingMode,
22
- updateCardKitCard,
23
- STREAMING_ELEMENT_ID,
24
- } from './cardkit.js'
25
- import { isCardRateLimitError, isCardTableLimitError } from './card-errors.js'
26
- import { optimizeMarkdownForFeishu, sanitizeTextForCard } from './markdown-style.js'
27
-
28
- // ---------------------------------------------------------------------------
29
- // Card JSON builders
30
- // ---------------------------------------------------------------------------
31
-
32
- /** 初始流式卡片:Schema 2.0 + streaming_mode + element_id。
33
- *
34
- * 只包含一个 markdown 元素 `streaming_content`,初始内容为 loading 提示。
35
- * 由 renderedText() 统一控制显示状态(思考中 / reasoning / 正文),
36
- * 避免静态 loading 元素和 streaming 内容同时显示造成"两个思考中"。
37
- *
38
- * finalize 时整卡 update 替换为纯答复正文。 */
39
- export function buildInitialStreamingCard(): Record<string, unknown> {
40
- return {
41
- schema: '2.0',
42
- config: {
43
- streaming_mode: true,
44
- update_multi: true,
45
- },
46
- body: {
47
- elements: [
48
- {
49
- tag: 'markdown',
50
- content: '☁️ *正在思考中...*',
51
- text_align: 'left',
52
- element_id: STREAMING_ELEMENT_ID,
53
- },
54
- ],
55
- },
56
- }
57
- }
58
-
59
- /** 已渲染完成的卡片:Schema 2.0,无 streaming_mode,单 markdown 元素。
60
- *
61
- * 代码块的 "10 行代码 >" 手机端 bug 是通过 `optimizeMarkdownForFeishu` 把
62
- * fenced code 降级成纯文字来规避的,不依赖多元素结构。这里就是最朴素的
63
- * 一张纯 markdown 卡片。 */
64
- export function buildRenderedCard(renderedMarkdown: string): Record<string, unknown> {
65
- return {
66
- schema: '2.0',
67
- config: {
68
- update_multi: true,
69
- },
70
- body: {
71
- elements: [
72
- {
73
- tag: 'markdown',
74
- content: renderedMarkdown || ' ',
75
- text_align: 'left',
76
- },
77
- ],
78
- },
79
- }
80
- }
81
-
82
- /** 错误卡片:红色 header + 错误文本。用于 abort() 兜底。 */
83
- export function buildErrorCard(message: string): Record<string, unknown> {
84
- return {
85
- schema: '2.0',
86
- config: { update_multi: true },
87
- header: {
88
- title: { tag: 'plain_text', content: '❌ 出错了' },
89
- template: 'red',
90
- },
91
- body: {
92
- elements: [
93
- {
94
- tag: 'markdown',
95
- content: message || '未知错误',
96
- },
97
- ],
98
- },
99
- }
100
- }
101
-
102
- /** 从末尾截取最多 maxLen 个字符;超过时前缀 "..." 保留最新 maxLen-3 个字。
103
- *
104
- * 思考内容往往是"先分析 → 得出结论"的线性过程,截取末尾比截取开头更有用 —— 用户
105
- * 最关心的是"模型现在在想什么",不是"五千个 token 前在想什么"。 */
106
- function truncateReasoningPreview(text: string, maxLen: number): string {
107
- if (text.length <= maxLen) return text
108
- return '...' + text.slice(text.length - maxLen + 3)
109
- }
110
-
111
- // ---------------------------------------------------------------------------
112
- // State machine
113
- // ---------------------------------------------------------------------------
114
-
115
- export type StreamingCardPhase =
116
- | 'idle' // constructor 后、ensureCreated 之前
117
- | 'creating' // ensureCreated 进行中
118
- | 'streaming' // 初始卡已发出,接受 appendText
119
- | 'finalizing' // finalize 进行中
120
- | 'completed' // finalize 完成
121
- | 'aborted' // abort 已调用
122
-
123
- export type StreamingCardDeps = {
124
- larkClient: Lark.Client
125
- chatId: string
126
- replyToMessageId?: string
127
- }
128
-
129
- /** One entry in the tool-use trace displayed above the answer text. */
130
- type ToolStep = {
131
- /** Prefer toolUseId for dedup; fall back to a synthetic id when missing. */
132
- id: string
133
- name: string
134
- status: 'running' | 'done'
135
- }
136
-
137
- /** 最多保留的 reasoning 预览字符数,超过则取末尾 + 省略号前缀。 */
138
- const REASONING_PREVIEW_CHARS = 600
139
-
140
- /** 连续 streamCardContent 失败多少次后才放弃 CardKit 流式。
141
- * 设成 3 而不是 1,是为了避免单次抖动(网络、临时校验失败等)把整张卡片
142
- * 冻结到 finalize —— 用户看到的就是 "long wait → 一次性 dump"。 */
143
- const STREAM_FAIL_DISABLE_THRESHOLD = 3
144
-
145
- export class StreamingCard {
146
- // ---- lifecycle state ----
147
- private phase: StreamingCardPhase = 'idle'
148
-
149
- // ---- CardKit state ----
150
- /** CardKit card_id。null = CardKit 创建失败,已退到 patch fallback 模式。 */
151
- private cardId: string | null = null
152
- /** IM message_id。始终应该有值(否则连 patch 也做不了)。 */
153
- private messageId: string | null = null
154
- /** CardKit cardElement.content() 单调递增序列号。 */
155
- private sequence = 0
156
- /** CardKit 流式还在工作。230099 或连续 N 次未知错误之后置为 false,
157
- * 中间帧将跳过,最终 finalize 仍会尝试 settings+update(cardId 仍然有效)。 */
158
- private cardKitStreamActive = false
159
- /** 连续 streamCardContent 未知错误计数。一次成功就清零。 */
160
- private consecutiveStreamFailures = 0
161
-
162
- // ---- text state ----
163
- private accumulatedText = ''
164
- private lastFlushedText = ''
165
- /** 累积 thinking_delta,渲染为卡片顶部的推理预览 blockquote。 */
166
- private accumulatedReasoningText = ''
167
- /** 工具调用轨迹:按 startTool 调用顺序排列,completeTool 改其 status。 */
168
- private toolSteps: ToolStep[] = []
169
-
170
- // ---- flush ----
171
- private flushController: FlushController
172
-
173
- constructor(private readonly deps: StreamingCardDeps) {
174
- this.flushController = new FlushController(() => this.performFlush())
175
- }
176
-
177
- // ------------------------------------------------------------------
178
- // Public API
179
- // ------------------------------------------------------------------
180
-
181
- /**
182
- * 首次创建卡片(CardKit 主路径;失败则降级到直发 Schema 2.0 卡 + patch)。
183
- * 幂等:已创建/正在创建时直接返回。
184
- */
185
- async ensureCreated(): Promise<void> {
186
- if (this.phase !== 'idle') return
187
- this.phase = 'creating'
188
-
189
- try {
190
- // CardKit 主路径
191
- const cardId = await createCardEntity(
192
- this.deps.larkClient,
193
- buildInitialStreamingCard(),
194
- )
195
- const messageId = await sendCardAsMessage(
196
- this.deps.larkClient,
197
- this.deps.chatId,
198
- cardId,
199
- this.deps.replyToMessageId,
200
- )
201
- this.cardId = cardId
202
- this.messageId = messageId
203
- this.cardKitStreamActive = true
204
- this.sequence = 1
205
- this.phase = 'streaming'
206
- this.flushController.setCardMessageReady(true)
207
- } catch (cardKitErr) {
208
- // CardKit 不可用(权限、网络、API 兼容性等)→ 降级到直发卡片 + patch
209
- console.warn(
210
- '[Feishu StreamingCard] CardKit create/send failed, falling back to im.message.patch:',
211
- cardKitErr instanceof Error ? cardKitErr.message : cardKitErr,
212
- )
213
- try {
214
- const fallbackResp = await this.deps.larkClient.im.message.create({
215
- params: { receive_id_type: 'chat_id' },
216
- data: {
217
- receive_id: this.deps.chatId,
218
- msg_type: 'interactive',
219
- content: JSON.stringify(buildRenderedCard(' ')),
220
- },
221
- })
222
- const mid = fallbackResp.data?.message_id
223
- if (!mid) {
224
- throw new Error('fallback im.message.create returned no message_id')
225
- }
226
- this.cardId = null
227
- this.messageId = mid
228
- this.cardKitStreamActive = false
229
- this.phase = 'streaming'
230
- this.flushController.setCardMessageReady(true)
231
- } catch (fallbackErr) {
232
- // 兜底都失败了 —— 无法显示任何东西
233
- console.error(
234
- '[Feishu StreamingCard] Fallback card creation also failed:',
235
- fallbackErr instanceof Error ? fallbackErr.message : fallbackErr,
236
- )
237
- this.phase = 'aborted'
238
- throw fallbackErr
239
- }
240
- }
241
-
242
- // 卡片可写之后若已有 buffered 内容(text / reasoning / tools),
243
- // 立刻触发一次 flush —— 否则 content_start{tool_use} 或 thinking 在
244
- // ensureCreated 期间到达的状态会一直卡在节流 gate 上,用户看不到。
245
- if (this.hasAnyContent()) {
246
- void this.flushController.throttledUpdate(this.currentThrottle())
247
- }
248
- }
249
-
250
- /** 追加文本增量。不等待,只安排一次节流 flush。 */
251
- appendText(delta: string): void {
252
- if (!delta) return
253
- if (this.phase === 'completed' || this.phase === 'aborted') return
254
- this.accumulatedText += delta
255
- void this.flushController.throttledUpdate(this.currentThrottle())
256
- }
257
-
258
- /** 追加 reasoning/thinking delta —— 与 appendText 并列,渲染为顶部预览。 */
259
- appendReasoning(delta: string): void {
260
- if (!delta) return
261
- if (this.phase === 'completed' || this.phase === 'aborted') return
262
- this.accumulatedReasoningText += delta
263
- void this.flushController.throttledUpdate(this.currentThrottle())
264
- }
265
-
266
- /** 记录一次 tool_use 开始。dedupe 按 toolUseId(缺省时按 name+index)。 */
267
- startTool(toolUseId: string | undefined, toolName: string | undefined): void {
268
- if (this.phase === 'completed' || this.phase === 'aborted') return
269
- if (!toolName) return
270
- const id = toolUseId || `${toolName}#${this.toolSteps.length}`
271
- if (this.toolSteps.some((s) => s.id === id)) return
272
- this.toolSteps.push({ id, name: toolName, status: 'running' })
273
- void this.flushController.throttledUpdate(this.currentThrottle())
274
- }
275
-
276
- /** 把指定 tool 的状态从 running 切到 done。先按 id 匹配,再 fallback name。 */
277
- completeTool(toolUseId: string | undefined, toolName: string | undefined): void {
278
- if (this.phase === 'completed' || this.phase === 'aborted') return
279
- let step: ToolStep | undefined
280
- if (toolUseId) {
281
- step = this.toolSteps.find((s) => s.id === toolUseId)
282
- }
283
- if (!step && toolName) {
284
- for (let i = this.toolSteps.length - 1; i >= 0; i--) {
285
- const s = this.toolSteps[i]!
286
- if (s.name === toolName && s.status === 'running') {
287
- step = s
288
- break
289
- }
290
- }
291
- }
292
- if (!step) return
293
- if (step.status === 'done') return
294
- step.status = 'done'
295
- void this.flushController.throttledUpdate(this.currentThrottle())
296
- }
297
-
298
- /** 是否已有任何可渲染内容(文本 / 推理 / 工具)。 */
299
- private hasAnyContent(): boolean {
300
- return (
301
- this.accumulatedText.length > 0 ||
302
- this.accumulatedReasoningText.length > 0 ||
303
- this.toolSteps.length > 0
304
- )
305
- }
306
-
307
- /**
308
- * 流式结束,切到最终态。
309
- * - 先 waitForFlush 确保中间帧写入完成
310
- * - 然后 close streaming_mode(仅 CardKit 路径)
311
- * - 最后用完整 rendered 卡片 update
312
- * - complete FlushController 锁死,后续 appendText 被忽略
313
- */
314
- async finalize(): Promise<void> {
315
- if (this.phase === 'completed' || this.phase === 'aborted') return
316
- if (this.phase === 'idle') {
317
- // 完全没开始 —— 直接标记完成
318
- this.phase = 'completed'
319
- this.flushController.complete()
320
- return
321
- }
322
- this.phase = 'finalizing'
323
- this.flushController.cancelPendingFlush()
324
- await this.flushController.waitForFlush()
325
-
326
- const finalText = this.terminalText()
327
- try {
328
- if (this.cardId) {
329
- // CardKit 路径: settings(false) + card.update(即使中间 stream 曾失败)
330
- this.sequence += 1
331
- await setCardStreamingMode(
332
- this.deps.larkClient,
333
- this.cardId,
334
- false,
335
- this.sequence,
336
- )
337
- this.sequence += 1
338
- await updateCardKitCard(
339
- this.deps.larkClient,
340
- this.cardId,
341
- buildRenderedCard(finalText),
342
- this.sequence,
343
- )
344
- } else if (this.messageId) {
345
- // Patch fallback 路径: 全量替换
346
- await this.deps.larkClient.im.message.patch({
347
- path: { message_id: this.messageId },
348
- data: { content: JSON.stringify(buildRenderedCard(finalText)) },
349
- })
350
- }
351
- } catch (err) {
352
- console.error(
353
- '[Feishu StreamingCard] finalize failed:',
354
- err instanceof Error ? err.message : err,
355
- )
356
- // 不抛出 —— 用户已经看到某种版本的内容,finalize 失败不是致命错误
357
- } finally {
358
- this.phase = 'completed'
359
- this.lastFlushedText = finalText
360
- this.flushController.complete()
361
- }
362
- }
363
-
364
- /** 错误中止 —— 尝试把错误信息渲染到卡片上。 */
365
- async abort(err: Error): Promise<void> {
366
- if (this.phase === 'completed' || this.phase === 'aborted') return
367
- const wasIdle = this.phase === 'idle'
368
- this.phase = 'aborted'
369
- this.flushController.cancelPendingFlush()
370
- await this.flushController.waitForFlush().catch(() => {})
371
-
372
- if (wasIdle || !this.messageId) {
373
- // 卡片还没创建成功,没法渲染错误 —— 由上层 sendText 兜底
374
- this.flushController.complete()
375
- return
376
- }
377
-
378
- const errCard = buildErrorCard(
379
- `${err.message}${this.accumulatedText ? '\n\n——\n\n' + this.accumulatedText : ''}`,
380
- )
381
- try {
382
- if (this.cardId) {
383
- this.sequence += 1
384
- await setCardStreamingMode(
385
- this.deps.larkClient,
386
- this.cardId,
387
- false,
388
- this.sequence,
389
- ).catch(() => {}) // 关流失败无所谓,update 才是关键
390
- this.sequence += 1
391
- await updateCardKitCard(
392
- this.deps.larkClient,
393
- this.cardId,
394
- errCard,
395
- this.sequence,
396
- )
397
- } else {
398
- await this.deps.larkClient.im.message.patch({
399
- path: { message_id: this.messageId },
400
- data: { content: JSON.stringify(errCard) },
401
- })
402
- }
403
- } catch (renderErr) {
404
- console.error(
405
- '[Feishu StreamingCard] abort render failed:',
406
- renderErr instanceof Error ? renderErr.message : renderErr,
407
- )
408
- } finally {
409
- this.flushController.complete()
410
- }
411
- }
412
-
413
- // ------------------------------------------------------------------
414
- // Internals
415
- // ------------------------------------------------------------------
416
-
417
- /** 当前应使用的节流时长。 */
418
- private currentThrottle(): number {
419
- return this.cardKitStreamActive ? THROTTLE.CARDKIT_MS : THROTTLE.PATCH_MS
420
- }
421
-
422
- /** 组合 reasoning + toolSteps + answerText,经 sanitize + optimize 管道出来。
423
- *
424
- * 顺序为 tools → reasoning → answer,以分隔符隔开:
425
- * - tools 永远在最顶部,方便用户先看到 "现在在跑什么"
426
- * - reasoning 居中(thinking 文本)
427
- * - answer 在底部
428
- *
429
- * 整张卡片只用最朴素的 markdown:plain text + emoji + bold + line break。
430
- * **不使用** blockquote / list / heading —— 这些会被
431
- * optimizeMarkdownForFeishu 触发额外的 <br> 注入和 H 降级,并且历史上
432
- * 曾导致飞书 CardKit 校验报错("long wait → 一次性 dump" 退化的根因)。
433
- * 任意 section 为空则忽略;全部为空时返回等待提示。 */
434
- private renderedText(): string {
435
- const sections: string[] = []
436
-
437
- if (this.toolSteps.length > 0) {
438
- // 单行 inline 形式: ⚙️ Bash · ✅ Read · ⚙️ Glob ...
439
- // 用中点分隔,比 markdown list 更不容易触发 Feishu 排版异常
440
- const inline = this.toolSteps
441
- .map((s) => `${s.status === 'done' ? '✅' : '⚙️'} ${s.name}`)
442
- .join(' · ')
443
- sections.push(`🛠️ ${inline}`)
444
- }
445
-
446
- if (this.accumulatedReasoningText) {
447
- const preview = truncateReasoningPreview(
448
- this.accumulatedReasoningText,
449
- REASONING_PREVIEW_CHARS,
450
- )
451
- // openclaw 风格: 一行 header + 空行 + 原文。不引用 / 不缩进,让飞书
452
- // markdown 元素按普通段落渲染。
453
- sections.push(`💭 **思考中**\n\n${preview}`)
454
- }
455
-
456
- if (this.accumulatedText) {
457
- sections.push(this.accumulatedText)
458
- }
459
-
460
- if (sections.length === 0) return '☁️ *正在思考中...*'
461
-
462
- // 用一行分隔符把 sections 分开,比单纯空行更稳定
463
- const composed = sections.join('\n\n---\n\n')
464
-
465
- // 表格数限制在 optimize 之前做 —— sanitize 对原始 markdown 最准
466
- const limited = sanitizeTextForCard(composed)
467
- return optimizeMarkdownForFeishu(limited, 2)
468
- }
469
-
470
- /** 终态文本: 只渲染最终答复正文,丢弃 reasoning 和 toolSteps。
471
- *
472
- * 推理过程和工具调用是"过程态"信息,已经在流式中展示给用户看过;
473
- * message_complete 之后用户应该看到一张干净的答复卡(与 Desktop UI 对齐)。
474
- * 这个方法专供 finalize 调用,不要在中间帧用。
475
- *
476
- * 边界情况: 如果完全没有 accumulatedText(比如纯 thinking 没产出答案
477
- * 这种异常 case),退回到 renderedText() 至少把推理留下来当兜底。 */
478
- private terminalText(): string {
479
- if (this.accumulatedText) {
480
- const limited = sanitizeTextForCard(this.accumulatedText)
481
- return optimizeMarkdownForFeishu(limited, 2)
482
- }
483
- return this.renderedText()
484
- }
485
-
486
- /** FlushController 调用的 doFlush。 */
487
- private async performFlush(): Promise<void> {
488
- if (this.phase !== 'streaming') return
489
- if (!this.messageId) return
490
-
491
- // CardKit 中间帧被禁用但 cardId 仍有效 —— 跳过中间 flush,
492
- // 等 finalize 用 cardId 做最终 settings + update
493
- if (this.cardId && !this.cardKitStreamActive) return
494
-
495
- const finalText = this.renderedText()
496
- if (finalText === this.lastFlushedText) return
497
-
498
- if (this.cardKitStreamActive && this.cardId) {
499
- // CardKit 主路径
500
- this.sequence += 1
501
- try {
502
- await streamCardContent(
503
- this.deps.larkClient,
504
- this.cardId,
505
- STREAMING_ELEMENT_ID,
506
- finalText,
507
- this.sequence,
508
- )
509
- this.lastFlushedText = finalText
510
- this.consecutiveStreamFailures = 0
511
- } catch (err) {
512
- if (isCardRateLimitError(err)) {
513
- // 跳帧 —— 下次 throttledUpdate 会重试
514
- return
515
- }
516
- if (isCardTableLimitError(err)) {
517
- // 表格超限 —— 禁用流式中间帧,等 finalize 用 update 一次性发完整卡
518
- console.warn(
519
- '[Feishu StreamingCard] 230099 table limit, disabling CardKit streaming',
520
- )
521
- this.cardKitStreamActive = false
522
- return
523
- }
524
- // 其他错误 —— 跳帧重试,避免单次失败把整张卡冻在最初状态。
525
- // 只有连续失败超过阈值才认定 CardKit 不可用并降级 —— 否则
526
- // 用户会看到 "long wait → 完事后一次性把所有内容刷出来" 的体验
527
- // 退化(这是 streamCardContent 一旦报错就 disable 流式造成的)。
528
- this.consecutiveStreamFailures += 1
529
- const errMsg = err instanceof Error ? err.message : String(err)
530
- if (this.consecutiveStreamFailures === 1) {
531
- // 首帧失败先记录一次,避免日志风暴
532
- console.warn(
533
- '[Feishu StreamingCard] stream flush failed (will retry):',
534
- errMsg,
535
- )
536
- }
537
- if (this.consecutiveStreamFailures >= STREAM_FAIL_DISABLE_THRESHOLD) {
538
- console.error(
539
- `[Feishu StreamingCard] stream flush failed ${this.consecutiveStreamFailures}× consecutively, disabling CardKit streaming until finalize:`,
540
- errMsg,
541
- )
542
- this.cardKitStreamActive = false
543
- }
544
- return
545
- }
546
- } else {
547
- // Patch fallback 路径(CardKit 从未成功)
548
- try {
549
- await this.deps.larkClient.im.message.patch({
550
- path: { message_id: this.messageId },
551
- data: { content: JSON.stringify(buildRenderedCard(finalText)) },
552
- })
553
- this.lastFlushedText = finalText
554
- } catch (err) {
555
- if (isCardRateLimitError(err)) return
556
- console.error(
557
- '[Feishu StreamingCard] patch flush failed:',
558
- err instanceof Error ? err.message : err,
559
- )
560
- }
561
- }
562
- }
563
-
564
- // ------------------------------------------------------------------
565
- // Test helpers (exposed for unit tests, not part of public API)
566
- // ------------------------------------------------------------------
567
-
568
- /** @internal */
569
- _getPhase(): StreamingCardPhase {
570
- return this.phase
571
- }
572
-
573
- /** @internal */
574
- _getCardId(): string | null {
575
- return this.cardId
576
- }
577
-
578
- /** @internal */
579
- _getMessageId(): string | null {
580
- return this.messageId
581
- }
582
-
583
- /** @internal */
584
- _getSequence(): number {
585
- return this.sequence
586
- }
587
-
588
- /** @internal */
589
- _isCardKitStreamActive(): boolean {
590
- return this.cardKitStreamActive
591
- }
592
-
593
- /** @internal */
594
- _getAccumulatedText(): string {
595
- return this.accumulatedText
596
- }
597
-
598
- /** @internal */
599
- _getAccumulatedReasoning(): string {
600
- return this.accumulatedReasoningText
601
- }
602
-
603
- /** @internal */
604
- _getToolSteps(): ReadonlyArray<ToolStep> {
605
- return this.toolSteps
606
- }
607
-
608
- /** @internal */
609
- _getFlushController(): FlushController {
610
- return this.flushController
611
- }
612
- }
@@ -1,23 +0,0 @@
1
- {
2
- "name": "claude-code-im-adapters",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "scripts": {
7
- "telegram": "bun run telegram/index.ts",
8
- "feishu": "bun run feishu/index.ts",
9
- "test": "bun test",
10
- "test:common": "bun test common/",
11
- "test:telegram": "bun test telegram/",
12
- "test:feishu": "bun test feishu/"
13
- },
14
- "dependencies": {
15
- "grammy": "^1.42.0",
16
- "@larksuiteoapi/node-sdk": "^1.60.0",
17
- "ws": "^8.18.0"
18
- },
19
- "devDependencies": {
20
- "@types/ws": "^8.5.0",
21
- "bun-types": "latest"
22
- }
23
- }