bingocode 1.0.41 → 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 +11 -4
  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,290 +0,0 @@
1
- /**
2
- * FlushController 单元测试
3
- *
4
- * 覆盖:
5
- * - 基础: cardMessageReady gate, complete() 锁死
6
- * - 节流窗口: 立即 flush / 延迟 flush
7
- * - Mutex: 进行中的 flush 重复调用标记 needsReflush
8
- * - Conflict reflush: API 结束后自动补一次
9
- * - 长间隔批量: elapsed > 2000ms 后延迟 300ms 再 flush
10
- * - waitForFlush: 等当前 flush 结束
11
- */
12
-
13
- import { describe, it, expect } from 'bun:test'
14
- import { FlushController, THROTTLE } from '../flush-controller.js'
15
-
16
- // 创建一个可控的 doFlush —— 返回一个 Promise 可以手动 resolve
17
- function makeControllableFlush() {
18
- const calls: string[] = []
19
- let resolveCurrent: (() => void) | null = null
20
- let latch: Promise<void> | null = null
21
-
22
- const doFlush = async () => {
23
- calls.push('flush-start')
24
- if (latch) {
25
- await latch
26
- latch = null
27
- }
28
- calls.push('flush-end')
29
- }
30
-
31
- const blockNext = () => {
32
- latch = new Promise<void>((resolve) => {
33
- resolveCurrent = resolve
34
- })
35
- }
36
-
37
- const unblock = () => {
38
- if (resolveCurrent) {
39
- const r = resolveCurrent
40
- resolveCurrent = null
41
- r()
42
- }
43
- }
44
-
45
- return { doFlush, calls, blockNext, unblock }
46
- }
47
-
48
- async function sleep(ms: number): Promise<void> {
49
- await new Promise((r) => setTimeout(r, ms))
50
- }
51
-
52
- // ---------------------------------------------------------------------------
53
- // Basic gating
54
- // ---------------------------------------------------------------------------
55
-
56
- describe('FlushController: cardMessageReady gate', () => {
57
- it('在 cardMessageReady=false 时不 flush', async () => {
58
- let count = 0
59
- const fc = new FlushController(async () => {
60
- count += 1
61
- })
62
- await fc.flush()
63
- await fc.throttledUpdate(50)
64
- expect(count).toBe(0)
65
- })
66
-
67
- it('setCardMessageReady(true) 后 flush 可执行', async () => {
68
- let count = 0
69
- const fc = new FlushController(async () => {
70
- count += 1
71
- })
72
- fc.setCardMessageReady(true)
73
- await fc.flush()
74
- expect(count).toBe(1)
75
- })
76
-
77
- it('setCardMessageReady(true) 同步初始化 lastUpdateTime —— 刚 ready 时 throttledUpdate 被节流窗口阻挡', async () => {
78
- let count = 0
79
- const fc = new FlushController(async () => {
80
- count += 1
81
- })
82
- fc.setCardMessageReady(true)
83
- // 立即调用 throttledUpdate 500ms 窗口,首次 elapsed≈0 → 进入延迟分支
84
- await fc.throttledUpdate(500)
85
- // 同步阶段还没到 500ms,不应触发 flush
86
- expect(count).toBe(0)
87
- // 500+ms 后延迟 timer 触发
88
- await sleep(600)
89
- expect(count).toBe(1)
90
- })
91
- })
92
-
93
- describe('FlushController: complete()', () => {
94
- it('complete() 后拒绝新 flush', async () => {
95
- let count = 0
96
- const fc = new FlushController(async () => {
97
- count += 1
98
- })
99
- fc.setCardMessageReady(true)
100
- fc.complete()
101
- await fc.flush()
102
- await fc.throttledUpdate(50)
103
- expect(count).toBe(0)
104
- })
105
- })
106
-
107
- // ---------------------------------------------------------------------------
108
- // Throttle window
109
- // ---------------------------------------------------------------------------
110
-
111
- describe('FlushController: 节流窗口', () => {
112
- it('超过窗口立即 flush', async () => {
113
- let count = 0
114
- const fc = new FlushController(async () => {
115
- count += 1
116
- })
117
- fc.setCardMessageReady(true)
118
- // 手动把 lastUpdateTime 挪远(等同于已过了节流窗口)
119
- await sleep(150)
120
- await fc.throttledUpdate(100)
121
- expect(count).toBe(1)
122
- })
123
-
124
- it('在窗口内首次调用安排延迟 flush', async () => {
125
- let count = 0
126
- const fc = new FlushController(async () => {
127
- count += 1
128
- })
129
- fc.setCardMessageReady(true) // lastUpdateTime = now
130
-
131
- await fc.throttledUpdate(200)
132
- expect(count).toBe(0) // 延迟中,还没触发
133
-
134
- await sleep(300)
135
- expect(count).toBe(1) // 200ms 后延迟 timer 触发
136
- })
137
-
138
- it('窗口内多次调用复用同一个延迟 timer(不重复 flush)', async () => {
139
- let count = 0
140
- const fc = new FlushController(async () => {
141
- count += 1
142
- })
143
- fc.setCardMessageReady(true)
144
-
145
- await fc.throttledUpdate(200)
146
- await fc.throttledUpdate(200)
147
- await fc.throttledUpdate(200)
148
- await fc.throttledUpdate(200)
149
-
150
- await sleep(300)
151
- expect(count).toBe(1)
152
- })
153
- })
154
-
155
- // ---------------------------------------------------------------------------
156
- // Mutex + conflict reflush
157
- // ---------------------------------------------------------------------------
158
-
159
- describe('FlushController: mutex + 冲突重刷', () => {
160
- it('flush 进行中的重复调用不并发执行', async () => {
161
- const { doFlush, calls, blockNext, unblock } = makeControllableFlush()
162
- const fc = new FlushController(doFlush)
163
- fc.setCardMessageReady(true)
164
-
165
- blockNext()
166
- const p1 = fc.flush() // flush-start 后被 latch 卡住
167
- // 让事件循环走一轮,确保第一次 flush 进入 body
168
- await sleep(10)
169
- // 第二次调用时第一次还没结束 —— 应被 mutex 挡住
170
- const p2 = fc.flush()
171
-
172
- // 两次 Promise 都已登记,但都还没 end
173
- expect(calls).toEqual(['flush-start'])
174
-
175
- unblock()
176
- await p1
177
- await p2
178
- // 第一次跑完后,由于 needsReflush 被标记,会触发一次补刷
179
- // (conflict reflush 是通过 setTimeout 0 调度的,需要让它跑完)
180
- await sleep(20)
181
-
182
- // 第一次 flush-start + flush-end,然后冲突补刷再一次 start + end
183
- expect(calls).toEqual([
184
- 'flush-start', 'flush-end',
185
- 'flush-start', 'flush-end',
186
- ])
187
- })
188
-
189
- it('flush 进行中的 throttledUpdate 也会触发补刷', async () => {
190
- const { doFlush, calls, blockNext, unblock } = makeControllableFlush()
191
- const fc = new FlushController(doFlush)
192
- fc.setCardMessageReady(true)
193
-
194
- blockNext()
195
- const p1 = fc.flush()
196
- await sleep(10)
197
-
198
- // API 进行中收到新的 update 请求
199
- await fc.throttledUpdate(10)
200
-
201
- unblock()
202
- await p1
203
- await sleep(30)
204
-
205
- // 第一次 flush + 冲突补刷
206
- expect(calls.filter((c) => c === 'flush-end').length).toBe(2)
207
- })
208
- })
209
-
210
- // ---------------------------------------------------------------------------
211
- // Long gap batching
212
- // ---------------------------------------------------------------------------
213
-
214
- describe('FlushController: 长间隔批量', () => {
215
- it('elapsed > LONG_GAP_THRESHOLD_MS 时延迟 BATCH_AFTER_GAP_MS 再 flush', async () => {
216
- let count = 0
217
- let flushAtMs = 0
218
- const start = Date.now()
219
- const fc = new FlushController(async () => {
220
- count += 1
221
- flushAtMs = Date.now() - start
222
- })
223
- fc.setCardMessageReady(true)
224
-
225
- // 等到 elapsed > 2000ms
226
- await sleep(THROTTLE.LONG_GAP_THRESHOLD_MS + 50)
227
- const callAt = Date.now() - start
228
-
229
- await fc.throttledUpdate(THROTTLE.CARDKIT_MS)
230
- // throttledUpdate 同步阶段不应立即 flush(因为走批量分支)
231
- expect(count).toBe(0)
232
-
233
- // 等 BATCH_AFTER_GAP_MS + 余量
234
- await sleep(THROTTLE.BATCH_AFTER_GAP_MS + 50)
235
- expect(count).toBe(1)
236
- // 实际 flush 时刻至少比 throttledUpdate 调用晚 300ms
237
- expect(flushAtMs - callAt).toBeGreaterThanOrEqual(THROTTLE.BATCH_AFTER_GAP_MS - 20)
238
- })
239
- })
240
-
241
- // ---------------------------------------------------------------------------
242
- // waitForFlush
243
- // ---------------------------------------------------------------------------
244
-
245
- describe('FlushController: waitForFlush', () => {
246
- it('没在 flush 时立即返回', async () => {
247
- const fc = new FlushController(async () => {})
248
- fc.setCardMessageReady(true)
249
- const start = Date.now()
250
- await fc.waitForFlush()
251
- expect(Date.now() - start).toBeLessThan(10)
252
- })
253
-
254
- it('有 flush 在跑时等它结束', async () => {
255
- const { doFlush, blockNext, unblock } = makeControllableFlush()
256
- const fc = new FlushController(doFlush)
257
- fc.setCardMessageReady(true)
258
-
259
- blockNext()
260
- const p1 = fc.flush()
261
- await sleep(10)
262
-
263
- let resolved = false
264
- const waiter = fc.waitForFlush().then(() => {
265
- resolved = true
266
- })
267
- await sleep(20)
268
- expect(resolved).toBe(false)
269
-
270
- unblock()
271
- await p1
272
- await waiter
273
- expect(resolved).toBe(true)
274
- })
275
- })
276
-
277
- // ---------------------------------------------------------------------------
278
- // 常量合理性
279
- // ---------------------------------------------------------------------------
280
-
281
- describe('FlushController: THROTTLE 常量', () => {
282
- it('CARDKIT_MS=100, PATCH_MS=1500', () => {
283
- expect(THROTTLE.CARDKIT_MS).toBe(100)
284
- expect(THROTTLE.PATCH_MS).toBe(1500)
285
- })
286
- it('LONG_GAP_THRESHOLD_MS=2000, BATCH_AFTER_GAP_MS=300', () => {
287
- expect(THROTTLE.LONG_GAP_THRESHOLD_MS).toBe(2000)
288
- expect(THROTTLE.BATCH_AFTER_GAP_MS).toBe(300)
289
- })
290
- })
@@ -1,353 +0,0 @@
1
- /**
2
- * markdown-style 单元测试
3
- *
4
- * 覆盖:
5
- * - 标题降级 (H1~H3 workaround)
6
- * - 代码块保护
7
- * - 空行压缩
8
- * - Schema 2.0: 连续标题/表格/代码块 <br> 间距
9
- * - stripInvalidImageKeys
10
- * - sanitizeTextForCard (表格数限制)
11
- * - findMarkdownTablesOutsideCodeBlocks
12
- */
13
-
14
- import { describe, it, expect } from 'bun:test'
15
- import {
16
- optimizeMarkdownForFeishu,
17
- sanitizeTextForCard,
18
- findMarkdownTablesOutsideCodeBlocks,
19
- FEISHU_CARD_TABLE_LIMIT,
20
- } from '../markdown-style.js'
21
-
22
- // 默认 cardVersion=2 的 shortcut
23
- const opt = (text: string, v?: number) => optimizeMarkdownForFeishu(text, v)
24
-
25
- // ---------------------------------------------------------------------------
26
- // 标题降级
27
- // ---------------------------------------------------------------------------
28
-
29
- describe('optimizeMarkdownForFeishu: 标题降级', () => {
30
- it('H1 → H4 (cardVersion=1 简化检查)', () => {
31
- expect(opt('# Title', 1)).toBe('#### Title')
32
- })
33
-
34
- it('H2 → H5 (cardVersion=1)', () => {
35
- expect(opt('## Title', 1)).toBe('##### Title')
36
- })
37
-
38
- it('H3 → H5 (cardVersion=1)', () => {
39
- expect(opt('### Title', 1)).toBe('##### Title')
40
- })
41
-
42
- it('混合 H1+H2+H3 全部降级 (cardVersion=1)', () => {
43
- expect(opt('# H1\n## H2\n### H3', 1)).toBe('#### H1\n##### H2\n##### H3')
44
- })
45
-
46
- it('纯 H4 文档不触发降级 (cardVersion=1)', () => {
47
- // 触发条件: 原文必须有 H1~H3
48
- expect(opt('#### Already H4', 1)).toBe('#### Already H4')
49
- })
50
-
51
- it('同时存在 H1 和 H4: H1→H4, 原 H4 → H5 (cardVersion=1)', () => {
52
- expect(opt('# Top\n#### Sub', 1)).toBe('#### Top\n##### Sub')
53
- })
54
-
55
- it('# 后必须有空格才算标题', () => {
56
- expect(opt('#notaheading', 1)).toBe('#notaheading')
57
- })
58
-
59
- it('顺序保证: # 降成 #### 后不会被 #{2,6} 再次吃成 #####', () => {
60
- // openclaw-lark 源码里的关键注释:顺序不能颠倒
61
- expect(opt('# Top', 1)).toBe('#### Top')
62
- })
63
-
64
- it('无标题文本原样返回', () => {
65
- expect(opt('just plain text', 1)).toBe('just plain text')
66
- })
67
-
68
- it('默认 cardVersion=2 下也能正确降级标题', () => {
69
- const out = opt('# Title')
70
- expect(out).toContain('#### Title')
71
- expect(out).not.toMatch(/^# Title$/m)
72
- })
73
- })
74
-
75
- // ---------------------------------------------------------------------------
76
- // 代码块保护
77
- // ---------------------------------------------------------------------------
78
-
79
- describe('optimizeMarkdownForFeishu: 代码块保护', () => {
80
- it('代码块内的 # 不被降级 (cardVersion=1)', () => {
81
- const input = '```\n# not a heading\n## also not\n```'
82
- expect(opt(input, 1)).toBe(input)
83
- })
84
-
85
- it('外部 H1 降级,代码块内 # 保持 (cardVersion=1)', () => {
86
- const input = '# Real heading\n\n```\n# inside code\n```'
87
- expect(opt(input, 1)).toBe('#### Real heading\n\n```\n# inside code\n```')
88
- })
89
-
90
- it('语言标记的 fenced 代码块也受保护 (cardVersion=1)', () => {
91
- const input = '## Section\n\n```python\n# python comment\n### not a heading\n```'
92
- expect(opt(input, 1)).toBe('##### Section\n\n```python\n# python comment\n### not a heading\n```')
93
- })
94
-
95
- it('多个代码块按顺序保护与还原 (cardVersion=1)', () => {
96
- const input = '# A\n```\n# b1\n```\n## C\n```\n### b2\n```'
97
- expect(opt(input, 1)).toBe('#### A\n```\n# b1\n```\n##### C\n```\n### b2\n```')
98
- })
99
-
100
- it('默认 cardVersion=2 下代码块内 # 仍受保护(语义断言)', () => {
101
- const out = opt('# Heading\n\n```\n# inside\n```')
102
- expect(out).toContain('#### Heading') // 外部 H1 降级
103
- expect(out).toContain('# inside') // 代码块内保留
104
- expect(out).toContain('```') // fence 保留
105
- })
106
- })
107
-
108
- // ---------------------------------------------------------------------------
109
- // 空行压缩
110
- // ---------------------------------------------------------------------------
111
-
112
- describe('optimizeMarkdownForFeishu: 空行压缩', () => {
113
- it('3 个换行 → 2 个 (cardVersion=1)', () => {
114
- expect(opt('line1\n\n\nline2', 1)).toBe('line1\n\nline2')
115
- })
116
-
117
- it('5 个换行 → 2 个 (cardVersion=1)', () => {
118
- expect(opt('line1\n\n\n\n\nline2', 1)).toBe('line1\n\nline2')
119
- })
120
-
121
- it('2 个换行保留 (cardVersion=1)', () => {
122
- expect(opt('line1\n\nline2', 1)).toBe('line1\n\nline2')
123
- })
124
-
125
- it('代码块内部连续换行被保留 (cardVersion=1)', () => {
126
- const input = '# Title\n\n```\nline1\n\n\nline2\n```'
127
- expect(opt(input, 1)).toBe('#### Title\n\n```\nline1\n\n\nline2\n```')
128
- })
129
- })
130
-
131
- // ---------------------------------------------------------------------------
132
- // Schema 2.0: <br> 间距
133
- // ---------------------------------------------------------------------------
134
-
135
- describe('optimizeMarkdownForFeishu: Schema 2.0 <br> 间距', () => {
136
- it('cardVersion=2 默认在代码块前后加 <br>', () => {
137
- const out = opt('text\n\n```\ncode\n```')
138
- // 代码块前后应包裹 <br>
139
- expect(out).toContain('<br>\n```')
140
- expect(out).toContain('```\n<br>')
141
- })
142
-
143
- it('cardVersion=1 代码块前后不加 <br>', () => {
144
- const out = opt('text\n\n```\ncode\n```', 1)
145
- expect(out).not.toContain('<br>')
146
- })
147
-
148
- it('cardVersion=2 连续标题之间加 <br>', () => {
149
- const out = opt('# A\n# B')
150
- // H1 降级为 H4,之间插入 <br>
151
- expect(out).toMatch(/#### A\n<br>\n#### B/)
152
- })
153
-
154
- it('cardVersion=2 表格前后加 <br>', () => {
155
- const input = 'text before\n\n| col1 | col2 |\n|------|------|\n| v1 | v2 |\n\ntext after'
156
- const out = opt(input)
157
- // 表格前: <br> 紧贴文本行(规则 3e 压缩多余空行)
158
- expect(out).toMatch(/text before\n<br>\n\| col1/)
159
- // 表格后: <br> 跟两个换行到下一段文本
160
- expect(out).toMatch(/\| v1 \| v2 \|\n<br>\n\ntext after/)
161
- })
162
-
163
- it('cardVersion=1 表格前后不加 <br>', () => {
164
- const input = 'text before\n\n| col1 | col2 |\n|------|------|\n| v1 | v2 |\n\ntext after'
165
- const out = opt(input, 1)
166
- expect(out).not.toContain('<br>')
167
- })
168
-
169
- it('代码块内的 | 不被当表格处理 (Schema 2.0)', () => {
170
- const input = '```\n| in code | not a table |\n|---|---|\n```'
171
- const out = opt(input)
172
- // 代码块本体应完整保留
173
- expect(out).toContain('| in code | not a table |')
174
- // 代码块外应该没有出现表格 br 标记(因为代码块内不算表格)
175
- // 代码块本身会被 <br> 包裹(Schema 2.0)但不会在 | 周围单独加 <br>
176
- expect(out).toMatch(/<br>\n```\n\| in code/)
177
- })
178
- })
179
-
180
- // ---------------------------------------------------------------------------
181
- // stripInvalidImageKeys
182
- // ---------------------------------------------------------------------------
183
-
184
- describe('optimizeMarkdownForFeishu: stripInvalidImageKeys', () => {
185
- it('img_* 图片 key 保留', () => {
186
- const out = opt('前缀 ![alt](img_abc123) 后缀')
187
- expect(out).toContain('![alt](img_abc123)')
188
- })
189
-
190
- it('http:// URL 图片被删除', () => {
191
- const out = opt('前缀 ![alt](http://example.com/img.png) 后缀')
192
- expect(out).toBe('前缀 后缀')
193
- })
194
-
195
- it('https:// URL 图片被删除', () => {
196
- const out = opt('![a](https://x.y/z.jpg)')
197
- expect(out).toBe('')
198
- })
199
-
200
- it('本地路径被删除', () => {
201
- const out = opt('![a](/Users/me/pic.png)')
202
- expect(out).toBe('')
203
- })
204
-
205
- it('无图片文本原样', () => {
206
- expect(opt('no images here', 1)).toBe('no images here')
207
- })
208
-
209
- it('混合: img_ 保留,URL 删除', () => {
210
- const out = opt('![keep](img_good) 和 ![drop](http://bad.com/x.png)')
211
- expect(out).toContain('![keep](img_good)')
212
- expect(out).not.toContain('bad.com')
213
- expect(out).not.toContain('![drop]')
214
- })
215
- })
216
-
217
- // ---------------------------------------------------------------------------
218
- // findMarkdownTablesOutsideCodeBlocks
219
- // ---------------------------------------------------------------------------
220
-
221
- describe('findMarkdownTablesOutsideCodeBlocks', () => {
222
- it('识别单张表格', () => {
223
- const text = '| a | b |\n|---|---|\n| 1 | 2 |'
224
- const matches = findMarkdownTablesOutsideCodeBlocks(text)
225
- expect(matches.length).toBe(1)
226
- expect(matches[0]!.raw).toContain('| a | b |')
227
- })
228
-
229
- it('识别多张表格', () => {
230
- const text =
231
- '| a | b |\n|---|---|\n| 1 | 2 |\n\ntext\n\n| x | y |\n|---|---|\n| 3 | 4 |'
232
- const matches = findMarkdownTablesOutsideCodeBlocks(text)
233
- expect(matches.length).toBe(2)
234
- })
235
-
236
- it('代码块内的 | 不被算作表格', () => {
237
- const text = '```\n| in | code |\n|---|---|\n| 1 | 2 |\n```'
238
- const matches = findMarkdownTablesOutsideCodeBlocks(text)
239
- expect(matches.length).toBe(0)
240
- })
241
-
242
- it('代码块 + 外部表格: 只识别外部的', () => {
243
- const text =
244
- '```\n| in | code |\n|---|---|\n| 1 | 2 |\n```\n\n| real | table |\n|---|---|\n| a | b |'
245
- const matches = findMarkdownTablesOutsideCodeBlocks(text)
246
- expect(matches.length).toBe(1)
247
- expect(matches[0]!.raw).toContain('real')
248
- })
249
-
250
- it('无表格文本返回空数组', () => {
251
- expect(findMarkdownTablesOutsideCodeBlocks('just text').length).toBe(0)
252
- })
253
- })
254
-
255
- // ---------------------------------------------------------------------------
256
- // sanitizeTextForCard
257
- // ---------------------------------------------------------------------------
258
-
259
- describe('sanitizeTextForCard: 表格数量限制', () => {
260
- function makeTable(label: string): string {
261
- return `| ${label} h1 | h2 |\n|---|---|\n| v1 | v2 |`
262
- }
263
-
264
- it('表格数 ≤ 3 时原样返回', () => {
265
- const text = [makeTable('A'), makeTable('B'), makeTable('C')].join('\n\n')
266
- expect(sanitizeTextForCard(text)).toBe(text)
267
- })
268
-
269
- it('恰好 3 张表格原样返回', () => {
270
- const text = [makeTable('A'), makeTable('B'), makeTable('C')].join('\n\n')
271
- const matches = findMarkdownTablesOutsideCodeBlocks(text)
272
- expect(matches.length).toBe(3)
273
- expect(sanitizeTextForCard(text)).toBe(text)
274
- })
275
-
276
- it('4 张表格: 前 3 张保留,第 4 张包裹成 code block', () => {
277
- const text = [makeTable('A'), makeTable('B'), makeTable('C'), makeTable('D')].join('\n\n')
278
- const out = sanitizeTextForCard(text)
279
- // 前 3 张表格原样
280
- expect(out).toContain(makeTable('A'))
281
- expect(out).toContain(makeTable('B'))
282
- expect(out).toContain(makeTable('C'))
283
- // 第 4 张被包裹
284
- expect(out).toContain('```\n' + makeTable('D') + '\n```')
285
- })
286
-
287
- it('自定义 limit=1: 第 1 张保留,之后全部包裹', () => {
288
- const text = [makeTable('A'), makeTable('B'), makeTable('C')].join('\n\n')
289
- const out = sanitizeTextForCard(text, 1)
290
- expect(out).toContain(makeTable('A'))
291
- expect(out).toContain('```\n' + makeTable('B') + '\n```')
292
- expect(out).toContain('```\n' + makeTable('C') + '\n```')
293
- })
294
-
295
- it('limit=0: 全部包裹', () => {
296
- const text = makeTable('Solo')
297
- const out = sanitizeTextForCard(text, 0)
298
- expect(out).toContain('```\n' + makeTable('Solo') + '\n```')
299
- })
300
-
301
- it('无表格原样返回', () => {
302
- expect(sanitizeTextForCard('no tables here')).toBe('no tables here')
303
- })
304
-
305
- it('FEISHU_CARD_TABLE_LIMIT 默认值 = 3', () => {
306
- expect(FEISHU_CARD_TABLE_LIMIT).toBe(3)
307
- })
308
- })
309
-
310
- // ---------------------------------------------------------------------------
311
- // 边界与真实场景
312
- // ---------------------------------------------------------------------------
313
-
314
- describe('optimizeMarkdownForFeishu: 边界与真实场景', () => {
315
- it('screenshot 里的 OpenCutSkill 项目结构报告', () => {
316
- const input = `## OpenCutSkill 项目架构概览
317
-
318
- ### 1. 项目定位
319
-
320
- Screen Studio 视频自动剪辑工具。
321
-
322
- ### 2. 模块结构
323
-
324
- \`\`\`
325
- opencutskill/
326
- ├── cli/
327
- ├── core/
328
- └── tests/
329
- \`\`\``
330
- const out = opt(input)
331
- // 所有 H2~H3 应被降级为 H5
332
- expect(out).toContain('##### OpenCutSkill 项目架构概览')
333
- expect(out).toContain('##### 1. 项目定位')
334
- expect(out).toContain('##### 2. 模块结构')
335
- // 代码块内容原封不动
336
- expect(out).toContain('opencutskill/')
337
- expect(out).toContain('├── cli/')
338
- // 原始 ## 字面量不残留
339
- expect(out).not.toMatch(/^## OpenCutSkill/m)
340
- expect(out).not.toMatch(/^### 1\./m)
341
- // 代码块前后有 <br>(Schema 2.0 默认)
342
- expect(out).toContain('<br>\n```')
343
- expect(out).toContain('```\n<br>')
344
- })
345
-
346
- it('异常输入 fallback 到原文不抛错', () => {
347
- expect(() => opt('\u0000\uFFFF```unclosed')).not.toThrow()
348
- })
349
-
350
- it('空字符串返回空字符串', () => {
351
- expect(opt('')).toBe('')
352
- })
353
- })