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,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 图片语法: `` */
|
|
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
|
-
}
|
package/adapters/feishu/media.ts
DELETED
|
@@ -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
|
-
}
|