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,212 +0,0 @@
1
- /**
2
- * Feishu 卡片 Markdown 样式优化
3
- *
4
- * 背景:
5
- * - 飞书卡片的 `tag: 'markdown'` 元素对 H1~H3 标题有已知渲染异常(字面量显示)。
6
- * 必须降级为 H4/H5 才能正常渲染。
7
- * - Schema 2.0 CardKit 支持完整的 markdown 但需要手动加 `<br>` 间距避免标题/表格/
8
- * 代码块贴得太紧。
9
- * - 卡片有表格数量上限(FEISHU_CARD_TABLE_LIMIT=3),超出会触发 230099/11310。
10
- * - 图片必须是飞书上传过的 `img_xxx` key,其它 URL 会触发 CardKit 错误 200570。
11
- *
12
- * 实现参考: openclaw-lark/src/card/markdown-style.ts + card-error.ts
13
- */
14
-
15
- // ---------------------------------------------------------------------------
16
- // Constants
17
- // ---------------------------------------------------------------------------
18
-
19
- /** 飞书卡片表格数量上限 —— 超出 3 张触发 230099/11310(openclaw 2026-03 实测) */
20
- export const FEISHU_CARD_TABLE_LIMIT = 3
21
-
22
- // ---------------------------------------------------------------------------
23
- // Public: optimizeMarkdownForFeishu
24
- // ---------------------------------------------------------------------------
25
-
26
- /**
27
- * 对将要放入 `tag: 'markdown'` 元素的 markdown 做安全预处理。
28
- *
29
- * - 标题降级: 若原文包含 H1~H3,则 H2~H6 → H5,H1 → H4
30
- * - 代码块内容受保护,不会被降级
31
- * - Schema 2.0: 连续标题/表格/代码块前后加 `<br>` 间距
32
- * - 连续 3+ 空行压缩为 2
33
- * - 删除非 `img_*` 的 markdown 图片引用(防止 CardKit 200570)
34
- * - 任何内部错误都 fallback 到原文,不阻塞消息发送
35
- *
36
- * @param text 原始 markdown
37
- * @param cardVersion 卡片 schema 版本。默认 2 (Schema 2.0 CardKit),
38
- * 1 对应老 Schema 1.0 fallback 路径(已不推荐)。
39
- */
40
- export function optimizeMarkdownForFeishu(text: string, cardVersion = 2): string {
41
- try {
42
- let r = _optimizeMarkdownForFeishu(text, cardVersion)
43
- r = stripInvalidImageKeys(r)
44
- return r
45
- } catch {
46
- return text
47
- }
48
- }
49
-
50
- function _optimizeMarkdownForFeishu(text: string, cardVersion: number): string {
51
- // ── 1. 提取代码块,用占位符保护,处理后再还原 ─────────────────────
52
- // 代码块内的 `#` / `|` 不能被标题降级或表格匹配误伤
53
- const MARK = '___CB_'
54
- const codeBlocks: string[] = []
55
- let r = text.replace(/```[\s\S]*?```/g, (m) => {
56
- return `${MARK}${codeBlocks.push(m) - 1}___`
57
- })
58
-
59
- // ── 2. 标题降级 ────────────────────────────────────────────────────
60
- // 只有当原文档(不是保护后的 r)包含 H1~H3 时才执行降级
61
- // 顺序: 先 H2~H6 → H5,再 H1 → H4
62
- // 若先 H1→H4,`####` 会被后面的 `#{2,6}` 再次匹配成 H5
63
- const hasH1toH3 = /^#{1,3} /m.test(text)
64
- if (hasH1toH3) {
65
- r = r.replace(/^#{2,6} (.+)$/gm, '##### $1') // H2~H6 → H5
66
- r = r.replace(/^# (.+)$/gm, '#### $1') // H1 → H4
67
- }
68
-
69
- // ── 3. Schema 2.0 段落间距 ────────────────────────────────────────
70
- if (cardVersion >= 2) {
71
- // 3a. 连续标题之间加 <br>
72
- r = r.replace(/^(#{4,5} .+)\n{1,2}(#{4,5} )/gm, '$1\n<br>\n$2')
73
-
74
- // 3b. 非表格行直接跟表格行 → 先补空行(保证后续规则生效)
75
- r = r.replace(/^([^|\n].*)\n(\|.+\|)/gm, '$1\n\n$2')
76
-
77
- // 3c. 表格前: 在空行之前插入 <br>(即 `\n\n|` → `\n<br>\n\n|`)
78
- r = r.replace(/\n\n((?:\|.+\|[^\S\n]*\n?)+)/g, '\n\n<br>\n\n$1')
79
-
80
- // 3d. 表格后: 在表格块末尾追加 <br>(跳过后接分隔线/标题/加粗/文末)
81
- r = r.replace(/((?:^\|.+\|[^\S\n]*\n?)+)/gm, (m, _table, offset) => {
82
- const after = r.slice(offset + m.length).replace(/^\n+/, '')
83
- if (!after || /^(---|#{4,5} |\*\*)/.test(after)) return m
84
- return m + '\n<br>\n'
85
- })
86
-
87
- // 3e. 表格前是普通文本: 只保留 <br>,去掉多余空行
88
- // "text\n\n<br>\n\n|" → "text\n<br>\n|"
89
- r = r.replace(/^((?!#{4,5} )(?!\*\*).+)\n\n(<br>)\n\n(\|)/gm, '$1\n$2\n$3')
90
-
91
- // 3f. 表格前是加粗行: <br> 紧贴加粗行,空行保留在后面
92
- // "**bold**\n\n<br>\n\n|" → "**bold**\n<br>\n\n|"
93
- r = r.replace(/^(\*\*.+)\n\n(<br>)\n\n(\|)/gm, '$1\n$2\n\n$3')
94
-
95
- // 3g. 表格后是普通文本: 去掉多余空行
96
- // "| row |\n\n<br>\ntext" → "| row |\n<br>\ntext"
97
- r = r.replace(/(\|[^\n]*\n)\n(<br>\n)((?!#{4,5} )(?!\*\*))/gm, '$1$2$3')
98
- }
99
-
100
- // ── 4. 压缩多余空行(3 个以上连续换行 → 2 个)────────────────────
101
- // 必须在还原代码块之前做,否则代码块内部的连续换行会被误伤
102
- r = r.replace(/\n{3,}/g, '\n\n')
103
-
104
- // ── 5. 还原代码块 ─────────────────────────────────────────────────
105
- // Schema 2.0 时前后加 <br>,让代码块与周围段落拉开距离
106
- codeBlocks.forEach((block, i) => {
107
- const replacement = cardVersion >= 2 ? `\n<br>\n${block}\n<br>\n` : block
108
- r = r.replace(`${MARK}${i}___`, replacement)
109
- })
110
-
111
- return r
112
- }
113
-
114
- // ---------------------------------------------------------------------------
115
- // stripInvalidImageKeys
116
- // ---------------------------------------------------------------------------
117
-
118
- /** 匹配完整的 markdown 图片语法: `![alt](value)` */
119
- const IMAGE_RE = /!\[([^\]]*)\]\(([^)\s]+)\)/g
120
-
121
- /**
122
- * 删除 value 不是飞书 image key (`img_xxx`) 的 markdown 图片引用。
123
- * 防止 CardKit 错误 200570(unknown image key)。
124
- *
125
- * HTTP URL 和本地路径也会被删除 —— 上游(ImageResolver)负责把它们转成
126
- * `img_xxx`,此函数是 safety net。
127
- */
128
- function stripInvalidImageKeys(text: string): string {
129
- if (!text.includes('![')) return text
130
- return text.replace(IMAGE_RE, (fullMatch, _alt, value) => {
131
- if (value.startsWith('img_')) return fullMatch
132
- return ''
133
- })
134
- }
135
-
136
- // ---------------------------------------------------------------------------
137
- // Table limiting: sanitizeTextForCard
138
- // ---------------------------------------------------------------------------
139
-
140
- export type MarkdownTableMatch = {
141
- index: number
142
- length: number
143
- raw: string
144
- }
145
-
146
- /**
147
- * 扫描正文里会被飞书卡片**实际渲染**的 markdown 表格位置。
148
- *
149
- * 代码块内的示例表格不会被飞书解析成卡片表格元素,因此要先排除。
150
- * 这份结果供 `sanitizeTextForCard` 判断是否需要降级多余的表格。
151
- */
152
- export function findMarkdownTablesOutsideCodeBlocks(text: string): MarkdownTableMatch[] {
153
- // 先扫描代码块区间
154
- const codeBlockRanges: Array<{ start: number; end: number }> = []
155
- const codeBlockRegex = /```[\s\S]*?```/g
156
- let cbMatch = codeBlockRegex.exec(text)
157
- while (cbMatch != null) {
158
- codeBlockRanges.push({
159
- start: cbMatch.index,
160
- end: cbMatch.index + cbMatch[0].length,
161
- })
162
- cbMatch = codeBlockRegex.exec(text)
163
- }
164
- const isInsideCodeBlock = (idx: number): boolean =>
165
- codeBlockRanges.some((range) => idx >= range.start && idx < range.end)
166
-
167
- // 扫描表格(header | sep | body...)
168
- const tableRegex = /\|.+\|[\r\n]+\|[-:| ]+\|[\s\S]*?(?=\n\n|\n(?!\|)|$)/g
169
- const matches: MarkdownTableMatch[] = []
170
- let tableMatch = tableRegex.exec(text)
171
- while (tableMatch != null) {
172
- if (!isInsideCodeBlock(tableMatch.index)) {
173
- matches.push({
174
- index: tableMatch.index,
175
- length: tableMatch[0].length,
176
- raw: tableMatch[0],
177
- })
178
- }
179
- tableMatch = tableRegex.exec(text)
180
- }
181
- return matches
182
- }
183
-
184
- /**
185
- * 对正文里超出 `tableLimit` 张的 markdown 表格降级为代码块,防止触发
186
- * 230099/11310。前 `tableLimit` 张保持原样(卡片正常渲染),其余用
187
- * 反引号包裹(飞书会当成 code block 而不是表格)。
188
- */
189
- export function sanitizeTextForCard(
190
- text: string,
191
- tableLimit: number = FEISHU_CARD_TABLE_LIMIT,
192
- ): string {
193
- const matches = findMarkdownTablesOutsideCodeBlocks(text)
194
- if (matches.length <= tableLimit) return text
195
- return wrapTablesBeyondLimit(text, matches, Math.max(tableLimit, 0))
196
- }
197
-
198
- function wrapTablesBeyondLimit(
199
- text: string,
200
- matches: readonly MarkdownTableMatch[],
201
- keepCount: number,
202
- ): string {
203
- if (matches.length <= keepCount) return text
204
- // 从后往前替换,避免前面的替换导致后面的 offset 错乱
205
- let result = text
206
- for (let i = matches.length - 1; i >= keepCount; i--) {
207
- const { index, length, raw } = matches[i]!
208
- const replacement = '```\n' + raw + '\n```'
209
- result = result.slice(0, index) + replacement + result.slice(index + length)
210
- }
211
- return result
212
- }
@@ -1,176 +0,0 @@
1
- /**
2
- * Feishu media service — wraps im.messageResource / im.image / im.file
3
- * so adapters/feishu/index.ts stays focused on flow control.
4
- *
5
- * References:
6
- * - Feishu OpenAPI: POST /open-apis/im/v1/images
7
- * POST /open-apis/im/v1/files
8
- * GET /open-apis/im/v1/messages/{message_id}/resources/{file_key}
9
- * - OpenClaw impl: openclaw-lark/src/messaging/outbound/media.ts:226,281,323,423,454
10
- */
11
-
12
- import * as Lark from '@larksuiteoapi/node-sdk'
13
- import * as fs from 'node:fs/promises'
14
- import * as path from 'node:path'
15
- import { AttachmentStore } from '../common/attachment/attachment-store.js'
16
- import type { LocalAttachment } from '../common/attachment/attachment-types.js'
17
-
18
- type LarkClient = InstanceType<typeof Lark.Client>
19
-
20
- /** Map a filename extension to Feishu's file_type enum. */
21
- function detectFeishuFileType(
22
- fileName: string,
23
- ): 'opus' | 'mp4' | 'pdf' | 'doc' | 'xls' | 'ppt' | 'stream' {
24
- const ext = path.extname(fileName).toLowerCase().replace(/^\./, '')
25
- switch (ext) {
26
- case 'opus':
27
- return 'opus'
28
- case 'mp4':
29
- return 'mp4'
30
- case 'pdf':
31
- return 'pdf'
32
- case 'doc':
33
- case 'docx':
34
- return 'doc'
35
- case 'xls':
36
- case 'xlsx':
37
- return 'xls'
38
- case 'ppt':
39
- case 'pptx':
40
- return 'ppt'
41
- default:
42
- return 'stream'
43
- }
44
- }
45
-
46
- function guessMime(fileName: string, kind: 'image' | 'file'): string {
47
- const ext = path.extname(fileName).toLowerCase().replace(/^\./, '')
48
- if (kind === 'image') {
49
- return (
50
- ({
51
- png: 'image/png',
52
- jpg: 'image/jpeg',
53
- jpeg: 'image/jpeg',
54
- gif: 'image/gif',
55
- webp: 'image/webp',
56
- heic: 'image/heic',
57
- } as Record<string, string>)[ext] || 'image/png'
58
- )
59
- }
60
- return (
61
- ({
62
- pdf: 'application/pdf',
63
- doc: 'application/msword',
64
- docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
65
- xls: 'application/vnd.ms-excel',
66
- xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
67
- ppt: 'application/vnd.ms-powerpoint',
68
- pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
69
- txt: 'text/plain',
70
- json: 'application/json',
71
- } as Record<string, string>)[ext] || 'application/octet-stream'
72
- )
73
- }
74
-
75
- export interface DownloadParams {
76
- messageId: string
77
- fileKey: string
78
- kind: 'image' | 'file'
79
- fileName?: string
80
- sessionId: string
81
- }
82
-
83
- export class FeishuMediaService {
84
- constructor(
85
- private readonly client: LarkClient,
86
- private readonly store: AttachmentStore,
87
- ) {}
88
-
89
- /** Download an image or file the user sent in Feishu into the local stage. */
90
- async downloadResource(params: DownloadParams): Promise<LocalAttachment> {
91
- const { messageId, fileKey, kind, sessionId } = params
92
- const fallbackName = `${fileKey}${kind === 'image' ? '.png' : ''}`
93
- const name = params.fileName || fallbackName
94
- const target = this.store.resolvePath('feishu', sessionId, name)
95
-
96
- // node-sdk returns an object with a `.writeFile(target)` helper
97
- // that dumps the underlying stream. See OpenClaw media.ts:147 and :237.
98
- const resp: any = await (this.client.im as any).messageResource.get({
99
- path: { message_id: messageId, file_key: fileKey },
100
- params: { type: kind },
101
- })
102
-
103
- if (typeof resp?.writeFile === 'function') {
104
- await resp.writeFile(target)
105
- } else if (resp?.data instanceof Buffer) {
106
- await this.store.write(target, resp.data)
107
- } else if (resp instanceof Buffer) {
108
- await this.store.write(target, resp)
109
- } else {
110
- throw new Error('[FeishuMedia] Unknown downloadResource response shape')
111
- }
112
-
113
- const buffer = await fs.readFile(target)
114
- return {
115
- kind,
116
- name,
117
- path: target,
118
- size: buffer.length,
119
- mimeType: guessMime(name, kind),
120
- buffer,
121
- }
122
- }
123
-
124
- /** Upload an image buffer, returns image_key.
125
- * The Lark node-sdk type for `image` accepts `Buffer | ReadStream`,
126
- * so passing the buffer directly is the simplest path. */
127
- async uploadImage(buffer: Buffer, _mime: string): Promise<string> {
128
- const resp: any = await this.client.im.image.create({
129
- data: {
130
- image_type: 'message',
131
- image: buffer,
132
- },
133
- })
134
- const key = resp?.data?.image_key
135
- if (!key) throw new Error('[FeishuMedia] uploadImage: missing image_key')
136
- return key
137
- }
138
-
139
- /** Upload a non-image file, returns file_key. */
140
- async uploadFile(buffer: Buffer, fileName: string): Promise<string> {
141
- const resp: any = await this.client.im.file.create({
142
- data: {
143
- file_type: detectFeishuFileType(fileName),
144
- file_name: fileName,
145
- file: buffer,
146
- },
147
- })
148
- const key = resp?.data?.file_key
149
- if (!key) throw new Error('[FeishuMedia] uploadFile: missing file_key')
150
- return key
151
- }
152
-
153
- /** Send an image message to a chat. See OpenClaw media.ts:435. */
154
- async sendImageMessage(chatId: string, imageKey: string): Promise<void> {
155
- await this.client.im.message.create({
156
- params: { receive_id_type: 'chat_id' },
157
- data: {
158
- receive_id: chatId,
159
- msg_type: 'image',
160
- content: JSON.stringify({ image_key: imageKey }),
161
- },
162
- })
163
- }
164
-
165
- /** Send a file message to a chat. See OpenClaw media.ts:466. */
166
- async sendFileMessage(chatId: string, fileKey: string): Promise<void> {
167
- await this.client.im.message.create({
168
- params: { receive_id_type: 'chat_id' },
169
- data: {
170
- receive_id: chatId,
171
- msg_type: 'file',
172
- content: JSON.stringify({ file_key: fileKey }),
173
- },
174
- })
175
- }
176
- }