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.
- package/bin/bingo-win.cjs +2 -1
- package/bin/bingocode-win.cjs +2 -1
- package/bin/claude-win.cjs +2 -1
- package/bun.lock +1716 -0
- package/package.json +14 -2
- package/src/server/config/providers.yaml +1 -1
- package/src/server/proxy/transform/anthropicToOpenaiChat.ts +23 -9
- package/adapters/README.md +0 -87
- package/adapters/common/__tests__/chat-queue.test.ts +0 -61
- package/adapters/common/__tests__/format.test.ts +0 -148
- package/adapters/common/__tests__/http-client.test.ts +0 -105
- package/adapters/common/__tests__/message-buffer.test.ts +0 -84
- package/adapters/common/__tests__/message-dedup.test.ts +0 -57
- package/adapters/common/__tests__/session-store.test.ts +0 -62
- package/adapters/common/__tests__/ws-bridge.test.ts +0 -177
- package/adapters/common/attachment/__tests__/attachment-limits.test.ts +0 -52
- package/adapters/common/attachment/__tests__/attachment-store.test.ts +0 -108
- package/adapters/common/attachment/__tests__/image-block-watcher.test.ts +0 -115
- package/adapters/common/attachment/attachment-limits.ts +0 -58
- package/adapters/common/attachment/attachment-store.ts +0 -121
- package/adapters/common/attachment/attachment-types.ts +0 -29
- package/adapters/common/attachment/image-block-watcher.ts +0 -94
- package/adapters/common/chat-queue.ts +0 -24
- package/adapters/common/config.ts +0 -96
- package/adapters/common/format.ts +0 -229
- package/adapters/common/http-client.ts +0 -107
- package/adapters/common/message-buffer.ts +0 -91
- package/adapters/common/message-dedup.ts +0 -57
- package/adapters/common/pairing.ts +0 -149
- package/adapters/common/session-store.ts +0 -60
- package/adapters/common/ws-bridge.ts +0 -282
- package/adapters/feishu/__tests__/card-errors.test.ts +0 -194
- package/adapters/feishu/__tests__/cardkit.test.ts +0 -295
- package/adapters/feishu/__tests__/extract-payload.test.ts +0 -77
- package/adapters/feishu/__tests__/feishu.test.ts +0 -907
- package/adapters/feishu/__tests__/flush-controller.test.ts +0 -290
- package/adapters/feishu/__tests__/markdown-style.test.ts +0 -353
- package/adapters/feishu/__tests__/media.test.ts +0 -120
- package/adapters/feishu/__tests__/streaming-card.test.ts +0 -914
- package/adapters/feishu/card-errors.ts +0 -151
- package/adapters/feishu/cardkit.ts +0 -294
- package/adapters/feishu/extract-payload.ts +0 -95
- package/adapters/feishu/flush-controller.ts +0 -149
- package/adapters/feishu/index.ts +0 -1275
- package/adapters/feishu/markdown-style.ts +0 -212
- package/adapters/feishu/media.ts +0 -176
- package/adapters/feishu/streaming-card.ts +0 -612
- package/adapters/package.json +0 -23
- package/adapters/telegram/__tests__/media.test.ts +0 -86
- package/adapters/telegram/__tests__/telegram.test.ts +0 -115
- package/adapters/telegram/index.ts +0 -754
- package/adapters/telegram/media.ts +0 -89
- package/adapters/tsconfig.json +0 -18
- package/runtime/mac_helper.py +0 -775
- package/runtime/requirements-win.txt +0 -7
- package/runtime/requirements.txt +0 -6
- package/runtime/test_helpers.py +0 -322
- package/runtime/win_helper.py +0 -723
- package/scripts/count-app-loc.ts +0 -256
- package/scripts/release.ts +0 -130
- package/start-cli.bat +0 -7
- package/stubs/ant-claude-for-chrome-mcp.ts +0 -24
- package/stubs/color-diff-napi.ts +0 -45
|
@@ -1,914 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* StreamingCard 生命周期测试
|
|
3
|
-
*
|
|
4
|
-
* 用 mock Lark client 覆盖:
|
|
5
|
-
* - ensureCreated: 成功路径 / 降级路径
|
|
6
|
-
* - appendText: 累积 + 触发 throttled flush
|
|
7
|
-
* - finalize: settings(false) + update 顺序、sequence 单调递增
|
|
8
|
-
* - abort: 渲染错误卡片
|
|
9
|
-
* - 230020 → 跳帧
|
|
10
|
-
* - 230099 table limit → 禁用流式,finalize 时仍走 CardKit
|
|
11
|
-
* - 纯 patch fallback 路径
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, beforeEach } from 'bun:test'
|
|
15
|
-
import {
|
|
16
|
-
StreamingCard,
|
|
17
|
-
buildInitialStreamingCard,
|
|
18
|
-
buildRenderedCard,
|
|
19
|
-
buildErrorCard,
|
|
20
|
-
} from '../streaming-card.js'
|
|
21
|
-
import { STREAMING_ELEMENT_ID } from '../cardkit.js'
|
|
22
|
-
|
|
23
|
-
// ---------------------------------------------------------------------------
|
|
24
|
-
// Mock client
|
|
25
|
-
// ---------------------------------------------------------------------------
|
|
26
|
-
|
|
27
|
-
type ApiCall = { api: string; args: any }
|
|
28
|
-
|
|
29
|
-
type MockBehavior = {
|
|
30
|
-
'card.create'?: any | ((args: any) => any)
|
|
31
|
-
'card.settings'?: any | ((args: any) => any)
|
|
32
|
-
'card.update'?: any | ((args: any) => any)
|
|
33
|
-
'cardElement.content'?: any | ((args: any, callIdx: number) => any)
|
|
34
|
-
'im.message.create'?: any | ((args: any) => any)
|
|
35
|
-
'im.message.reply'?: any | ((args: any) => any)
|
|
36
|
-
'im.message.patch'?: any | ((args: any, callIdx: number) => any)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function makeMockClient(behavior: MockBehavior = {}) {
|
|
40
|
-
const calls: ApiCall[] = []
|
|
41
|
-
let contentCallIdx = 0
|
|
42
|
-
let patchCallIdx = 0
|
|
43
|
-
|
|
44
|
-
function handle(api: string, resp: any, args: any, idx?: number): any {
|
|
45
|
-
calls.push({ api, args })
|
|
46
|
-
if (typeof resp === 'function') return resp(args, idx ?? 0)
|
|
47
|
-
return resp
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const client: any = {
|
|
51
|
-
cardkit: {
|
|
52
|
-
v1: {
|
|
53
|
-
card: {
|
|
54
|
-
create: async (args: any) =>
|
|
55
|
-
handle('cardkit.v1.card.create', behavior['card.create'] ?? {
|
|
56
|
-
code: 0, data: { card_id: 'ck_default' },
|
|
57
|
-
}, args),
|
|
58
|
-
settings: async (args: any) =>
|
|
59
|
-
handle('cardkit.v1.card.settings', behavior['card.settings'] ?? { code: 0 }, args),
|
|
60
|
-
update: async (args: any) =>
|
|
61
|
-
handle('cardkit.v1.card.update', behavior['card.update'] ?? { code: 0 }, args),
|
|
62
|
-
},
|
|
63
|
-
cardElement: {
|
|
64
|
-
content: async (args: any) => {
|
|
65
|
-
const idx = contentCallIdx++
|
|
66
|
-
return handle('cardkit.v1.cardElement.content',
|
|
67
|
-
behavior['cardElement.content'] ?? { code: 0 }, args, idx)
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
im: {
|
|
73
|
-
message: {
|
|
74
|
-
create: async (args: any) =>
|
|
75
|
-
handle('im.message.create', behavior['im.message.create'] ?? {
|
|
76
|
-
data: { message_id: 'om_default' },
|
|
77
|
-
}, args),
|
|
78
|
-
reply: async (args: any) =>
|
|
79
|
-
handle('im.message.reply', behavior['im.message.reply'] ?? {
|
|
80
|
-
data: { message_id: 'om_reply_default' },
|
|
81
|
-
}, args),
|
|
82
|
-
patch: async (args: any) => {
|
|
83
|
-
const idx = patchCallIdx++
|
|
84
|
-
return handle('im.message.patch', behavior['im.message.patch'] ?? { code: 0 }, args, idx)
|
|
85
|
-
},
|
|
86
|
-
},
|
|
87
|
-
},
|
|
88
|
-
}
|
|
89
|
-
return { client, calls }
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
async function sleep(ms: number) {
|
|
93
|
-
await new Promise((r) => setTimeout(r, ms))
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
// ---------------------------------------------------------------------------
|
|
97
|
-
// Card JSON builders
|
|
98
|
-
// ---------------------------------------------------------------------------
|
|
99
|
-
|
|
100
|
-
describe('buildInitialStreamingCard', () => {
|
|
101
|
-
it('Schema 2.0 + streaming_mode + element_id', () => {
|
|
102
|
-
const card = buildInitialStreamingCard() as any
|
|
103
|
-
expect(card.schema).toBe('2.0')
|
|
104
|
-
expect(card.config.streaming_mode).toBe(true)
|
|
105
|
-
// 唯一元素:streaming_content,初始内容为 loading 提示
|
|
106
|
-
const elements = card.body.elements as any[]
|
|
107
|
-
expect(elements.length).toBe(1)
|
|
108
|
-
const streaming = elements[0]
|
|
109
|
-
expect(streaming.tag).toBe('markdown')
|
|
110
|
-
expect(streaming.content).toContain('正在思考中')
|
|
111
|
-
expect(streaming.element_id).toBe(STREAMING_ELEMENT_ID)
|
|
112
|
-
})
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
describe('buildRenderedCard', () => {
|
|
116
|
-
it('Schema 2.0, 无 streaming_mode, 单 markdown 元素', () => {
|
|
117
|
-
const card = buildRenderedCard('hello world') as any
|
|
118
|
-
expect(card.schema).toBe('2.0')
|
|
119
|
-
expect(card.config.streaming_mode).toBeUndefined()
|
|
120
|
-
expect(card.body.elements.length).toBe(1)
|
|
121
|
-
const el = card.body.elements[0]
|
|
122
|
-
expect(el.tag).toBe('markdown')
|
|
123
|
-
expect(el.content).toBe('hello world')
|
|
124
|
-
// 最终卡无需 element_id
|
|
125
|
-
expect(el.element_id).toBeUndefined()
|
|
126
|
-
})
|
|
127
|
-
|
|
128
|
-
it('空字符串保底为单空格', () => {
|
|
129
|
-
const card = buildRenderedCard('') as any
|
|
130
|
-
expect(card.body.elements[0].content).toBe(' ')
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
describe('buildErrorCard', () => {
|
|
135
|
-
it('红色 header + markdown body', () => {
|
|
136
|
-
const card = buildErrorCard('oops') as any
|
|
137
|
-
expect((card.header as any).template).toBe('red')
|
|
138
|
-
expect((card.header as any).title.content).toContain('出错')
|
|
139
|
-
expect(card.body.elements[0].content).toBe('oops')
|
|
140
|
-
})
|
|
141
|
-
})
|
|
142
|
-
|
|
143
|
-
// ---------------------------------------------------------------------------
|
|
144
|
-
// StreamingCard lifecycle
|
|
145
|
-
// ---------------------------------------------------------------------------
|
|
146
|
-
|
|
147
|
-
describe('StreamingCard: ensureCreated (CardKit 主路径)', () => {
|
|
148
|
-
it('依次调用 card.create + im.message.create,sequence=1', async () => {
|
|
149
|
-
const { client, calls } = makeMockClient({
|
|
150
|
-
'card.create': { code: 0, data: { card_id: 'ck_main_1' } },
|
|
151
|
-
'im.message.create': { data: { message_id: 'om_main_1' } },
|
|
152
|
-
})
|
|
153
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'oc_chat_1' })
|
|
154
|
-
await sc.ensureCreated()
|
|
155
|
-
|
|
156
|
-
expect(sc._getPhase()).toBe('streaming')
|
|
157
|
-
expect(sc._getCardId()).toBe('ck_main_1')
|
|
158
|
-
expect(sc._getMessageId()).toBe('om_main_1')
|
|
159
|
-
expect(sc._getSequence()).toBe(1)
|
|
160
|
-
expect(sc._isCardKitStreamActive()).toBe(true)
|
|
161
|
-
|
|
162
|
-
expect(calls[0]!.api).toBe('cardkit.v1.card.create')
|
|
163
|
-
expect(calls[1]!.api).toBe('im.message.create')
|
|
164
|
-
|
|
165
|
-
// 初始卡 JSON 包含 streaming_mode 和 element_id
|
|
166
|
-
const cardJson = JSON.parse(calls[0]!.args.data.data)
|
|
167
|
-
expect(cardJson.schema).toBe('2.0')
|
|
168
|
-
expect(cardJson.config.streaming_mode).toBe(true)
|
|
169
|
-
// 唯一元素即 streaming_content
|
|
170
|
-
expect(cardJson.body.elements[0].element_id).toBe(STREAMING_ELEMENT_ID)
|
|
171
|
-
|
|
172
|
-
// IM message 引用 card_id
|
|
173
|
-
const content = JSON.parse(calls[1]!.args.data.content)
|
|
174
|
-
expect(content).toEqual({ type: 'card', data: { card_id: 'ck_main_1' } })
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
it('幂等: 重复调用 ensureCreated 不重复创建', async () => {
|
|
178
|
-
const { client, calls } = makeMockClient({
|
|
179
|
-
'card.create': { code: 0, data: { card_id: 'ck_1' } },
|
|
180
|
-
'im.message.create': { data: { message_id: 'om_1' } },
|
|
181
|
-
})
|
|
182
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
183
|
-
await sc.ensureCreated()
|
|
184
|
-
await sc.ensureCreated()
|
|
185
|
-
await sc.ensureCreated()
|
|
186
|
-
// 只一次 create + 一次 send
|
|
187
|
-
const createCalls = calls.filter((c) => c.api === 'cardkit.v1.card.create')
|
|
188
|
-
const sendCalls = calls.filter((c) => c.api === 'im.message.create')
|
|
189
|
-
expect(createCalls.length).toBe(1)
|
|
190
|
-
expect(sendCalls.length).toBe(1)
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
it('replyToMessageId 走 im.message.reply 而非 create', async () => {
|
|
194
|
-
const { client, calls } = makeMockClient({
|
|
195
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
196
|
-
'im.message.reply': { data: { message_id: 'om_reply' } },
|
|
197
|
-
})
|
|
198
|
-
const sc = new StreamingCard({
|
|
199
|
-
larkClient: client,
|
|
200
|
-
chatId: 'c',
|
|
201
|
-
replyToMessageId: 'om_parent',
|
|
202
|
-
})
|
|
203
|
-
await sc.ensureCreated()
|
|
204
|
-
expect(calls.some((c) => c.api === 'im.message.reply')).toBe(true)
|
|
205
|
-
expect(calls.some((c) => c.api === 'im.message.create')).toBe(false)
|
|
206
|
-
expect(sc._getMessageId()).toBe('om_reply')
|
|
207
|
-
})
|
|
208
|
-
})
|
|
209
|
-
|
|
210
|
-
describe('StreamingCard: ensureCreated (fallback 降级路径)', () => {
|
|
211
|
-
it('CardKit create 失败 → 直发 Schema 2.0 卡 + patch 模式', async () => {
|
|
212
|
-
const { client, calls } = makeMockClient({
|
|
213
|
-
'card.create': { code: 99991672, msg: 'permission denied' },
|
|
214
|
-
'im.message.create': { data: { message_id: 'om_fb' } },
|
|
215
|
-
})
|
|
216
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
217
|
-
await sc.ensureCreated()
|
|
218
|
-
|
|
219
|
-
expect(sc._getPhase()).toBe('streaming')
|
|
220
|
-
expect(sc._getCardId()).toBeNull()
|
|
221
|
-
expect(sc._getMessageId()).toBe('om_fb')
|
|
222
|
-
expect(sc._isCardKitStreamActive()).toBe(false)
|
|
223
|
-
|
|
224
|
-
// fallback 发送的是 Schema 2.0 interactive 卡
|
|
225
|
-
const createCall = calls.find((c) => c.api === 'im.message.create')
|
|
226
|
-
expect(createCall).toBeDefined()
|
|
227
|
-
expect(createCall!.args.data.msg_type).toBe('interactive')
|
|
228
|
-
const cardContent = JSON.parse(createCall!.args.data.content)
|
|
229
|
-
expect(cardContent.schema).toBe('2.0')
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('CardKit send 失败(create 成功但 im.message.create 失败)也能降级', async () => {
|
|
233
|
-
let sendCallCount = 0
|
|
234
|
-
const { client } = makeMockClient({
|
|
235
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
236
|
-
'im.message.create': () => {
|
|
237
|
-
sendCallCount++
|
|
238
|
-
if (sendCallCount === 1) throw new Error('send failed')
|
|
239
|
-
return { data: { message_id: 'om_fb2' } }
|
|
240
|
-
},
|
|
241
|
-
})
|
|
242
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
243
|
-
await sc.ensureCreated()
|
|
244
|
-
expect(sc._getPhase()).toBe('streaming')
|
|
245
|
-
expect(sc._getCardId()).toBeNull()
|
|
246
|
-
expect(sc._getMessageId()).toBe('om_fb2')
|
|
247
|
-
})
|
|
248
|
-
|
|
249
|
-
it('降级发送也失败 → aborted + throw', async () => {
|
|
250
|
-
const { client } = makeMockClient({
|
|
251
|
-
'card.create': { code: 99991672 },
|
|
252
|
-
'im.message.create': () => {
|
|
253
|
-
throw new Error('really broken')
|
|
254
|
-
},
|
|
255
|
-
})
|
|
256
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
257
|
-
await expect(sc.ensureCreated()).rejects.toThrow()
|
|
258
|
-
expect(sc._getPhase()).toBe('aborted')
|
|
259
|
-
})
|
|
260
|
-
})
|
|
261
|
-
|
|
262
|
-
// ---------------------------------------------------------------------------
|
|
263
|
-
// appendText + flush
|
|
264
|
-
// ---------------------------------------------------------------------------
|
|
265
|
-
|
|
266
|
-
describe('StreamingCard: appendText + flush', () => {
|
|
267
|
-
it('accumulated 文本写入 cardElement.content,sequence 单调递增', async () => {
|
|
268
|
-
const { client, calls } = makeMockClient({
|
|
269
|
-
'card.create': { code: 0, data: { card_id: 'ck_stream' } },
|
|
270
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
271
|
-
})
|
|
272
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
273
|
-
await sc.ensureCreated()
|
|
274
|
-
|
|
275
|
-
// 第一次 appendText 进入节流窗口(刚 ready,lastUpdateTime 还新)
|
|
276
|
-
sc.appendText('Hello ')
|
|
277
|
-
sc.appendText('world')
|
|
278
|
-
|
|
279
|
-
// 节流窗口 100ms + 余量
|
|
280
|
-
await sleep(150)
|
|
281
|
-
|
|
282
|
-
const contentCalls = calls.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
283
|
-
expect(contentCalls.length).toBeGreaterThan(0)
|
|
284
|
-
// 最后一次 flush 的内容应包含完整累积文本
|
|
285
|
-
const lastCall = contentCalls[contentCalls.length - 1]!
|
|
286
|
-
expect(lastCall.args.data.content).toContain('Hello world')
|
|
287
|
-
expect(lastCall.args.path.element_id).toBe(STREAMING_ELEMENT_ID)
|
|
288
|
-
// sequence 严格单调递增
|
|
289
|
-
const seqs = contentCalls.map((c) => c.args.data.sequence)
|
|
290
|
-
for (let i = 1; i < seqs.length; i++) {
|
|
291
|
-
expect(seqs[i]).toBeGreaterThan(seqs[i - 1]!)
|
|
292
|
-
}
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
it('内容未变化时不重复 flush(基于 lastFlushedText 对比)', async () => {
|
|
296
|
-
const { client, calls } = makeMockClient({
|
|
297
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
298
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
299
|
-
})
|
|
300
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
301
|
-
await sc.ensureCreated()
|
|
302
|
-
|
|
303
|
-
sc.appendText('same')
|
|
304
|
-
await sleep(150)
|
|
305
|
-
|
|
306
|
-
// 强制再跑一次 flush(无新文本)
|
|
307
|
-
await sc._getFlushController().flush()
|
|
308
|
-
|
|
309
|
-
const contentCalls = calls.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
310
|
-
// 应该只有一次 content 调用
|
|
311
|
-
expect(contentCalls.length).toBe(1)
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
it('completed 之后的 appendText 被忽略', async () => {
|
|
315
|
-
const { client } = makeMockClient({
|
|
316
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
317
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
318
|
-
})
|
|
319
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
320
|
-
await sc.ensureCreated()
|
|
321
|
-
await sc.finalize()
|
|
322
|
-
sc.appendText('ignored')
|
|
323
|
-
expect(sc._getAccumulatedText()).toBe('')
|
|
324
|
-
})
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
// ---------------------------------------------------------------------------
|
|
328
|
-
// finalize
|
|
329
|
-
// ---------------------------------------------------------------------------
|
|
330
|
-
|
|
331
|
-
describe('StreamingCard: finalize', () => {
|
|
332
|
-
it('CardKit 路径: settings(false) + card.update,sequence 连续递增', async () => {
|
|
333
|
-
const { client, calls } = makeMockClient({
|
|
334
|
-
'card.create': { code: 0, data: { card_id: 'ck_final' } },
|
|
335
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
336
|
-
})
|
|
337
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
338
|
-
await sc.ensureCreated()
|
|
339
|
-
sc.appendText('# Title\n\nBody')
|
|
340
|
-
await sleep(150)
|
|
341
|
-
const contentSeqs = calls
|
|
342
|
-
.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
343
|
-
.map((c) => c.args.data.sequence)
|
|
344
|
-
const lastContentSeq = contentSeqs[contentSeqs.length - 1] ?? 1
|
|
345
|
-
|
|
346
|
-
await sc.finalize()
|
|
347
|
-
|
|
348
|
-
expect(sc._getPhase()).toBe('completed')
|
|
349
|
-
|
|
350
|
-
const settingsCalls = calls.filter((c) => c.api === 'cardkit.v1.card.settings')
|
|
351
|
-
const updateCalls = calls.filter((c) => c.api === 'cardkit.v1.card.update')
|
|
352
|
-
expect(settingsCalls.length).toBe(1)
|
|
353
|
-
expect(updateCalls.length).toBe(1)
|
|
354
|
-
|
|
355
|
-
const settingsSeq = settingsCalls[0]!.args.data.sequence
|
|
356
|
-
const updateSeq = updateCalls[0]!.args.data.sequence
|
|
357
|
-
expect(settingsSeq).toBeGreaterThan(lastContentSeq)
|
|
358
|
-
expect(updateSeq).toBeGreaterThan(settingsSeq)
|
|
359
|
-
|
|
360
|
-
// settings 关闭 streaming_mode
|
|
361
|
-
const settings = JSON.parse(settingsCalls[0]!.args.data.settings)
|
|
362
|
-
expect(settings.streaming_mode).toBe(false)
|
|
363
|
-
|
|
364
|
-
// update 卡内容是预处理后的 markdown
|
|
365
|
-
const finalCardJson = JSON.parse(updateCalls[0]!.args.data.card.data)
|
|
366
|
-
const finalContent = finalCardJson.body.elements[0].content
|
|
367
|
-
// H1 被降级为 H4
|
|
368
|
-
expect(finalContent).toContain('#### Title')
|
|
369
|
-
expect(finalContent).toContain('Body')
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
it('Fallback 路径: im.message.patch 发完整渲染卡', async () => {
|
|
373
|
-
const { client, calls } = makeMockClient({
|
|
374
|
-
'card.create': { code: 99991672 },
|
|
375
|
-
'im.message.create': { data: { message_id: 'om_fb' } },
|
|
376
|
-
})
|
|
377
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
378
|
-
await sc.ensureCreated()
|
|
379
|
-
sc.appendText('## Heading\n\nContent')
|
|
380
|
-
await sleep(1600) // 等 PATCH_MS 窗口
|
|
381
|
-
await sc.finalize()
|
|
382
|
-
|
|
383
|
-
const patchCalls = calls.filter((c) => c.api === 'im.message.patch')
|
|
384
|
-
expect(patchCalls.length).toBeGreaterThan(0)
|
|
385
|
-
// 最后一次 patch 是 finalize 的(full final card)
|
|
386
|
-
const lastPatch = patchCalls[patchCalls.length - 1]!
|
|
387
|
-
const finalCard = JSON.parse(lastPatch.args.data.content)
|
|
388
|
-
const finalContent = finalCard.body.elements[0].content
|
|
389
|
-
// ## → ##### 降级
|
|
390
|
-
expect(finalContent).toContain('##### Heading')
|
|
391
|
-
})
|
|
392
|
-
|
|
393
|
-
it('完全 idle 时 finalize 直接标记 completed 不抛错', async () => {
|
|
394
|
-
const { client } = makeMockClient()
|
|
395
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
396
|
-
await sc.finalize()
|
|
397
|
-
expect(sc._getPhase()).toBe('completed')
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
it('finalize 只保留 answerText,丢弃 reasoning + toolSteps', async () => {
|
|
401
|
-
const { client, calls } = makeMockClient({
|
|
402
|
-
'card.create': { code: 0, data: { card_id: 'ck_term' } },
|
|
403
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
404
|
-
})
|
|
405
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
406
|
-
await sc.ensureCreated()
|
|
407
|
-
|
|
408
|
-
// 同时塞入三种内容
|
|
409
|
-
sc.appendReasoning('Let me think about this problem carefully...')
|
|
410
|
-
sc.startTool('tu_1', 'Read')
|
|
411
|
-
sc.completeTool('tu_1', 'Read')
|
|
412
|
-
sc.appendText('## 答复\n\n这是最终答复正文。')
|
|
413
|
-
await sleep(150)
|
|
414
|
-
|
|
415
|
-
// 流式中间帧应该包含 reasoning + tools + answer 全套
|
|
416
|
-
const lastMidFrame = calls
|
|
417
|
-
.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
418
|
-
.pop()!.args.data.content as string
|
|
419
|
-
expect(lastMidFrame).toContain('思考中')
|
|
420
|
-
expect(lastMidFrame).toContain('Read')
|
|
421
|
-
expect(lastMidFrame).toContain('最终答复正文')
|
|
422
|
-
|
|
423
|
-
await sc.finalize()
|
|
424
|
-
|
|
425
|
-
// finalize 用的是 card.update,把整张卡换成只有 answer 的版本
|
|
426
|
-
const updateCall = calls.filter((c) => c.api === 'cardkit.v1.card.update').pop()!
|
|
427
|
-
const finalCardJson = JSON.parse(updateCall.args.data.card.data)
|
|
428
|
-
const finalContent = finalCardJson.body.elements[0].content as string
|
|
429
|
-
|
|
430
|
-
expect(finalContent).toContain('最终答复正文')
|
|
431
|
-
// H2 → 降级 H5
|
|
432
|
-
expect(finalContent).toContain('##### 答复')
|
|
433
|
-
// reasoning + tools 都不应该出现在终态
|
|
434
|
-
expect(finalContent).not.toContain('思考中')
|
|
435
|
-
expect(finalContent).not.toContain('think about this problem')
|
|
436
|
-
expect(finalContent).not.toContain('Read')
|
|
437
|
-
expect(finalContent).not.toContain('🛠️')
|
|
438
|
-
expect(finalContent).not.toContain('💭')
|
|
439
|
-
})
|
|
440
|
-
|
|
441
|
-
it('finalize 边界: 没有 answerText 时退到组合渲染(保留推理)', async () => {
|
|
442
|
-
const { client, calls } = makeMockClient({
|
|
443
|
-
'card.create': { code: 0, data: { card_id: 'ck_no_answer' } },
|
|
444
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
445
|
-
})
|
|
446
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
447
|
-
await sc.ensureCreated()
|
|
448
|
-
|
|
449
|
-
// 只有推理,没有 appendText —— 异常 case 但要可控降级
|
|
450
|
-
sc.appendReasoning('I was thinking but never produced an answer.')
|
|
451
|
-
await sleep(150)
|
|
452
|
-
|
|
453
|
-
await sc.finalize()
|
|
454
|
-
const updateCall = calls.filter((c) => c.api === 'cardkit.v1.card.update').pop()!
|
|
455
|
-
const finalContent = JSON.parse(updateCall.args.data.card.data).body.elements[0].content as string
|
|
456
|
-
// 至少能看到推理内容
|
|
457
|
-
expect(finalContent).toContain('thinking')
|
|
458
|
-
})
|
|
459
|
-
|
|
460
|
-
it('finalize 失败不抛出', async () => {
|
|
461
|
-
const { client } = makeMockClient({
|
|
462
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
463
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
464
|
-
'card.settings': () => {
|
|
465
|
-
throw new Error('settings exploded')
|
|
466
|
-
},
|
|
467
|
-
})
|
|
468
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
469
|
-
await sc.ensureCreated()
|
|
470
|
-
sc.appendText('text')
|
|
471
|
-
await sleep(150)
|
|
472
|
-
// finalize 内部捕获错误不 rethrow
|
|
473
|
-
await sc.finalize()
|
|
474
|
-
expect(sc._getPhase()).toBe('completed')
|
|
475
|
-
})
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
// ---------------------------------------------------------------------------
|
|
479
|
-
// Rate limit + table limit
|
|
480
|
-
// ---------------------------------------------------------------------------
|
|
481
|
-
|
|
482
|
-
describe('StreamingCard: 错误处理', () => {
|
|
483
|
-
it('230020 rate limit → 跳帧,后续 flush 继续', async () => {
|
|
484
|
-
let callIdx = 0
|
|
485
|
-
const { client, calls } = makeMockClient({
|
|
486
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
487
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
488
|
-
'cardElement.content': () => {
|
|
489
|
-
const i = callIdx++
|
|
490
|
-
if (i === 0) {
|
|
491
|
-
const err: any = new Error('rate limit')
|
|
492
|
-
err.code = 230020
|
|
493
|
-
throw err
|
|
494
|
-
}
|
|
495
|
-
return { code: 0 }
|
|
496
|
-
},
|
|
497
|
-
})
|
|
498
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
499
|
-
await sc.ensureCreated()
|
|
500
|
-
sc.appendText('first')
|
|
501
|
-
await sleep(150)
|
|
502
|
-
// 第一次被限流
|
|
503
|
-
sc.appendText(' second')
|
|
504
|
-
await sleep(150)
|
|
505
|
-
// 第二次应能成功
|
|
506
|
-
|
|
507
|
-
// CardKit 仍然 active(没降级)
|
|
508
|
-
expect(sc._isCardKitStreamActive()).toBe(true)
|
|
509
|
-
const contentCalls = calls.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
510
|
-
expect(contentCalls.length).toBeGreaterThanOrEqual(2)
|
|
511
|
-
})
|
|
512
|
-
|
|
513
|
-
it('230099 table limit → 禁用流式但 cardId 保留,finalize 仍走 CardKit', async () => {
|
|
514
|
-
const { client, calls } = makeMockClient({
|
|
515
|
-
'card.create': { code: 0, data: { card_id: 'ck_tbl' } },
|
|
516
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
517
|
-
'cardElement.content': () => {
|
|
518
|
-
const err: any = new Error('content failed')
|
|
519
|
-
err.code = 230099
|
|
520
|
-
err.msg = 'Failed to create card content, ext=ErrCode: 11310; ErrMsg: card table number over limit; '
|
|
521
|
-
throw err
|
|
522
|
-
},
|
|
523
|
-
})
|
|
524
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
525
|
-
await sc.ensureCreated()
|
|
526
|
-
sc.appendText('some content')
|
|
527
|
-
await sleep(150)
|
|
528
|
-
expect(sc._isCardKitStreamActive()).toBe(false)
|
|
529
|
-
expect(sc._getCardId()).toBe('ck_tbl') // card_id 保留
|
|
530
|
-
|
|
531
|
-
await sc.finalize()
|
|
532
|
-
// finalize 仍然走 CardKit 的 settings + update(cardId 还在)
|
|
533
|
-
expect(calls.some((c) => c.api === 'cardkit.v1.card.settings')).toBe(true)
|
|
534
|
-
expect(calls.some((c) => c.api === 'cardkit.v1.card.update')).toBe(true)
|
|
535
|
-
// 不走 patch
|
|
536
|
-
expect(calls.some((c) => c.api === 'im.message.patch')).toBe(false)
|
|
537
|
-
})
|
|
538
|
-
})
|
|
539
|
-
|
|
540
|
-
// ---------------------------------------------------------------------------
|
|
541
|
-
// abort
|
|
542
|
-
// ---------------------------------------------------------------------------
|
|
543
|
-
|
|
544
|
-
describe('StreamingCard: abort', () => {
|
|
545
|
-
it('CardKit 路径: 渲染错误卡并关闭流式', async () => {
|
|
546
|
-
const { client, calls } = makeMockClient({
|
|
547
|
-
'card.create': { code: 0, data: { card_id: 'ck_err' } },
|
|
548
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
549
|
-
})
|
|
550
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
551
|
-
await sc.ensureCreated()
|
|
552
|
-
sc.appendText('partial...')
|
|
553
|
-
await sleep(150)
|
|
554
|
-
|
|
555
|
-
await sc.abort(new Error('something went wrong'))
|
|
556
|
-
expect(sc._getPhase()).toBe('aborted')
|
|
557
|
-
|
|
558
|
-
const updateCalls = calls.filter((c) => c.api === 'cardkit.v1.card.update')
|
|
559
|
-
expect(updateCalls.length).toBeGreaterThan(0)
|
|
560
|
-
const errCard = JSON.parse(updateCalls[updateCalls.length - 1]!.args.data.card.data)
|
|
561
|
-
expect(errCard.header.template).toBe('red')
|
|
562
|
-
expect(errCard.body.elements[0].content).toContain('something went wrong')
|
|
563
|
-
// 保留已累积的部分文本
|
|
564
|
-
expect(errCard.body.elements[0].content).toContain('partial...')
|
|
565
|
-
})
|
|
566
|
-
|
|
567
|
-
it('idle 阶段 abort 不抛错', async () => {
|
|
568
|
-
const { client } = makeMockClient()
|
|
569
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
570
|
-
await sc.abort(new Error('before any card'))
|
|
571
|
-
expect(sc._getPhase()).toBe('aborted')
|
|
572
|
-
})
|
|
573
|
-
})
|
|
574
|
-
|
|
575
|
-
// ---------------------------------------------------------------------------
|
|
576
|
-
// Reasoning / tool use rendering
|
|
577
|
-
// ---------------------------------------------------------------------------
|
|
578
|
-
|
|
579
|
-
describe('StreamingCard: appendReasoning', () => {
|
|
580
|
-
it('累积 thinking delta 并渲染在卡片中(plain markdown,不用 blockquote)', async () => {
|
|
581
|
-
const { client, calls } = makeMockClient({
|
|
582
|
-
'card.create': { code: 0, data: { card_id: 'ck_think' } },
|
|
583
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
584
|
-
})
|
|
585
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
586
|
-
await sc.ensureCreated()
|
|
587
|
-
|
|
588
|
-
sc.appendReasoning('Analyzing the problem. ')
|
|
589
|
-
sc.appendReasoning('Let me check file A.')
|
|
590
|
-
await sleep(150)
|
|
591
|
-
|
|
592
|
-
const contentCalls = calls.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
593
|
-
expect(contentCalls.length).toBeGreaterThan(0)
|
|
594
|
-
const last = contentCalls[contentCalls.length - 1]!
|
|
595
|
-
expect(last.args.data.content).toContain('💭')
|
|
596
|
-
expect(last.args.data.content).toContain('思考中')
|
|
597
|
-
expect(last.args.data.content).toContain('Analyzing the problem.')
|
|
598
|
-
expect(last.args.data.content).toContain('Let me check file A.')
|
|
599
|
-
// 没有 blockquote `>` 前缀 —— 这是新格式的关键
|
|
600
|
-
expect(last.args.data.content).not.toContain('> Analyzing')
|
|
601
|
-
// 没有 appendText → 不应有普通正文
|
|
602
|
-
expect(sc._getAccumulatedReasoning()).toContain('Analyzing')
|
|
603
|
-
expect(sc._getAccumulatedText()).toBe('')
|
|
604
|
-
})
|
|
605
|
-
|
|
606
|
-
it('completed 之后 appendReasoning 被忽略', async () => {
|
|
607
|
-
const { client } = makeMockClient({
|
|
608
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
609
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
610
|
-
})
|
|
611
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
612
|
-
await sc.ensureCreated()
|
|
613
|
-
await sc.finalize()
|
|
614
|
-
sc.appendReasoning('too late')
|
|
615
|
-
expect(sc._getAccumulatedReasoning()).toBe('')
|
|
616
|
-
})
|
|
617
|
-
})
|
|
618
|
-
|
|
619
|
-
describe('StreamingCard: startTool / completeTool', () => {
|
|
620
|
-
it('startTool 压入 running 步骤,completeTool 翻到 done', async () => {
|
|
621
|
-
const { client, calls } = makeMockClient({
|
|
622
|
-
'card.create': { code: 0, data: { card_id: 'ck_tool' } },
|
|
623
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
624
|
-
})
|
|
625
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
626
|
-
await sc.ensureCreated()
|
|
627
|
-
|
|
628
|
-
sc.startTool('tu_1', 'Read')
|
|
629
|
-
await sleep(150)
|
|
630
|
-
let steps = sc._getToolSteps()
|
|
631
|
-
expect(steps.length).toBe(1)
|
|
632
|
-
expect(steps[0]!.name).toBe('Read')
|
|
633
|
-
expect(steps[0]!.status).toBe('running')
|
|
634
|
-
|
|
635
|
-
// 卡片也应显示 "🛠️ ⚙️ Read"(inline 形式)
|
|
636
|
-
const runningContent = calls
|
|
637
|
-
.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
638
|
-
.map((c) => c.args.data.content)
|
|
639
|
-
.join('\n')
|
|
640
|
-
expect(runningContent).toContain('⚙️')
|
|
641
|
-
expect(runningContent).toContain('Read')
|
|
642
|
-
expect(runningContent).toContain('🛠️')
|
|
643
|
-
|
|
644
|
-
sc.completeTool('tu_1', 'Read')
|
|
645
|
-
await sleep(150)
|
|
646
|
-
steps = sc._getToolSteps()
|
|
647
|
-
expect(steps[0]!.status).toBe('done')
|
|
648
|
-
|
|
649
|
-
// 最新 flush 应显示 "✅ Read" 不再有 "⚙️"
|
|
650
|
-
const lastContent = calls
|
|
651
|
-
.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
652
|
-
.pop()!.args.data.content as string
|
|
653
|
-
expect(lastContent).toContain('✅')
|
|
654
|
-
expect(lastContent).toContain('Read')
|
|
655
|
-
// 这一行整体换成了 `✅ Read`,不该再出现 ⚙️ 图标
|
|
656
|
-
expect(lastContent).not.toContain('⚙️')
|
|
657
|
-
})
|
|
658
|
-
|
|
659
|
-
it('按 toolUseId 去重: 同一 id 不重复压入', async () => {
|
|
660
|
-
const { client } = makeMockClient({
|
|
661
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
662
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
663
|
-
})
|
|
664
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
665
|
-
await sc.ensureCreated()
|
|
666
|
-
|
|
667
|
-
sc.startTool('tu_1', 'Read')
|
|
668
|
-
sc.startTool('tu_1', 'Read')
|
|
669
|
-
sc.startTool('tu_1', 'Read')
|
|
670
|
-
expect(sc._getToolSteps().length).toBe(1)
|
|
671
|
-
})
|
|
672
|
-
|
|
673
|
-
it('缺省 toolUseId 时按 name + index 合成 id,不同步骤可并存', async () => {
|
|
674
|
-
const { client } = makeMockClient({
|
|
675
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
676
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
677
|
-
})
|
|
678
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
679
|
-
await sc.ensureCreated()
|
|
680
|
-
|
|
681
|
-
sc.startTool(undefined, 'Read')
|
|
682
|
-
sc.startTool(undefined, 'Read')
|
|
683
|
-
// 合成 id 不同 → 两个独立步骤
|
|
684
|
-
expect(sc._getToolSteps().length).toBe(2)
|
|
685
|
-
})
|
|
686
|
-
|
|
687
|
-
it('completeTool 只匹配最近的 running 同名步骤', async () => {
|
|
688
|
-
const { client } = makeMockClient({
|
|
689
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
690
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
691
|
-
})
|
|
692
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
693
|
-
await sc.ensureCreated()
|
|
694
|
-
|
|
695
|
-
sc.startTool('tu_1', 'Bash')
|
|
696
|
-
sc.startTool('tu_2', 'Bash')
|
|
697
|
-
sc.completeTool(undefined, 'Bash')
|
|
698
|
-
const steps = sc._getToolSteps()
|
|
699
|
-
// 更晚的 tu_2 被标记 done
|
|
700
|
-
expect(steps[0]!.status).toBe('running')
|
|
701
|
-
expect(steps[1]!.status).toBe('done')
|
|
702
|
-
})
|
|
703
|
-
|
|
704
|
-
it('空 toolName 忽略', async () => {
|
|
705
|
-
const { client } = makeMockClient({
|
|
706
|
-
'card.create': { code: 0, data: { card_id: 'ck' } },
|
|
707
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
708
|
-
})
|
|
709
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
710
|
-
await sc.ensureCreated()
|
|
711
|
-
|
|
712
|
-
sc.startTool('tu_1', undefined)
|
|
713
|
-
sc.startTool('tu_1', '')
|
|
714
|
-
expect(sc._getToolSteps().length).toBe(0)
|
|
715
|
-
})
|
|
716
|
-
})
|
|
717
|
-
|
|
718
|
-
// 复刻用户的真实场景: 用户发消息 → 服务端 thinking → tool_use → 最终 text。
|
|
719
|
-
// 验证每个阶段都向 cardElement.content 写入了对应内容(不被 throttle / phase
|
|
720
|
-
// gate / 等任何东西吃掉)。
|
|
721
|
-
describe('StreamingCard: 真实事件流(用户场景回归)', () => {
|
|
722
|
-
it('thinking → tool_use → text 应该在每个阶段都触发可见的 flush', async () => {
|
|
723
|
-
const { client, calls } = makeMockClient({
|
|
724
|
-
'card.create': { code: 0, data: { card_id: 'ck_real' } },
|
|
725
|
-
'im.message.create': { data: { message_id: 'om_real' } },
|
|
726
|
-
})
|
|
727
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'oc_real' })
|
|
728
|
-
|
|
729
|
-
// 1. 用户发消息 → handleMessage 预建卡(fire-and-forget)
|
|
730
|
-
const creating = sc.ensureCreated()
|
|
731
|
-
await creating // 等卡可写
|
|
732
|
-
|
|
733
|
-
// 2. 服务端: status streaming + content_start{text} (thinking block)
|
|
734
|
-
// feishu/index.ts 的 content_start text 分支会再 await ensureCreated(no-op)
|
|
735
|
-
// (no direct call here — 等同于 no-op)
|
|
736
|
-
|
|
737
|
-
// 3. 服务端: thinking deltas(5 个增量,间隔 30ms 模拟流式)
|
|
738
|
-
sc.appendReasoning('Analyzing the latest commits to find ')
|
|
739
|
-
await sleep(30)
|
|
740
|
-
sc.appendReasoning('breaking changes. Need to look at ')
|
|
741
|
-
await sleep(30)
|
|
742
|
-
sc.appendReasoning('the public API surface, the schema files, ')
|
|
743
|
-
await sleep(30)
|
|
744
|
-
sc.appendReasoning('and any removed exports. Let me check the ')
|
|
745
|
-
await sleep(30)
|
|
746
|
-
sc.appendReasoning('git log first.')
|
|
747
|
-
|
|
748
|
-
// 等节流窗口结束
|
|
749
|
-
await sleep(200)
|
|
750
|
-
|
|
751
|
-
const flushesAfterReasoning = calls.filter((c) => c.api === 'cardkit.v1.cardElement.content').length
|
|
752
|
-
expect(flushesAfterReasoning).toBeGreaterThan(0)
|
|
753
|
-
|
|
754
|
-
const lastReasoningContent = calls
|
|
755
|
-
.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
756
|
-
.pop()!.args.data.content as string
|
|
757
|
-
// 应该包含 reasoning 累积内容
|
|
758
|
-
expect(lastReasoningContent).toContain('breaking changes')
|
|
759
|
-
expect(lastReasoningContent).toContain('git log first')
|
|
760
|
-
|
|
761
|
-
// 4. 服务端: content_start{tool_use, name: 'Bash'}
|
|
762
|
-
sc.startTool('tu_bash_1', 'Bash')
|
|
763
|
-
await sleep(150)
|
|
764
|
-
|
|
765
|
-
const lastWithTool = calls
|
|
766
|
-
.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
767
|
-
.pop()!.args.data.content as string
|
|
768
|
-
expect(lastWithTool).toContain('Bash')
|
|
769
|
-
expect(lastWithTool).toContain('⚙️')
|
|
770
|
-
expect(lastWithTool).toContain('🛠️')
|
|
771
|
-
|
|
772
|
-
// 5. 服务端: tool_use_complete
|
|
773
|
-
sc.completeTool('tu_bash_1', 'Bash')
|
|
774
|
-
await sleep(150)
|
|
775
|
-
|
|
776
|
-
const lastAfterToolDone = calls
|
|
777
|
-
.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
778
|
-
.pop()!.args.data.content as string
|
|
779
|
-
expect(lastAfterToolDone).toContain('Bash')
|
|
780
|
-
// ⚙️ 切到 ✅ —— 当前唯一一步已完成
|
|
781
|
-
expect(lastAfterToolDone).toContain('✅')
|
|
782
|
-
expect(lastAfterToolDone).not.toContain('⚙️')
|
|
783
|
-
|
|
784
|
-
// 6. 第二个 tool 序列
|
|
785
|
-
sc.startTool('tu_read_1', 'Read')
|
|
786
|
-
await sleep(150)
|
|
787
|
-
sc.completeTool('tu_read_1', 'Read')
|
|
788
|
-
await sleep(150)
|
|
789
|
-
|
|
790
|
-
// 7. 最终 text 输出
|
|
791
|
-
sc.appendText('## 破坏性变更分析\n\n')
|
|
792
|
-
await sleep(120)
|
|
793
|
-
sc.appendText('1. **API 重命名**: foo → bar\n')
|
|
794
|
-
await sleep(120)
|
|
795
|
-
sc.appendText('2. **删除导出**: baz')
|
|
796
|
-
await sleep(200)
|
|
797
|
-
|
|
798
|
-
const lastWithText = calls
|
|
799
|
-
.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
800
|
-
.pop()!.args.data.content as string
|
|
801
|
-
// 应该同时包含 reasoning, tools, answer
|
|
802
|
-
expect(lastWithText).toContain('git log first') // reasoning
|
|
803
|
-
expect(lastWithText).toContain('Bash') // tool
|
|
804
|
-
expect(lastWithText).toContain('Read') // tool
|
|
805
|
-
expect(lastWithText).toContain('破坏性变更分析') // answer (post optimize: H2→H5)
|
|
806
|
-
expect(lastWithText).toContain('API 重命名')
|
|
807
|
-
|
|
808
|
-
// 8. message_complete → finalize
|
|
809
|
-
await sc.finalize()
|
|
810
|
-
expect(sc._getPhase()).toBe('completed')
|
|
811
|
-
|
|
812
|
-
// 验证有 settings + update 收尾
|
|
813
|
-
expect(calls.some((c) => c.api === 'cardkit.v1.card.settings')).toBe(true)
|
|
814
|
-
expect(calls.some((c) => c.api === 'cardkit.v1.card.update')).toBe(true)
|
|
815
|
-
})
|
|
816
|
-
|
|
817
|
-
it('cardKit 流式中第一帧失败不应永久禁用流式 —— 后续帧应能继续', async () => {
|
|
818
|
-
let firstFrameRejected = false
|
|
819
|
-
const { client, calls } = makeMockClient({
|
|
820
|
-
'card.create': { code: 0, data: { card_id: 'ck_recover' } },
|
|
821
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
822
|
-
'cardElement.content': () => {
|
|
823
|
-
if (!firstFrameRejected) {
|
|
824
|
-
firstFrameRejected = true
|
|
825
|
-
// 模拟一个 *非* rate-limit、*非* table-limit 错误
|
|
826
|
-
// 当前实现会把 cardKitStreamActive 设 false,本测试就是要发现这个问题
|
|
827
|
-
const err: any = new Error('mystery cardkit error')
|
|
828
|
-
err.code = 999999
|
|
829
|
-
throw err
|
|
830
|
-
}
|
|
831
|
-
return { code: 0 }
|
|
832
|
-
},
|
|
833
|
-
})
|
|
834
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
835
|
-
await sc.ensureCreated()
|
|
836
|
-
|
|
837
|
-
sc.appendReasoning('first thought')
|
|
838
|
-
await sleep(150)
|
|
839
|
-
// 此时第一帧已被拒,但我们期望流式仍然开着 —— 这样第二帧能继续
|
|
840
|
-
sc.appendReasoning(' second thought')
|
|
841
|
-
await sleep(150)
|
|
842
|
-
// 验证: 至少尝试了 2 次 cardElement.content 调用
|
|
843
|
-
const contentCalls = calls.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
844
|
-
expect(contentCalls.length).toBeGreaterThanOrEqual(2)
|
|
845
|
-
// 而且 streaming 仍是 active
|
|
846
|
-
expect(sc._isCardKitStreamActive()).toBe(true)
|
|
847
|
-
})
|
|
848
|
-
})
|
|
849
|
-
|
|
850
|
-
describe('StreamingCard: 组合渲染 (tools + reasoning + text)', () => {
|
|
851
|
-
it('三个 section 按顺序 tools → reasoning → answer 组合', async () => {
|
|
852
|
-
const { client, calls } = makeMockClient({
|
|
853
|
-
'card.create': { code: 0, data: { card_id: 'ck_all' } },
|
|
854
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
855
|
-
})
|
|
856
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
857
|
-
await sc.ensureCreated()
|
|
858
|
-
|
|
859
|
-
sc.appendReasoning('Should I read file A first?')
|
|
860
|
-
sc.startTool('tu_1', 'Read')
|
|
861
|
-
sc.appendText('Here is the answer.')
|
|
862
|
-
await sleep(150)
|
|
863
|
-
|
|
864
|
-
const lastContent = calls
|
|
865
|
-
.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
866
|
-
.pop()!.args.data.content as string
|
|
867
|
-
|
|
868
|
-
const idxTools = lastContent.indexOf('🛠️')
|
|
869
|
-
const idxReasoning = lastContent.indexOf('思考中')
|
|
870
|
-
const idxAnswer = lastContent.indexOf('Here is the answer')
|
|
871
|
-
|
|
872
|
-
expect(idxTools).toBeGreaterThan(-1)
|
|
873
|
-
expect(idxReasoning).toBeGreaterThan(-1)
|
|
874
|
-
expect(idxAnswer).toBeGreaterThan(-1)
|
|
875
|
-
// tools 在最顶部 → reasoning 居中 → answer 在底部
|
|
876
|
-
expect(idxTools).toBeLessThan(idxReasoning)
|
|
877
|
-
expect(idxReasoning).toBeLessThan(idxAnswer)
|
|
878
|
-
})
|
|
879
|
-
|
|
880
|
-
it('ensureCreated 期间到达的 tool_use 在卡可写后立即 flush', async () => {
|
|
881
|
-
let resolveCreate: (() => void) | null = null
|
|
882
|
-
const createLatch = new Promise<void>((r) => { resolveCreate = r })
|
|
883
|
-
|
|
884
|
-
const { client, calls } = makeMockClient({
|
|
885
|
-
'card.create': async () => {
|
|
886
|
-
await createLatch
|
|
887
|
-
return { code: 0, data: { card_id: 'ck_slow' } }
|
|
888
|
-
},
|
|
889
|
-
'im.message.create': { data: { message_id: 'om' } },
|
|
890
|
-
})
|
|
891
|
-
const sc = new StreamingCard({ larkClient: client, chatId: 'c' })
|
|
892
|
-
|
|
893
|
-
// 不 await: 在 create 还没 resolve 之前,先压入一个 tool step
|
|
894
|
-
const creating = sc.ensureCreated()
|
|
895
|
-
// 让事件循环推进到 create 被 await
|
|
896
|
-
await sleep(10)
|
|
897
|
-
sc.startTool('tu_1', 'Glob')
|
|
898
|
-
|
|
899
|
-
// 此时 cardMessageReady 仍是 false —— 没有任何 flush
|
|
900
|
-
const contentBefore = calls.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
901
|
-
expect(contentBefore.length).toBe(0)
|
|
902
|
-
|
|
903
|
-
// 解锁 create → ensureCreated 继续 → setCardMessageReady(true) → 触发 pending flush
|
|
904
|
-
resolveCreate!()
|
|
905
|
-
await creating
|
|
906
|
-
await sleep(150)
|
|
907
|
-
|
|
908
|
-
const contentAfter = calls.filter((c) => c.api === 'cardkit.v1.cardElement.content')
|
|
909
|
-
expect(contentAfter.length).toBeGreaterThan(0)
|
|
910
|
-
const last = contentAfter[contentAfter.length - 1]!
|
|
911
|
-
expect(last.args.data.content).toContain('Glob')
|
|
912
|
-
expect(last.args.data.content).toContain('🛠️')
|
|
913
|
-
})
|
|
914
|
-
})
|