dsh-qq-tools 0.8.0 → 0.8.2
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/README.md +5 -5
- package/lib/shared.js +238 -44
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -99,7 +99,7 @@ tools/qq-send-poke/
|
|
|
99
99
|
|
|
100
100
|
私聊和群聊在程序里是分开的两条路径:群聊靠轮询 @,私聊靠 WebSocket 的 `message_type=private` 事件。私聊没有观察窗,每收到一条私聊消息都会直接唤醒目标会话。
|
|
101
101
|
|
|
102
|
-
- 唤醒提示里会带发送者昵称、QQ 号、`message_id`
|
|
102
|
+
- 唤醒提示里会带发送者昵称、QQ 号、`message_id` 和文字;如果消息里有图片,也会把图片一起给模型看(有 sharp 时 GIF 会拆成首/中/尾最多 3 帧;没有 sharp 时交给 DSH 显示首帧,和群聊记录的处理一致)。
|
|
103
103
|
- 模型用 `qq_get_private_history` 看和对方的最近记录,用 `qq_send_private_message` 回文字,用 `qq_send_face` 回表情。
|
|
104
104
|
- `qq_list_friends` 列好友,可以按昵称、备注或 QQ 号搜;`privateUsers` 非空时只列白名单里的好友。
|
|
105
105
|
- `qq_get_private_history` 的消息带 `#message_id`,可以传给 `qq_send_private_message` / `qq_send_face` 的 `reply_to` 做引用回复。
|
|
@@ -128,7 +128,7 @@ dsh plugin --profile <profile> add dsh-qq-tools
|
|
|
128
128
|
需要固定版本时:
|
|
129
129
|
|
|
130
130
|
```sh
|
|
131
|
-
dsh plugin --profile <profile> add dsh-qq-tools@0.8.
|
|
131
|
+
dsh plugin --profile <profile> add dsh-qq-tools@0.8.2
|
|
132
132
|
```
|
|
133
133
|
|
|
134
134
|
### 本地开发 / 自己改代码
|
|
@@ -141,7 +141,7 @@ dsh plugin --profile <profile> add link:/path/to/dsh-qq-tools
|
|
|
141
141
|
|
|
142
142
|
```sh
|
|
143
143
|
pnpm pack
|
|
144
|
-
dsh plugin --profile <profile> add ./dsh-qq-tools-0.8.
|
|
144
|
+
dsh plugin --profile <profile> add ./dsh-qq-tools-0.8.2.tgz
|
|
145
145
|
```
|
|
146
146
|
|
|
147
147
|
`dsh plugin` 会在安装后自动把这个包追加进 profile 的 `dsh.profile.bundles`,不用手改 `package.json`。卸载用:
|
|
@@ -155,8 +155,8 @@ dsh plugin --profile <profile> remove dsh-qq-tools
|
|
|
155
155
|
- DSH 0.2.x(cordis 插件体系)
|
|
156
156
|
- Node.js 22+(内置 `fetch` / `WebSocket`)
|
|
157
157
|
- SnowLuma 提供 OneBot 11 HTTP 接口和 WebSocket 事件(收藏表情需要 SnowLuma;系统表情 NapCat 也能发)
|
|
158
|
-
- 可选:`sharp`(GIF
|
|
158
|
+
- 可选:`sharp`(GIF 拆帧、尺寸被拒时的兜底重编码)。会从 `SHARP_PATH`、插件自己的 `node_modules`、DSH 宿主 runtimeDir(桌面版通过 `process.argv[2]` 传入)等位置自动查找;找不到时 GIF 交给 DSH 显示首帧,大图仍由 DSH 自动归一化(默认单张源图上限 20MiB),图片不会因此丢失。
|
|
159
159
|
|
|
160
160
|
---
|
|
161
161
|
|
|
162
|
-
版本:0.8.
|
|
162
|
+
版本:0.8.2
|
package/lib/shared.js
CHANGED
|
@@ -34,7 +34,7 @@
|
|
|
34
34
|
//
|
|
35
35
|
// 工具本身的开关在 DSH 插件页的组件列表里:关掉某个组件,它就不再注册对应工具。
|
|
36
36
|
|
|
37
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, renameSync } from 'node:fs'
|
|
37
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, appendFileSync, renameSync, statSync } from 'node:fs'
|
|
38
38
|
import { join } from 'node:path'
|
|
39
39
|
import { homedir } from 'node:os'
|
|
40
40
|
import { randomUUID } from 'node:crypto'
|
|
@@ -47,34 +47,82 @@ const DIAG_PATH = join(DSH_HOME, 'qq-tools-diag.log')
|
|
|
47
47
|
|
|
48
48
|
|
|
49
49
|
const requireFromHere = createRequire(import.meta.url)
|
|
50
|
+
let sharpSource = ''
|
|
50
51
|
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
52
|
+
function sharpFromDir(dir) {
|
|
53
|
+
try {
|
|
54
|
+
const s = createRequire(join(String(dir), 'noop.js'))('sharp')
|
|
55
|
+
return s || null
|
|
56
|
+
} catch (error) {
|
|
57
|
+
return null
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function sharpFromCache() {
|
|
62
|
+
try {
|
|
63
|
+
const cache = requireFromHere.cache || {}
|
|
64
|
+
for (const key of Object.keys(cache)) {
|
|
65
|
+
if (!/[\\/]node_modules[\\/]sharp[\\/]/.test(key)) continue
|
|
66
|
+
const mod = cache[key]
|
|
67
|
+
if (mod && mod.exports) return mod.exports
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {}
|
|
70
|
+
return null
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// sharp 是可选的,按顺序找:SHARP_PATH → 插件自己装的 → 宿主 runtime → 常见宿主目录。
|
|
74
|
+
// 关键的一条是 process.argv[2]:DSH 桌面版的 dsh-desktop-host 用 argv[2] 接 runtimeDir,
|
|
75
|
+
// 插件和宿主在同一个进程里,所以 runtimeDir/node_modules/sharp 就是宿主实际加载的那份。
|
|
76
|
+
// 不写死安装路径;找不到时核心图片逻辑照常工作,只是不拆 GIF 帧,
|
|
77
|
+
// 尺寸被宿主拒绝时也没法兜底重编码。
|
|
54
78
|
function loadSharp() {
|
|
55
79
|
const probes = []
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (process.
|
|
59
|
-
|
|
60
|
-
|
|
80
|
+
const add = (label, fn) => probes.push({ label, fn })
|
|
81
|
+
|
|
82
|
+
if (process.env.SHARP_PATH) add('SHARP_PATH', () => requireFromHere(process.env.SHARP_PATH))
|
|
83
|
+
add('插件 node_modules', () => requireFromHere('sharp'))
|
|
84
|
+
if (process.argv[1]) add('宿主入口', () => createRequire(process.argv[1])('sharp'))
|
|
85
|
+
if (process.argv[2]) add('宿主 runtimeDir', () => sharpFromDir(process.argv[2]))
|
|
86
|
+
|
|
87
|
+
// 不同启动方式下 runtimeDir 的位置可能变;把 argv 里看起来像目录的路径都试一遍。
|
|
88
|
+
for (let i = 1; i < process.argv.length; i++) {
|
|
89
|
+
const raw = String(process.argv[i] || '')
|
|
90
|
+
if (!raw) continue
|
|
91
|
+
try {
|
|
92
|
+
if (existsSync(raw) && statSync(raw).isDirectory()) add('argv 目录 ' + raw, () => sharpFromDir(raw))
|
|
93
|
+
} catch (error) {}
|
|
61
94
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
95
|
+
|
|
96
|
+
if (process.cwd()) add('当前目录', () => sharpFromDir(process.cwd()))
|
|
97
|
+
if (process.resourcesPath) {
|
|
98
|
+
for (const root of [
|
|
99
|
+
join(process.resourcesPath, 'app.asar', 'dsh'),
|
|
100
|
+
join(process.resourcesPath, 'app.asar.unpacked', 'dsh'),
|
|
101
|
+
join(process.resourcesPath, 'app.asar'),
|
|
102
|
+
join(process.resourcesPath, 'app.asar.unpacked'),
|
|
103
|
+
join(process.resourcesPath, 'runtime'),
|
|
104
|
+
]) {
|
|
105
|
+
add('resources ' + root, () => sharpFromDir(root))
|
|
106
|
+
}
|
|
67
107
|
}
|
|
108
|
+
add('已加载模块缓存', sharpFromCache)
|
|
109
|
+
|
|
68
110
|
for (const probe of probes) {
|
|
69
111
|
try {
|
|
70
|
-
const s = probe()
|
|
71
|
-
if (s)
|
|
112
|
+
const s = probe.fn()
|
|
113
|
+
if (s) {
|
|
114
|
+
sharpSource = probe.label
|
|
115
|
+
return s
|
|
116
|
+
}
|
|
72
117
|
} catch (error) {}
|
|
73
118
|
}
|
|
74
119
|
return null
|
|
75
120
|
}
|
|
76
121
|
|
|
77
122
|
const sharp = loadSharp()
|
|
123
|
+
diag('sharp:' + (sharp
|
|
124
|
+
? '已加载(' + sharpSource + ')'
|
|
125
|
+
: '没找到;GIF 只取首帧,大图交给 DSH 归一化,图片不会因此丢失'))
|
|
78
126
|
let gifWarned = false
|
|
79
127
|
|
|
80
128
|
const DEFAULT_GROUPS = []
|
|
@@ -354,12 +402,14 @@ function segText(segs) {
|
|
|
354
402
|
.join(''))
|
|
355
403
|
}
|
|
356
404
|
|
|
357
|
-
//
|
|
405
|
+
// 这里只做「优先展示动图」的排序提示,不决定 mediaType。
|
|
406
|
+
// QQ 的 [动画表情] 摘要经常贴在静态 PNG/JPEG 上,所以别再信 summary 里的「动画」;
|
|
407
|
+
// 真正是不是 GIF 由后面按字节嗅探,只有 mface 和 .gif 扩展名算动图候选。
|
|
358
408
|
function animatedSeg(seg) {
|
|
359
409
|
if (!seg || !seg.data) return false
|
|
410
|
+
if (seg.type === 'mface') return true
|
|
360
411
|
const d = seg.data
|
|
361
|
-
return
|
|
362
|
-
/gif|动画/i.test(String(d.summary || '') + ' ' + String(d.file || '') + ' ' + String(d.url || ''))
|
|
412
|
+
return /\.gif(?:[?#]|$)/i.test(String(d.file || '')) || /\.gif(?:[?#]|$)/i.test(String(d.url || ''))
|
|
363
413
|
}
|
|
364
414
|
|
|
365
415
|
function fmtTime(sec) {
|
|
@@ -465,13 +515,81 @@ export function makeListTool() {
|
|
|
465
515
|
}
|
|
466
516
|
|
|
467
517
|
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
518
|
+
// ---------- 图片类型与尺寸 ----------
|
|
519
|
+
// DSH 附件层按真实字节复核 mediaType(IMAGE_TYPE_MISMATCH),文件名和 QQ 的
|
|
520
|
+
// animated 提示都可能撒谎,所以只信字节。大图不依赖 sharp:DSH 默认单张源图
|
|
521
|
+
// 上限 20MiB、归一化目标 4MiB,直接交给它处理;sharp 只用于 GIF 拆帧和
|
|
522
|
+
// admit 被尺寸拒绝后的兜底重编码。
|
|
523
|
+
const ADMIT_IMAGE_LIMIT = 6
|
|
524
|
+
const IMAGE_SOFT_LIMIT = 4 * 1024 * 1024
|
|
525
|
+
const IMAGE_MEDIA_TYPES = {
|
|
526
|
+
png: 'image/png',
|
|
527
|
+
jpeg: 'image/jpeg',
|
|
528
|
+
webp: 'image/webp',
|
|
529
|
+
gif: 'image/gif',
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function sniffImageType(bytes) {
|
|
533
|
+
if (!bytes || bytes.length < 12) return ''
|
|
534
|
+
if (bytes[0] === 0x89 && bytes[1] === 0x50 && bytes[2] === 0x4e && bytes[3] === 0x47 &&
|
|
535
|
+
bytes[4] === 0x0d && bytes[5] === 0x0a && bytes[6] === 0x1a && bytes[7] === 0x0a) return 'image/png'
|
|
536
|
+
if (bytes[0] === 0xff && bytes[1] === 0xd8 && bytes[2] === 0xff) return 'image/jpeg'
|
|
537
|
+
if (bytes[0] === 0x47 && bytes[1] === 0x49 && bytes[2] === 0x46 && bytes[3] === 0x38 &&
|
|
538
|
+
(bytes[4] === 0x37 || bytes[4] === 0x39) && bytes[5] === 0x61) return 'image/gif'
|
|
539
|
+
if (bytes.toString('latin1', 0, 4) === 'RIFF' && bytes.toString('latin1', 8, 12) === 'WEBP') return 'image/webp'
|
|
540
|
+
return ''
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
async function sharpImageType(bytes) {
|
|
544
|
+
if (!sharp) return ''
|
|
545
|
+
try {
|
|
546
|
+
const meta = await sharp(bytes, { animated: true }).metadata()
|
|
547
|
+
return IMAGE_MEDIA_TYPES[String((meta && meta.format) || '').toLowerCase()] || ''
|
|
548
|
+
} catch (error) {
|
|
549
|
+
return ''
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// DSH 只收 png/jpeg/webp/gif;sharp 能解的其他格式(TIFF 等)先转成它认识的格式。
|
|
554
|
+
async function convertImage(bytes) {
|
|
555
|
+
if (!sharp) return null
|
|
556
|
+
try {
|
|
557
|
+
const meta = await sharp(bytes).metadata()
|
|
558
|
+
const hasAlpha = !!meta.hasAlpha
|
|
559
|
+
const out = hasAlpha
|
|
560
|
+
? await sharp(bytes).webp({ quality: 82 }).toBuffer()
|
|
561
|
+
: await sharp(bytes).jpeg({ quality: 82 }).toBuffer()
|
|
562
|
+
return { bytes: out, mediaType: hasAlpha ? 'image/webp' : 'image/jpeg' }
|
|
563
|
+
} catch (error) {
|
|
564
|
+
return null
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
// 大图不丢:超过 DSH 归一化目标就先缩到 2048 长边并重编码,通常能压到 1MiB 以内。
|
|
569
|
+
async function shrinkImage(bytes) {
|
|
570
|
+
if (!sharp || !bytes || !bytes.length) return null
|
|
571
|
+
try {
|
|
572
|
+
const meta = await sharp(bytes, { animated: false }).metadata()
|
|
573
|
+
const width = Number(meta && meta.width) || 0
|
|
574
|
+
const height = Number(meta && meta.height) || 0
|
|
575
|
+
if (!width || !height) return null
|
|
576
|
+
const hasAlpha = !!meta.hasAlpha
|
|
577
|
+
const scale = Math.min(1, 2048 / Math.max(width, height))
|
|
578
|
+
const targetWidth = Math.max(1, Math.round(width * scale))
|
|
579
|
+
const targetHeight = Math.max(1, Math.round(height * scale))
|
|
580
|
+
for (const quality of [82, 70, 58]) {
|
|
581
|
+
const pipeline = sharp(bytes, { animated: false })
|
|
582
|
+
.rotate()
|
|
583
|
+
.resize({ width: targetWidth, height: targetHeight, fit: 'inside', withoutEnlargement: true })
|
|
584
|
+
const out = hasAlpha
|
|
585
|
+
? await pipeline.webp({ quality }).toBuffer()
|
|
586
|
+
: await pipeline.jpeg({ quality }).toBuffer()
|
|
587
|
+
if (out.length <= IMAGE_SOFT_LIMIT) {
|
|
588
|
+
return { bytes: out, mediaType: hasAlpha ? 'image/webp' : 'image/jpeg' }
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} catch (error) {}
|
|
592
|
+
return null
|
|
475
593
|
}
|
|
476
594
|
|
|
477
595
|
|
|
@@ -544,29 +662,111 @@ async function imageBytes(data, cfg) {
|
|
|
544
662
|
return null
|
|
545
663
|
}
|
|
546
664
|
|
|
665
|
+
function admitErrorText(error) {
|
|
666
|
+
if (!error) return '未知错误'
|
|
667
|
+
return (error.code ? String(error.code) + ':' : '') + String(error.message || error)
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// 单张被拒时可以重编码再试;批量/存储层错误重试没有意义。
|
|
671
|
+
const RETRYABLE_IMAGE_ERRORS = new Set([
|
|
672
|
+
'IMAGE_TYPE_MISMATCH',
|
|
673
|
+
'INVALID_IMAGE',
|
|
674
|
+
'UNSUPPORTED_IMAGE_TYPE',
|
|
675
|
+
'IMAGE_TOO_LARGE',
|
|
676
|
+
'IMAGE_TOO_MANY_PIXELS',
|
|
677
|
+
'IMAGE_DIMENSION_TOO_LARGE',
|
|
678
|
+
])
|
|
679
|
+
|
|
680
|
+
async function admitOneImage(attachments, block) {
|
|
681
|
+
try {
|
|
682
|
+
const out = await attachments.admitPromptContent([block])
|
|
683
|
+
const hit = (Array.isArray(out) ? out : []).find((x) => x && x.type === 'image')
|
|
684
|
+
return hit ? { block: hit } : { error: new Error('附件层没有返回图片') }
|
|
685
|
+
} catch (error) {
|
|
686
|
+
return { error }
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
async function retryImageBlock(block) {
|
|
691
|
+
let bytes
|
|
692
|
+
try {
|
|
693
|
+
bytes = Buffer.from(String((block && block.data) || ''), 'base64')
|
|
694
|
+
} catch (error) {
|
|
695
|
+
return null
|
|
696
|
+
}
|
|
697
|
+
if (!bytes || !bytes.length) return null
|
|
698
|
+
const type = sniffImageType(bytes)
|
|
699
|
+
const fixed = type ? await shrinkImage(bytes) : await convertImage(bytes)
|
|
700
|
+
if (!fixed) return null
|
|
701
|
+
return { ...block, data: fixed.bytes.toString('base64'), mediaType: fixed.mediaType }
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// 一张坏图不能拖垮整批:先整批送,失败后退回逐张,逐张失败的再重编码试一次。
|
|
705
|
+
async function admitPreparedImages(attachments, prepared) {
|
|
706
|
+
const blocks = prepared.map((x) => x.block)
|
|
707
|
+
try {
|
|
708
|
+
const out = await attachments.admitPromptContent(blocks)
|
|
709
|
+
const images = (Array.isArray(out) ? out : []).filter((x) => x && x.type === 'image')
|
|
710
|
+
if (images.length === blocks.length) {
|
|
711
|
+
return images.map((block, i) => ({ caption: prepared[i].caption, block }))
|
|
712
|
+
}
|
|
713
|
+
diag('图片 admit 返回数量不符(' + images.length + '/' + blocks.length + '),改为逐张处理')
|
|
714
|
+
} catch (error) {
|
|
715
|
+
diag('图片批量 admit 失败,改为逐张重试:' + admitErrorText(error))
|
|
716
|
+
}
|
|
717
|
+
const admitted = []
|
|
718
|
+
let firstError = ''
|
|
719
|
+
for (const item of prepared) {
|
|
720
|
+
let result = await admitOneImage(attachments, item.block)
|
|
721
|
+
if (result.error && RETRYABLE_IMAGE_ERRORS.has(String(result.error.code || ''))) {
|
|
722
|
+
const retried = await retryImageBlock(item.block)
|
|
723
|
+
if (retried) result = await admitOneImage(attachments, retried)
|
|
724
|
+
}
|
|
725
|
+
if (result.block) admitted.push({ caption: item.caption, block: result.block })
|
|
726
|
+
else if (!firstError) firstError = admitErrorText(result.error)
|
|
727
|
+
}
|
|
728
|
+
if (firstError && admitted.length < prepared.length) {
|
|
729
|
+
diag('有图片没能进来(已跳过,其余保留):' + firstError)
|
|
730
|
+
}
|
|
731
|
+
return admitted
|
|
732
|
+
}
|
|
733
|
+
|
|
547
734
|
export function admitImagesFor(ctx) {
|
|
548
735
|
return async function admitImages(list) {
|
|
549
736
|
const attachments = ctx.attachments || (typeof ctx.get === 'function' ? ctx.get('attachments') : null)
|
|
550
737
|
if (!attachments || typeof attachments.admitPromptContent !== 'function') return []
|
|
551
738
|
const prepared = []
|
|
552
739
|
for (const x of Array.isArray(list) ? list : []) {
|
|
553
|
-
|
|
740
|
+
if (prepared.length >= ADMIT_IMAGE_LIMIT) break
|
|
741
|
+
let bytes = await imageBytes(x.data, x.cfg || loadCfg())
|
|
554
742
|
if (!bytes || !bytes.length) continue
|
|
555
|
-
if (bytes.length > 5 * 1024 * 1024) continue
|
|
556
743
|
const name = String(x.name || 'qq-image.jpg')
|
|
557
|
-
|
|
558
|
-
|
|
744
|
+
// animated 只是提示,type 一律按字节来;猜错会被 DSH 按 IMAGE_TYPE_MISMATCH 整批拒掉。
|
|
745
|
+
let mediaType = sniffImageType(bytes) || (await sharpImageType(bytes))
|
|
746
|
+
if (!mediaType) {
|
|
747
|
+
const converted = await convertImage(bytes)
|
|
748
|
+
if (!converted) {
|
|
749
|
+
diag('图片跳过:认不出格式(' + name + ')')
|
|
750
|
+
continue
|
|
751
|
+
}
|
|
752
|
+
bytes = converted.bytes
|
|
753
|
+
mediaType = converted.mediaType
|
|
754
|
+
}
|
|
755
|
+
if (mediaType === 'image/gif') {
|
|
559
756
|
const frames = await gifFrames(bytes, 3)
|
|
560
757
|
if (frames) {
|
|
561
758
|
for (const f of frames) {
|
|
562
|
-
const frameName = name.replace(/\.[^.]+$/, '') + `-gif-${f.page}.png`
|
|
563
759
|
prepared.push({
|
|
564
760
|
caption: String(x.caption || '') + `(GIF 第 ${f.page}/${f.pages} 帧)`,
|
|
565
|
-
block: {
|
|
761
|
+
block: {
|
|
762
|
+
type: 'image',
|
|
763
|
+
data: f.bytes.toString('base64'),
|
|
764
|
+
mediaType: 'image/png',
|
|
765
|
+
name: name.replace(/\.[^.]+$/, '') + `-gif-${f.page}.png`,
|
|
766
|
+
},
|
|
566
767
|
})
|
|
567
|
-
if (prepared.length >=
|
|
768
|
+
if (prepared.length >= ADMIT_IMAGE_LIMIT) break
|
|
568
769
|
}
|
|
569
|
-
if (prepared.length >= 6) break
|
|
570
770
|
continue
|
|
571
771
|
}
|
|
572
772
|
if (!sharp && !gifWarned) {
|
|
@@ -574,21 +774,15 @@ export function admitImagesFor(ctx) {
|
|
|
574
774
|
diag('GIF 帧拆分不可用:没有加载到 sharp,动图会按静图处理')
|
|
575
775
|
}
|
|
576
776
|
}
|
|
777
|
+
// 大图不在这里预缩放:DSH 自己会把源图归一化到 4MiB,20MiB 以内都能收。
|
|
778
|
+
// 只有 admit 因尺寸被拒时才在 retryImageBlock 里用 sharp 兜底重编码。
|
|
577
779
|
prepared.push({
|
|
578
780
|
caption: String(x.caption || ''),
|
|
579
|
-
block: { type: 'image', data: bytes.toString('base64'), mediaType
|
|
781
|
+
block: { type: 'image', data: bytes.toString('base64'), mediaType, name },
|
|
580
782
|
})
|
|
581
|
-
if (prepared.length >= 6) break
|
|
582
783
|
}
|
|
583
784
|
if (!prepared.length) return []
|
|
584
|
-
|
|
585
|
-
const out = await attachments.admitPromptContent(prepared.map((x) => x.block))
|
|
586
|
-
const blocks = (out || []).filter((x) => x && x.type === 'image')
|
|
587
|
-
return blocks.map((block, i) => ({ caption: prepared[i] ? prepared[i].caption : '', block }))
|
|
588
|
-
} catch (error) {
|
|
589
|
-
diag('图片 admit 失败:' + String((error && error.message) || error))
|
|
590
|
-
return []
|
|
591
|
-
}
|
|
785
|
+
return admitPreparedImages(attachments, prepared)
|
|
592
786
|
}
|
|
593
787
|
}
|
|
594
788
|
|
package/package.json
CHANGED