evolclaw 3.1.10 → 3.1.11
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/CHANGELOG.md +14 -0
- package/README.md +0 -4
- package/dist/channels/aun.js +51 -97
- package/dist/channels/feishu.js +7 -11
- package/dist/channels/wechat.js +8 -2
- package/dist/cli/index.js +19 -5
- package/dist/utils/media-cache.js +40 -1
- package/dist/utils/npm-ops.js +13 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## v3.1.11 (2026-06-04)
|
|
4
|
+
|
|
5
|
+
### Improvements
|
|
6
|
+
|
|
7
|
+
- **统一入站图片识别** — 新增 `bufferToInboundImage()`,AUN/飞书/微信共用 magic-bytes → 元数据 → 后缀判定链,消除各通道重复实现的 `detectImageMime`
|
|
8
|
+
- **AUN 附件处理重构** — 抽出 `processAttachments()`,私聊/群聊统一处理;图片注入视觉通道不再追加冗余 `[文件: …]` 文本行
|
|
9
|
+
- **入站去抖** — bridge 默认 inbound debounce 2s → 0(消息即处理)
|
|
10
|
+
- **Windows npm 安装** — `npmInstallGlobal` 改用 `cmd /c`,消除 Node 22 的 `shell:true` 弃用警告
|
|
11
|
+
|
|
12
|
+
### Bug Fixes
|
|
13
|
+
|
|
14
|
+
- **截断图片缓冲** — `validateImage` 捕获 image-type 的 EndOfStreamError,避免短/截断 buffer 抛异常
|
|
15
|
+
- **evolclaw-web 退出容错** — `evolclaw web` 容忍前台进程信号终止(SIGINT/SIGKILL)/非零退出,不再向用户打印堆栈
|
|
16
|
+
|
|
3
17
|
## v3.1.10 (2026-06-04)
|
|
4
18
|
|
|
5
19
|
### Bug Fixes
|
package/README.md
CHANGED
|
@@ -13,7 +13,6 @@ EvolClaw 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
|
|
|
13
13
|
- 👥 **双模式会话**:多用户私聊会话隔离,群聊会话共享,满足不同协作场景
|
|
14
14
|
- 🌐 **多渠道接入**:Channel Adapter 模式,飞书 + 微信 + 钉钉 + QQ频道 + 企业微信 + AUN 网络
|
|
15
15
|
- 🤖 **Agent 间互联**:通过 AUN 网络,你的 Agent 可被其他 Agent 发现和调用
|
|
16
|
-
- 🖥️ **终端 TUI 客户端**:`evolclaw tui` 直接在终端与远程 Agent 对话,无需 IM
|
|
17
16
|
- 🔐 **分层权限**:三级权限体系(user/admin/owner),多用户安全隔离
|
|
18
17
|
- 🛠️ **Agent 自管理**:Agent 可通过 CLI 命令自主管理运行时(查看状态、切换模型、调整配置等)
|
|
19
18
|
- 📦 **项目搬家**:`evolclaw mv` 一键迁移项目目录,保留 Claude/Codex/EvolClaw 全部会话历史
|
|
@@ -29,7 +28,6 @@ EvolClaw 是一个轻量级 AI Agent 网关系统。它为 Claude Code / Codex
|
|
|
29
28
|
|
|
30
29
|
- **通勤路上**:手机打开飞书,继续昨晚的代码 review,到公司无缝切回终端
|
|
31
30
|
- **会议间隙**:微信快速问一句「这个接口的返回格式是什么」,Agent 直接查代码回复
|
|
32
|
-
- **终端直连**:`evolclaw tui` 在任意终端直接与远程 Agent 对话,无需打开 IM
|
|
33
31
|
- **Agent 协作**:通过 AUN 网络,让你的 Agent 被其他 Agent 调用,组成分布式协作
|
|
34
32
|
- **外出离开工位**:不带电脑也能通过 IM 给 Agent 下达任务,回来看结果
|
|
35
33
|
- **团队协作**:拉个飞书群,成员共享同一个 Agent 会话,一起讨论和调试
|
|
@@ -170,7 +168,6 @@ evolclaw stop # 停止服务
|
|
|
170
168
|
evolclaw restart # 重启服务
|
|
171
169
|
evolclaw status # 查看状态
|
|
172
170
|
evolclaw logs # 查看日志(tail -f)
|
|
173
|
-
evolclaw tui # 启动 AUN TUI 终端客户端
|
|
174
171
|
evolclaw agent # 管理 EvolAgent(list / show / new / reload)
|
|
175
172
|
evolclaw mv <old> <new> # 项目搬家(保留全部会话)
|
|
176
173
|
evolclaw diagnose # 诊断启动环境
|
|
@@ -281,7 +278,6 @@ evolclaw/
|
|
|
281
278
|
## TODO
|
|
282
279
|
|
|
283
280
|
- [x] AUN Mesh 网络通道接入
|
|
284
|
-
- [x] TUI 终端客户端(`evolclaw tui`)
|
|
285
281
|
- [x] 项目搬家工具(`evolclaw mv`)
|
|
286
282
|
- [x] 手动授权支持(文本回复 + 飞书卡片)
|
|
287
283
|
- [x] 自动授权可配置(自动放行/自动拒绝)
|
package/dist/channels/aun.js
CHANGED
|
@@ -7,7 +7,7 @@ import { logger, localTimestamp } from '../utils/logger.js';
|
|
|
7
7
|
import { LogWriter } from '../utils/log-writer.js';
|
|
8
8
|
import { normalizeChannelInstances, getChannelShowActivities } from '../utils/channel-helpers.js';
|
|
9
9
|
import { resolvePaths, getPackageRoot, agentMdPath as agentMdPathFn, agentDir as agentDirPath, resolveRoot } from '../paths.js';
|
|
10
|
-
import { saveToUploads, sanitizeFileName } from '../utils/media-cache.js';
|
|
10
|
+
import { saveToUploads, sanitizeFileName, bufferToInboundImage } from '../utils/media-cache.js';
|
|
11
11
|
import { appendAidEvent } from '../utils/instance-registry.js';
|
|
12
12
|
import { appendMessageLog, buildOutboundEntry } from '../core/message/message-log.js';
|
|
13
13
|
import { chatDirPath } from '../core/session/session-fs-store.js';
|
|
@@ -848,43 +848,55 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
848
848
|
}
|
|
849
849
|
// ── Event handlers ──────────────────────────────────────────
|
|
850
850
|
/**
|
|
851
|
-
*
|
|
852
|
-
*
|
|
851
|
+
* 统一处理入站附件:下载 → 图片识别+base64 注入 → 拼接文本。
|
|
852
|
+
*
|
|
853
|
+
* - 图片:base64 注入视觉通道(不再追加 [文件: …] 文本行,避免冗余)
|
|
854
|
+
* - 非图片:拼 [文件: name → path],并提示用 Read 工具读取
|
|
855
|
+
*
|
|
856
|
+
* @param baseText 已解析的正文(私聊 text / 群聊 strippedText)
|
|
857
|
+
* @param channelId 下载归属(私聊 fromAid / 群聊 groupId)
|
|
858
|
+
* @param preCollected 已收集的附件(群聊路径会提前 collect,避免重复)
|
|
853
859
|
*/
|
|
854
|
-
|
|
855
|
-
const
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
860
|
+
async processAttachments(payload, baseText, channelId, preCollected) {
|
|
861
|
+
const rawAttachments = preCollected ?? this.collectAllAttachments(payload);
|
|
862
|
+
const images = [];
|
|
863
|
+
let finalText = baseText;
|
|
864
|
+
if (rawAttachments.length === 0 || !this.client) {
|
|
865
|
+
return { finalText, images };
|
|
866
|
+
}
|
|
867
|
+
const fileParts = [];
|
|
868
|
+
for (const att of rawAttachments) {
|
|
869
|
+
const filePath = await this.downloadAttachment(att, channelId);
|
|
870
|
+
if (!filePath)
|
|
871
|
+
continue;
|
|
872
|
+
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
873
|
+
let img = null;
|
|
874
|
+
try {
|
|
875
|
+
const { readFileSync } = await import('node:fs');
|
|
876
|
+
img = await bufferToInboundImage(readFileSync(filePath), {
|
|
877
|
+
contentType: att.content_type, mimeType: att.mime_type, filename: name,
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
catch { /* read failed, treat as non-image file */ }
|
|
881
|
+
if (img) {
|
|
882
|
+
images.push(img);
|
|
883
|
+
// 图片已注入视觉通道,不追加 [文件: …] 文本行
|
|
884
|
+
}
|
|
885
|
+
else {
|
|
886
|
+
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
const parts = [];
|
|
890
|
+
if (baseText)
|
|
891
|
+
parts.push(baseText);
|
|
892
|
+
if (fileParts.length > 0) {
|
|
893
|
+
parts.push(...fileParts);
|
|
894
|
+
parts.push('请使用 Read 工具读取文件内容。');
|
|
895
|
+
}
|
|
896
|
+
if (parts.length > 0)
|
|
897
|
+
finalText = parts.join('\n\n');
|
|
898
|
+
logger.info(`${this.logPrefix()} [attachments] count=${rawAttachments.length} images=${images.length} files=${fileParts.length}`);
|
|
899
|
+
return { finalText, images };
|
|
888
900
|
}
|
|
889
901
|
async downloadAttachment(att, channelId) {
|
|
890
902
|
const ownerAid = att.owner_aid || this._aid || '';
|
|
@@ -974,37 +986,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
974
986
|
mentions.push(this._aid);
|
|
975
987
|
}
|
|
976
988
|
// Process attachments (顶层 + 嵌套在 merge.items / quote.quote 中的)
|
|
977
|
-
const
|
|
978
|
-
let finalText = text;
|
|
979
|
-
const inboundImages = [];
|
|
980
|
-
if (rawAttachments.length > 0 && this.client) {
|
|
981
|
-
const fileParts = [];
|
|
982
|
-
for (const att of rawAttachments) {
|
|
983
|
-
const filePath = await this.downloadAttachment(att, fromAid);
|
|
984
|
-
if (filePath) {
|
|
985
|
-
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
986
|
-
const mime = this.detectImageMime(att, filePath);
|
|
987
|
-
if (mime) {
|
|
988
|
-
try {
|
|
989
|
-
const { readFileSync } = await import('node:fs');
|
|
990
|
-
inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
|
|
991
|
-
}
|
|
992
|
-
catch { /* fallback to file path */ }
|
|
993
|
-
}
|
|
994
|
-
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
995
|
-
}
|
|
996
|
-
}
|
|
997
|
-
if (fileParts.length > 0) {
|
|
998
|
-
const parts = [];
|
|
999
|
-
if (text)
|
|
1000
|
-
parts.push(text);
|
|
1001
|
-
parts.push(...fileParts);
|
|
1002
|
-
if (inboundImages.length === 0)
|
|
1003
|
-
parts.push('请使用 Read 工具读取文件内容。');
|
|
1004
|
-
finalText = parts.join('\n\n');
|
|
1005
|
-
}
|
|
1006
|
-
logger.info(`${this.logPrefix()} [img-debug] private attachments=${rawAttachments.length} images=${inboundImages.length}`);
|
|
1007
|
-
}
|
|
989
|
+
const { finalText, images: inboundImages } = await this.processAttachments(payload, text, fromAid);
|
|
1008
990
|
// 私聊 channelId = 对端 AID(不再读 payload.chat_id 含 device 三段式)
|
|
1009
991
|
// device_id 仅 SDK 内部多实例去重用,evolclaw session 层面跨端共享会话
|
|
1010
992
|
const chatId = fromAid;
|
|
@@ -1210,35 +1192,7 @@ EvolClaw AI Agent 网关,支持多项目会话管理和多 AI 后端切换。
|
|
|
1210
1192
|
? ['all']
|
|
1211
1193
|
: mentionedSelf && this._aid ? [this._aid] : [];
|
|
1212
1194
|
// Process attachments
|
|
1213
|
-
|
|
1214
|
-
const inboundImages = [];
|
|
1215
|
-
if (hasAttachments && this.client) {
|
|
1216
|
-
const fileParts = [];
|
|
1217
|
-
for (const att of rawAttachments) {
|
|
1218
|
-
const filePath = await this.downloadAttachment(att, groupId);
|
|
1219
|
-
if (filePath) {
|
|
1220
|
-
const name = sanitizeFileName(att.filename || att.object_key?.split('/').pop() || 'file');
|
|
1221
|
-
const mime = this.detectImageMime(att, filePath);
|
|
1222
|
-
if (mime) {
|
|
1223
|
-
try {
|
|
1224
|
-
const { readFileSync } = await import('node:fs');
|
|
1225
|
-
inboundImages.push({ data: readFileSync(filePath).toString('base64'), mimeType: mime });
|
|
1226
|
-
}
|
|
1227
|
-
catch { /* fallback to file path */ }
|
|
1228
|
-
}
|
|
1229
|
-
fileParts.push(`[文件: ${name} → ${filePath}]`);
|
|
1230
|
-
}
|
|
1231
|
-
}
|
|
1232
|
-
if (fileParts.length > 0) {
|
|
1233
|
-
const parts = [];
|
|
1234
|
-
if (strippedText)
|
|
1235
|
-
parts.push(strippedText);
|
|
1236
|
-
parts.push(...fileParts);
|
|
1237
|
-
if (inboundImages.length === 0)
|
|
1238
|
-
parts.push('请使用 Read 工具读取文件内容。');
|
|
1239
|
-
finalText = parts.join('\n\n');
|
|
1240
|
-
}
|
|
1241
|
-
}
|
|
1195
|
+
const { finalText, images: inboundImages } = await this.processAttachments(payload, strippedText, groupId, rawAttachments);
|
|
1242
1196
|
const selfAgentDir = path.join(resolvePaths().agentsDir, this.config.aid);
|
|
1243
1197
|
const peerIdentity = await PeerIdentityCache.resolve('aun', senderAid, selfAgentDir, this.store, false);
|
|
1244
1198
|
const shortAid = this.getShortAid(senderAid);
|
package/dist/channels/feishu.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import imageType from 'image-type';
|
|
4
|
-
import { sanitizeFileName, saveToUploads,
|
|
4
|
+
import { sanitizeFileName, saveToUploads, bufferToInboundImage } from '../utils/media-cache.js';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
6
|
import { hasRichContent, renderAllRichContent, checkDependencies } from '../utils/rich-content-renderer.js';
|
|
7
7
|
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
@@ -789,18 +789,14 @@ export class FeishuChannel {
|
|
|
789
789
|
logger.warn('[Feishu] Empty response from image download');
|
|
790
790
|
return null;
|
|
791
791
|
}
|
|
792
|
-
//
|
|
793
|
-
const
|
|
794
|
-
if (
|
|
795
|
-
logger.warn(
|
|
792
|
+
// 统一图片识别 + base64 注入(magic bytes → 元数据 → 后缀)
|
|
793
|
+
const img = await bufferToInboundImage(buffer);
|
|
794
|
+
if (!img) {
|
|
795
|
+
logger.warn('[Feishu] Image validation failed (not a supported image)');
|
|
796
796
|
return null;
|
|
797
797
|
}
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
return {
|
|
801
|
-
data: base64Data,
|
|
802
|
-
mimeType: result.mime
|
|
803
|
-
};
|
|
798
|
+
logger.debug('[Feishu] Image downloaded successfully, type:', img.mimeType, 'size:', img.data.length);
|
|
799
|
+
return img;
|
|
804
800
|
}
|
|
805
801
|
logger.error('[Feishu] Image download failed: no valid method');
|
|
806
802
|
return null;
|
package/dist/channels/wechat.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from 'fs';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import { resolvePaths } from '../paths.js';
|
|
5
5
|
import { logger } from '../utils/logger.js';
|
|
6
|
-
import { sanitizeFileName, saveToUploads, safeFetch } from '../utils/media-cache.js';
|
|
6
|
+
import { sanitizeFileName, saveToUploads, safeFetch, bufferToInboundImage } from '../utils/media-cache.js';
|
|
7
7
|
import { markdownToPlainText } from '../utils/rich-content-renderer.js';
|
|
8
8
|
import { formatItemsAsText } from '../core/message/items-formatter.js';
|
|
9
9
|
const CHANNEL_VERSION = '1.0.0';
|
|
@@ -527,7 +527,13 @@ export class WechatChannel {
|
|
|
527
527
|
try {
|
|
528
528
|
if (item.type === MSG_ITEM_IMAGE && item.image_item?.media) {
|
|
529
529
|
const buf = await downloadMedia(item.image_item.media, item.image_item.aeskey);
|
|
530
|
-
|
|
530
|
+
// 统一图片识别:magic bytes 优先正确区分 jpeg/png/gif/webp;
|
|
531
|
+
// 检测失败时回退到 image/jpeg(微信入站图片实际均为 jpeg,保留历史行为)。
|
|
532
|
+
const img = await bufferToInboundImage(buf, { contentType: 'image/jpeg' });
|
|
533
|
+
if (img)
|
|
534
|
+
images.push(img);
|
|
535
|
+
else
|
|
536
|
+
logger.warn('[WeChat] Image validation failed (not a supported image)');
|
|
531
537
|
}
|
|
532
538
|
else if (item.type === MSG_ITEM_FILE && item.file_item?.media) {
|
|
533
539
|
const buf = await downloadMedia(item.file_item.media);
|
package/dist/cli/index.js
CHANGED
|
@@ -2127,13 +2127,27 @@ async function cmdWatchWeb() {
|
|
|
2127
2127
|
}
|
|
2128
2128
|
// Node 18.20+/20+/22 起,execFile 拒绝直接 spawn .cmd/.bat(CVE-2024-27980),必须 shell:true。
|
|
2129
2129
|
// shell 模式下含空格的路径/参数需加引号。
|
|
2130
|
+
// evolclaw-web 是前台长驻服务:用户 Ctrl-C、被新实例的单实例保护 SIGKILL、或正常退出,
|
|
2131
|
+
// execFileSync 都会抛错(signal 终止时 status=null)。这些都是正常生命周期,
|
|
2132
|
+
// 不应让父进程 evolclaw 带堆栈崩溃。只有真正的非信号失败才提示。
|
|
2130
2133
|
const isBatch = /\.(cmd|bat)$/i.test(exe);
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
+
try {
|
|
2135
|
+
if (isBatch) {
|
|
2136
|
+
const q = (s) => `"${s}"`;
|
|
2137
|
+
execFileSync(q(exe), ['--home', q(home)], { stdio: 'inherit', shell: true });
|
|
2138
|
+
}
|
|
2139
|
+
else {
|
|
2140
|
+
execFileSync(exe, ['--home', home], { stdio: 'inherit' });
|
|
2141
|
+
}
|
|
2134
2142
|
}
|
|
2135
|
-
|
|
2136
|
-
|
|
2143
|
+
catch (e) {
|
|
2144
|
+
// 信号终止(SIGINT/SIGTERM/SIGKILL)= 用户主动退出或被新实例顶替,静默返回
|
|
2145
|
+
if (e?.signal)
|
|
2146
|
+
return;
|
|
2147
|
+
// 退出码非 0 但非信号:可能是启动失败,提示但不崩溃
|
|
2148
|
+
if (typeof e?.status === 'number' && e.status !== 0) {
|
|
2149
|
+
process.stderr.write(`⚠ evolclaw-web 退出(code ${e.status})\n`);
|
|
2150
|
+
}
|
|
2137
2151
|
}
|
|
2138
2152
|
}
|
|
2139
2153
|
async function cmdRestartMonitor() {
|
|
@@ -85,7 +85,14 @@ export async function validateImage(buffer, opts) {
|
|
|
85
85
|
}
|
|
86
86
|
// 动态导入 image-type(ESM only)
|
|
87
87
|
const { default: imageType } = await import('image-type');
|
|
88
|
-
|
|
88
|
+
// image-type 对极短/截断的 buffer 会抛 EndOfStreamError,捕获后按「无法识别」处理。
|
|
89
|
+
let type;
|
|
90
|
+
try {
|
|
91
|
+
type = await imageType(buffer);
|
|
92
|
+
}
|
|
93
|
+
catch (e) {
|
|
94
|
+
return { mime: null, reason: `Image type detection error: ${e.message}` };
|
|
95
|
+
}
|
|
89
96
|
if (!type) {
|
|
90
97
|
return { mime: null, reason: 'Unable to detect image type' };
|
|
91
98
|
}
|
|
@@ -94,6 +101,38 @@ export async function validateImage(buffer, opts) {
|
|
|
94
101
|
}
|
|
95
102
|
return { mime: type.mime };
|
|
96
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* 把已下载的附件 Buffer 转成入站图片条目(供 baseagent 视觉通道)。
|
|
106
|
+
*
|
|
107
|
+
* 适用于所有「需要先下载再注入」的通道(AUN ticket 下载、未来其它 CDN 下载等)。
|
|
108
|
+
* 飞书/微信已有各自的 SDK 下载路径,可按需复用本函数统一 MIME 判定。
|
|
109
|
+
*
|
|
110
|
+
* 判定优先级:
|
|
111
|
+
* 1. magic bytes(image-type 库,最可靠,同时做大小/白名单校验)
|
|
112
|
+
* 2. 元数据 MIME 字段(att.content_type / mime_type / mimeType)
|
|
113
|
+
* 3. 文件名后缀
|
|
114
|
+
*
|
|
115
|
+
* @returns 是图片返回 InboundImage;非图片或校验失败返回 null。
|
|
116
|
+
*/
|
|
117
|
+
export async function bufferToInboundImage(buffer, hints) {
|
|
118
|
+
// 1. magic bytes(含大小 + 白名单校验;validateImage 已吞掉 image-type 的异常)
|
|
119
|
+
const validated = await validateImage(buffer);
|
|
120
|
+
if (validated.mime) {
|
|
121
|
+
return { data: buffer.toString('base64'), mimeType: validated.mime };
|
|
122
|
+
}
|
|
123
|
+
// 2/3. magic bytes 未识别时,回退到元数据字段 / 文件名后缀
|
|
124
|
+
// (仍受 image 白名单约束,避免把任意文件当图片注入)
|
|
125
|
+
const metaCt = hints?.contentType || hints?.mimeType || '';
|
|
126
|
+
const byMeta = typeof metaCt === 'string' && ALLOWED_IMAGE_MIMES.has(metaCt) ? metaCt : '';
|
|
127
|
+
const byExt = hints?.filename
|
|
128
|
+
? (ALLOWED_IMAGE_MIMES.has(guessMime(hints.filename)) ? guessMime(hints.filename) : '')
|
|
129
|
+
: '';
|
|
130
|
+
const fallback = byMeta || byExt;
|
|
131
|
+
if (fallback && buffer.length > 0 && buffer.length <= DEFAULT_MAX_IMAGE_SIZE) {
|
|
132
|
+
return { data: buffer.toString('base64'), mimeType: fallback };
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
97
136
|
/**
|
|
98
137
|
* 保存 Buffer 到 uploads 目录
|
|
99
138
|
* - 自动创建目录
|
package/dist/utils/npm-ops.js
CHANGED
|
@@ -15,10 +15,20 @@ import { isWindows } from './cross-platform.js';
|
|
|
15
15
|
const execFileAsync = promisify(execFile);
|
|
16
16
|
// ── npm install -g (shared) ────────────────────────────────────────────────
|
|
17
17
|
export async function npmInstallGlobal(pkg) {
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
let cmd;
|
|
19
|
+
let args;
|
|
20
|
+
// Windows: run via cmd /c to avoid shell:true deprecation warning on Node 22.
|
|
21
|
+
// Unix: npm directly, no shell needed.
|
|
22
|
+
if (isWindows) {
|
|
23
|
+
cmd = 'cmd';
|
|
24
|
+
args = ['/c', 'npm', 'install', '-g', pkg];
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
cmd = 'npm';
|
|
28
|
+
args = ['install', '-g', pkg];
|
|
29
|
+
}
|
|
20
30
|
try {
|
|
21
|
-
await execFileAsync(
|
|
31
|
+
await execFileAsync(cmd, args, { timeout: 180000 });
|
|
22
32
|
}
|
|
23
33
|
catch (e) {
|
|
24
34
|
if (e.stderr?.includes('EACCES') || e.message?.includes('EACCES')) {
|
package/package.json
CHANGED