@xmanrui/dsh-im 4.22.0 → 4.24.0
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.en.md +5 -4
- package/README.md +5 -4
- package/lib/client.js +2584 -1153
- package/lib/index.js +290 -298
- package/package.json +14 -2
- package/plugin-src/client/channel-card-meta.js +2 -0
- package/plugin-src/client/channel-logos.js +11 -0
- package/plugin-src/client/channels/dingtalk/api.js +5 -1
- package/plugin-src/client/channels/dingtalk/index.js +15 -13
- package/plugin-src/client/channels/email/api.js +52 -0
- package/plugin-src/client/channels/email/index.js +728 -0
- package/plugin-src/client/channels/email/styles.js +51 -0
- package/plugin-src/client/channels/feishu/api.js +4 -2
- package/plugin-src/client/channels/feishu/index.js +23 -7
- package/plugin-src/client/channels/imessage/index.js +2 -1
- package/plugin-src/client/channels/office/api.js +2 -0
- package/plugin-src/client/channels/office/index.js +6 -5
- package/plugin-src/client/channels/qq/api.js +7 -0
- package/plugin-src/client/channels/qq/index.js +16 -3
- package/plugin-src/client/channels/shared/token-api.js +8 -1
- package/plugin-src/client/channels/shared/token-channel.js +28 -3
- package/plugin-src/client/channels/slack/index.js +2 -1
- package/plugin-src/client/channels/telegram/index.js +3 -0
- package/plugin-src/client/channels/telegram/styles.js +12 -0
- package/plugin-src/client/channels/telegram/thinking-traces.js +25 -0
- package/plugin-src/client/channels/wecom/api.js +7 -1
- package/plugin-src/client/channels/wecom/index.js +16 -3
- package/plugin-src/client/channels/wecom-app/api.js +6 -1
- package/plugin-src/client/channels/wecom-app/index.js +16 -3
- package/plugin-src/client/channels/weixin/api.js +2 -1
- package/plugin-src/client/channels/weixin/connection-error.js +1 -71
- package/plugin-src/client/channels/whatsapp/api.js +5 -0
- package/plugin-src/client/channels/whatsapp/index.js +16 -3
- package/plugin-src/client/connection-error.js +87 -0
- package/plugin-src/client/global-settings.js +81 -1
- package/plugin-src/client/i18n.js +69 -1
- package/plugin-src/client/index.js +40 -4
- package/plugin-src/client/last-message-error.js +2 -1
- package/plugin-src/client/styles.js +8 -2
- package/plugin-src/host/build.mjs +4 -0
- package/plugin-src/host/channels/dingtalk/production.mjs +2 -0
- package/plugin-src/host/channels/dingtalk/rpc.mjs +7 -4
- package/plugin-src/host/channels/email/availability.mjs +60 -0
- package/plugin-src/host/channels/email/index.mjs +36 -0
- package/plugin-src/host/channels/email/production.mjs +58 -0
- package/plugin-src/host/channels/email/rpc.mjs +163 -0
- package/plugin-src/host/channels/feishu/production.mjs +3 -0
- package/plugin-src/host/channels/feishu/rpc.mjs +7 -4
- package/plugin-src/host/channels/imessage/rpc.mjs +13 -6
- package/plugin-src/host/channels/office/rpc.mjs +3 -1
- package/plugin-src/host/channels/qq/production.mjs +2 -0
- package/plugin-src/host/channels/qq/rpc.mjs +5 -3
- package/plugin-src/host/channels/shared/production.mjs +52 -5
- package/plugin-src/host/channels/shared/rpc.mjs +14 -3
- package/plugin-src/host/channels/shared/startup-error.mjs +4 -3
- package/plugin-src/host/channels/shared/startup.mjs +11 -6
- package/plugin-src/host/channels/shared/thinking-traces-rpc.mjs +11 -0
- package/plugin-src/host/channels/slack/production.mjs +2 -0
- package/plugin-src/host/channels/slack/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom/production.mjs +2 -0
- package/plugin-src/host/channels/wecom/rpc.mjs +5 -3
- package/plugin-src/host/channels/wecom-app/production.mjs +2 -0
- package/plugin-src/host/channels/wecom-app/rpc.mjs +5 -3
- package/plugin-src/host/channels/weixin/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/production.mjs +2 -0
- package/plugin-src/host/channels/whatsapp/rpc.mjs +5 -3
- package/plugin-src/host/delivery-adapter.mjs +11 -0
- package/plugin-src/host/image-input-rpc.mjs +24 -0
- package/plugin-src/host/inbound-ttl-rpc.mjs +6 -1
- package/plugin-src/host/index.mjs +3 -0
- package/plugin-src/host/modern-harness-api.mjs +7 -2
- package/plugin-src/management-rpc.mjs +12 -2
- package/scripts/verify-package.mjs +13 -5
- package/src/channels/dingtalk/connection-error.mjs +5 -12
- package/src/channels/dingtalk/device-auth.mjs +4 -1
- package/src/channels/dingtalk/dingtalk-bridge.mjs +6 -4
- package/src/channels/dingtalk/dingtalk-controller.mjs +60 -45
- package/src/channels/dingtalk/dingtalk-runtime.mjs +20 -4
- package/src/channels/discord/discord-api.mjs +3 -3
- package/src/channels/discord/discord-runtime.mjs +18 -12
- package/src/channels/email/config-store.mjs +194 -0
- package/src/channels/email/email-api.mjs +20 -0
- package/src/channels/email/email-bridge.mjs +17 -0
- package/src/channels/email/email-controller.mjs +824 -0
- package/src/channels/email/email-runtime.mjs +699 -0
- package/src/channels/email/harness-client.mjs +7 -0
- package/src/channels/email/mail-format.mjs +91 -0
- package/src/channels/email/state-store.mjs +176 -0
- package/src/channels/email/transport.mjs +64 -0
- package/src/channels/email/transports/agent-mail.mjs +604 -0
- package/src/channels/email/transports/agently-cli.mjs +288 -0
- package/src/channels/email/transports/imap-smtp.mjs +165 -0
- package/src/channels/feishu/bridge.mjs +39 -11
- package/src/channels/feishu/feishu-channel.mjs +35 -0
- package/src/channels/feishu/feishu-runtime.mjs +15 -8
- package/src/channels/feishu/group-message-permission-manager.mjs +2 -1
- package/src/channels/feishu/live-cot.mjs +260 -0
- package/src/channels/feishu/message-utils.mjs +3 -2
- package/src/channels/feishu/multi-bot-controller.mjs +56 -47
- package/src/channels/feishu/registration-manager.mjs +8 -4
- package/src/channels/feishu/repair-manager.mjs +2 -1
- package/src/channels/feishu/slash-command-registry.mjs +17 -0
- package/src/channels/feishu/step-push-mode.mjs +10 -4
- package/src/channels/imessage/runtime.mjs +9 -5
- package/src/channels/office/office-controller.mjs +23 -12
- package/src/channels/office/office-runtime.mjs +6 -3
- package/src/channels/office/office-transport.mjs +1 -0
- package/src/channels/qq/qq-bridge.mjs +7 -5
- package/src/channels/qq/qq-controller.mjs +56 -42
- package/src/channels/qq/qq-runtime.mjs +16 -11
- package/src/channels/shared/bot-workspace-store.mjs +29 -10
- package/src/channels/shared/connection-error.mjs +191 -0
- package/src/channels/shared/connection-test.mjs +3 -1
- package/src/channels/shared/conversation-state-store.mjs +46 -1
- package/src/channels/shared/diagnostic-details.mjs +95 -0
- package/src/channels/shared/harness-client.mjs +287 -57
- package/src/channels/shared/i18n-en/diagnostics.mjs +55 -0
- package/src/channels/shared/i18n-en/email.mjs +25 -0
- package/src/channels/shared/i18n-en/image-input.mjs +17 -0
- package/src/channels/shared/i18n-en/shared-a.mjs +1 -0
- package/src/channels/shared/i18n-en/shared-c.mjs +1 -1
- package/src/channels/shared/i18n-en.mjs +6 -0
- package/src/channels/shared/image-input-policy.mjs +35 -0
- package/src/channels/shared/image-input-settings-store.mjs +60 -0
- package/src/channels/shared/image-input.mjs +124 -0
- package/src/channels/shared/image-prompt.mjs +15 -7
- package/src/channels/shared/message-failure.mjs +12 -0
- package/src/channels/shared/semantic/reply-reference.mjs +3 -3
- package/src/channels/shared/text-harness-bridge.mjs +150 -25
- package/src/channels/shared/token-bot-controller.mjs +52 -47
- package/src/channels/slack/slack-api.mjs +3 -3
- package/src/channels/slack/slack-controller.mjs +59 -51
- package/src/channels/slack/slack-runtime.mjs +18 -12
- package/src/channels/telegram/config-store.mjs +4 -1
- package/src/channels/telegram/telegram-api.mjs +2 -2
- package/src/channels/telegram/telegram-controller.mjs +13 -1
- package/src/channels/telegram/telegram-runtime.mjs +215 -14
- package/src/channels/wecom/wecom-bridge.mjs +15 -6
- package/src/channels/wecom/wecom-controller.mjs +51 -37
- package/src/channels/wecom/wecom-runtime.mjs +10 -6
- package/src/channels/wecom-app/wecom-app-bridge.mjs +7 -5
- package/src/channels/wecom-app/wecom-app-controller.mjs +50 -37
- package/src/channels/wecom-app/wecom-app-runtime.mjs +2 -1
- package/src/channels/weixin/connection-error.mjs +26 -92
- package/src/channels/weixin/diagnostic-details.mjs +1 -63
- package/src/channels/weixin/weixin-bridge.mjs +7 -5
- package/src/channels/whatsapp/whatsapp-controller.mjs +34 -17
- package/src/channels/whatsapp/whatsapp-runtime.mjs +35 -8
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
"图片原文件已保存到工作区,文件清单中的图片序号与本条消息一致。图片内容块可能经过缩放或压缩;需要查看细节时可用工具读取原文件。": "Original images are saved in the workspace. Image numbers in the file list match this message. Image blocks may be resized or compressed; use tools to read originals for details.",
|
|
3
|
+
"部分图片未直接提供给模型,图片序号:{indexes}。原文件已保存,请使用可用工具读取或分析;不要假设自己已看到这些图片。": "Some images were not provided directly to the model: {indexes}. Originals are saved; use available tools to read or analyze them. Do not assume you have seen these images.",
|
|
4
|
+
"宿主拒绝了本次图片输入。原图已保存到工作区,请根据文件清单使用可用工具读取或分析,不要假设自己已看到图片。": "The Host rejected the image input. Originals are saved in the workspace; use the file list and available tools to read or analyze them. Do not assume you have seen the images.",
|
|
5
|
+
"图片超过原图接收上限 {maxMb} MB,请压缩后重试,或在通用设置的附件中调整上限。": "The image exceeds the original-image receipt limit of {maxMb} MB. Compress it and retry, or adjust the limit in General settings > Attachments.",
|
|
6
|
+
"图片 {index}(原图文件:{name})": "Image {index} (original file: {name})",
|
|
7
|
+
"图片限制无效:请输入正整数,原图接收上限和总量上限不能小于单图上限。": "Invalid image limits: enter positive integers. The receipt and total limits must be at least the per-image limit.",
|
|
8
|
+
"无法读取或保存图片设置,请稍后重试。": "Could not read or save image settings. Try again later.",
|
|
9
|
+
"原图接收上限 (MB)": "Original-image receipt limit (MB)",
|
|
10
|
+
"模型单图上限 (MB)": "Model per-image limit (MB)",
|
|
11
|
+
"模型图片总量上限 (MB)": "Model total image limit (MB)",
|
|
12
|
+
"每条消息最多图片数": "Maximum images per message",
|
|
13
|
+
"图片输入": "Image input",
|
|
14
|
+
"适用于支持图片的聊天渠道。原图按附件保留时长保存,发送给模型的副本会自动缩放或压缩;无法直接发送时交给模型按文件处理。": "Applies to chat channels that support images. Originals follow attachment retention settings. Model copies are resized or compressed automatically; images that cannot be sent directly are provided as files.",
|
|
15
|
+
"保存图片设置": "Save image settings",
|
|
16
|
+
"正在读取图片设置…": "Loading image settings…"
|
|
17
|
+
};
|
|
@@ -3,6 +3,7 @@ export default {
|
|
|
3
3
|
'这个问题已在其他客户端处理,无需再次回答。':
|
|
4
4
|
'This question has already been answered from another client; no further reply is needed.',
|
|
5
5
|
'任务已完成。': 'Task completed.',
|
|
6
|
+
'本轮处理已结束,没有文本回复。': 'This turn has ended with no text reply.',
|
|
6
7
|
'结果文件': 'result file',
|
|
7
8
|
'结果文件「{name}」的发送结果未能确认,请先检查聊天内是否已收到,不要立即重试。':
|
|
8
9
|
'The delivery result of the result file "{name}" could not be confirmed. Please check whether it already arrived in the chat before retrying.',
|
|
@@ -8,7 +8,7 @@ export default {
|
|
|
8
8
|
'{channel}配置格式错误。请检查其数据目录中的 config.json 和 workspaces.json,修复后重启 DSH。详细原因请查看启动日志。': 'The {channel} configuration is invalid. Check config.json and workspaces.json in its data directory, then restart DSH. See the startup log for details.',
|
|
9
9
|
'{channel}配置格式错误。请检查其数据目录中的 config.json,修复后重启 DSH。详细原因请查看启动日志。': 'The {channel} configuration is invalid. Check config.json in its data directory, then restart DSH. See the startup log for details.',
|
|
10
10
|
'无法读取或写入{channel}配置。请检查其数据目录的访问权限,修复后重启 DSH。详细原因请查看启动日志。': 'The {channel} configuration could not be read or written. Check access to its data directory, then restart DSH. See the startup log for details.',
|
|
11
|
-
'{channel}
|
|
11
|
+
'{channel}初始化失败。请展开诊断详情,并通过参考号查找 DSH 启动日志,修复后重启 DSH。': '{channel} initialization failed. Expand the diagnostic details and find the reference in the DSH startup log, fix the issue, then restart DSH.',
|
|
12
12
|
// history-command.mjs / command help
|
|
13
13
|
'/history [数量] 查看最近历史消息(默认 3 条,最多 5 条)':
|
|
14
14
|
'/history [count] Preview recent messages (default 3, maximum 5)',
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import imageInput from './i18n-en/image-input.mjs';
|
|
2
|
+
import diagnostics from './i18n-en/diagnostics.mjs';
|
|
1
3
|
// English translations for host-side user-facing text.
|
|
2
4
|
// Keys are the exact Chinese literals passed to t() in src/channels/**.
|
|
3
5
|
// Chinese output is the identity default and needs no entries here.
|
|
@@ -17,9 +19,12 @@ import telegram from './i18n-en/telegram.mjs';
|
|
|
17
19
|
import discord from './i18n-en/discord.mjs';
|
|
18
20
|
import whatsapp from './i18n-en/whatsapp.mjs';
|
|
19
21
|
import office from './i18n-en/office.mjs';
|
|
22
|
+
import email from './i18n-en/email.mjs';
|
|
20
23
|
|
|
21
24
|
export const EN = Object.freeze(Object.assign(
|
|
22
25
|
{},
|
|
26
|
+
diagnostics,
|
|
27
|
+
imageInput,
|
|
23
28
|
sharedA,
|
|
24
29
|
sharedB,
|
|
25
30
|
sharedC,
|
|
@@ -34,4 +39,5 @@ export const EN = Object.freeze(Object.assign(
|
|
|
34
39
|
discord,
|
|
35
40
|
whatsapp,
|
|
36
41
|
office,
|
|
42
|
+
email,
|
|
37
43
|
));
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export const IMAGE_MEGABYTE = 1024 * 1024;
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_IMAGE_INPUT_SETTINGS = Object.freeze({
|
|
4
|
+
maxDownloadMb: 30,
|
|
5
|
+
maxImageMb: 5,
|
|
6
|
+
maxTotalMb: 20,
|
|
7
|
+
maxImages: 20,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
/** Shared by the settings form and Host; reject incomplete or unsafe values. */
|
|
11
|
+
export function normalizeImageInputSettings(value) {
|
|
12
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return null;
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const key of Object.keys(DEFAULT_IMAGE_INPUT_SETTINGS)) {
|
|
15
|
+
const input = value[key];
|
|
16
|
+
const number = typeof input === 'string' && /^\d+$/.test(input.trim())
|
|
17
|
+
? Number(input.trim()) : input;
|
|
18
|
+
if (!Number.isSafeInteger(number) || number < 1
|
|
19
|
+
|| !Number.isSafeInteger(number * IMAGE_MEGABYTE)) return null;
|
|
20
|
+
result[key] = number;
|
|
21
|
+
}
|
|
22
|
+
if (result.maxDownloadMb < result.maxImageMb || result.maxTotalMb < result.maxImageMb) return null;
|
|
23
|
+
return Object.freeze(result);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function imageInputLimits(settings = DEFAULT_IMAGE_INPUT_SETTINGS) {
|
|
27
|
+
const normalized = normalizeImageInputSettings(settings);
|
|
28
|
+
if (!normalized) throw new TypeError('Invalid image input settings');
|
|
29
|
+
return Object.freeze({
|
|
30
|
+
maxDownloadBytes: normalized.maxDownloadMb * IMAGE_MEGABYTE,
|
|
31
|
+
maxImageBytes: normalized.maxImageMb * IMAGE_MEGABYTE,
|
|
32
|
+
maxTotalImageBytes: normalized.maxTotalMb * IMAGE_MEGABYTE,
|
|
33
|
+
maxImages: normalized.maxImages,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { mkdir, readFile, rename, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
|
|
6
|
+
import { DEFAULT_IMAGE_INPUT_SETTINGS, normalizeImageInputSettings } from './image-input-policy.mjs';
|
|
7
|
+
|
|
8
|
+
const stores = new Map();
|
|
9
|
+
|
|
10
|
+
export class ImageInputSettingsStore {
|
|
11
|
+
#path;
|
|
12
|
+
#settings = DEFAULT_IMAGE_INPUT_SETTINGS;
|
|
13
|
+
#ready;
|
|
14
|
+
#writes = Promise.resolve();
|
|
15
|
+
|
|
16
|
+
constructor(path) { this.#path = path; }
|
|
17
|
+
|
|
18
|
+
async get() {
|
|
19
|
+
this.#ready ??= this.#load().catch((error) => { this.#ready = null; throw error; });
|
|
20
|
+
await this.#ready;
|
|
21
|
+
return { ...this.#settings };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async #load() {
|
|
25
|
+
let raw;
|
|
26
|
+
try { raw = await readFile(this.#path, 'utf8'); }
|
|
27
|
+
catch (error) { if (error.code === 'ENOENT') return; throw error; }
|
|
28
|
+
const document = JSON.parse(raw);
|
|
29
|
+
const settings = document.version === 1 && normalizeImageInputSettings(document.imageInput);
|
|
30
|
+
if (!settings) throw new Error('Invalid image input settings document');
|
|
31
|
+
this.#settings = settings;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async set(value) {
|
|
35
|
+
const settings = normalizeImageInputSettings(value);
|
|
36
|
+
if (!settings) throw new TypeError('Invalid image input settings');
|
|
37
|
+
const operation = this.#writes.catch(() => {}).then(async () => {
|
|
38
|
+
await this.get();
|
|
39
|
+
await mkdir(dirname(this.#path), { recursive: true, mode: 0o700 });
|
|
40
|
+
const temporary = `${this.#path}.${randomUUID()}.tmp`;
|
|
41
|
+
try {
|
|
42
|
+
await writeFile(temporary, `${JSON.stringify({ version: 1, imageInput: settings }, null, 2)}\n`,
|
|
43
|
+
{ mode: 0o600, flag: 'wx' });
|
|
44
|
+
await rename(temporary, this.#path);
|
|
45
|
+
this.#settings = settings;
|
|
46
|
+
} finally { await rm(temporary, { force: true }); }
|
|
47
|
+
return { ...settings };
|
|
48
|
+
});
|
|
49
|
+
this.#writes = operation;
|
|
50
|
+
return operation;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One settings owner per DSH home, shared by the UI and channel Harness clients. */
|
|
55
|
+
export function getImageInputSettingsStore(config = {}) {
|
|
56
|
+
const root = resolve(config.dshHome ?? process.env.DSH_HOME ?? join(homedir(), '.dsh'));
|
|
57
|
+
const path = join(root, 'integrations', 'dsh-im', 'image-input-settings.json');
|
|
58
|
+
if (!stores.has(path)) stores.set(path, new ImageInputSettingsStore(path));
|
|
59
|
+
return stores.get(path);
|
|
60
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { basename, join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
detectedImageMediaType, ImagePromptError, imageStorageName, loadedImage, imageDownloadLimitMessage,
|
|
6
|
+
} from './image-prompt.mjs';
|
|
7
|
+
import { t } from './i18n.mjs';
|
|
8
|
+
|
|
9
|
+
const MAX_INPUT_PIXELS = 64_000_000;
|
|
10
|
+
const COMPRESSION_STEPS = [[2048, 80], [1536, 70], [1024, 60]];
|
|
11
|
+
let sharpPromise;
|
|
12
|
+
async function loadSharp() {
|
|
13
|
+
sharpPromise ??= import('sharp').then((module) => module.default).catch(() => null);
|
|
14
|
+
return sharpPromise;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const IMAGE_ORIGINALS_PROMPT = '图片原文件已保存到工作区,文件清单中的图片序号与本条消息一致。图片内容块可能经过缩放或压缩;需要查看细节时可用工具读取原文件。';
|
|
18
|
+
export const IMAGE_INPUT_FALLBACK_PROMPT = '部分图片未直接提供给模型,图片序号:{indexes}。原文件已保存,请使用可用工具读取或分析;不要假设自己已看到这些图片。';
|
|
19
|
+
export const IMAGE_HOST_LIMIT_FALLBACK_PROMPT = '宿主拒绝了本次图片输入。原图已保存到工作区,请根据文件清单使用可用工具读取或分析,不要假设自己已看到图片。';
|
|
20
|
+
|
|
21
|
+
/** Keep provider loaders intact; stage and validate each original once. */
|
|
22
|
+
export function inboundImagesAsFiles(images, limits) {
|
|
23
|
+
if (images.length > limits.maxImages) {
|
|
24
|
+
throw new ImagePromptError('too-many-images', 'Too many inbound images',
|
|
25
|
+
t('一次最多只能处理 {maxImages} 张图片。', { maxImages: limits.maxImages }));
|
|
26
|
+
}
|
|
27
|
+
const tooLarge = () => new ImagePromptError('image-too-large',
|
|
28
|
+
`Image exceeds ${limits.maxDownloadBytes} bytes`, imageDownloadLimitMessage(limits.maxDownloadBytes));
|
|
29
|
+
// Check declared sizes before any download starts, including later images.
|
|
30
|
+
for (const source of images) {
|
|
31
|
+
if (Number.isFinite(source?.size) && source.size > limits.maxDownloadBytes) throw tooLarge();
|
|
32
|
+
}
|
|
33
|
+
return images.map((source, index) => ({
|
|
34
|
+
async load({ signal } = {}) {
|
|
35
|
+
signal?.throwIfAborted();
|
|
36
|
+
let value;
|
|
37
|
+
try {
|
|
38
|
+
value = source.data === undefined
|
|
39
|
+
? await source.load?.({ signal, maxBytes: limits.maxDownloadBytes }) : source.data;
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (signal?.aborted) throw signal.reason ?? error;
|
|
42
|
+
if (error?.code === 'image-too-large') throw tooLarge();
|
|
43
|
+
if (error instanceof ImagePromptError || ['AbortError', 'TimeoutError'].includes(error?.name)) throw error;
|
|
44
|
+
throw new ImagePromptError('image-download-failed', 'Unable to download image',
|
|
45
|
+
t('图片下载失败,请重新发送后再试。'), { cause: error });
|
|
46
|
+
}
|
|
47
|
+
signal?.throwIfAborted();
|
|
48
|
+
const loaded = loadedImage(value);
|
|
49
|
+
if (!loaded?.data.length) throw new ImagePromptError('invalid-image-data', 'Empty image',
|
|
50
|
+
t('未能读取图片内容,请重新发送。'));
|
|
51
|
+
if (loaded.data.length > limits.maxDownloadBytes) throw tooLarge();
|
|
52
|
+
const mediaType = detectedImageMediaType(loaded.data);
|
|
53
|
+
if (!mediaType) throw new ImagePromptError('unsupported-image-type', 'Unsupported image',
|
|
54
|
+
t('暂不支持该图片格式,请发送 JPEG、PNG、WebP 或 GIF 图片。'));
|
|
55
|
+
return { data: loaded.data, mediaType,
|
|
56
|
+
name: `${index + 1}-${imageStorageName(loaded.name ?? source.name, mediaType, index)}` };
|
|
57
|
+
},
|
|
58
|
+
}));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** A missing optional codec still permits small native images and file delivery. */
|
|
62
|
+
export async function modelImageFromOriginal(data, mediaType, maxBytes, {
|
|
63
|
+
signal, sharpLoader = loadSharp,
|
|
64
|
+
} = {}) {
|
|
65
|
+
signal?.throwIfAborted();
|
|
66
|
+
if (maxBytes <= 0) return null;
|
|
67
|
+
const sharp = await sharpLoader();
|
|
68
|
+
signal?.throwIfAborted();
|
|
69
|
+
if (!sharp) return data.length <= maxBytes ? { data, mediaType } : null;
|
|
70
|
+
try {
|
|
71
|
+
const metadata = await sharp(data, { limitInputPixels: MAX_INPUT_PIXELS }).metadata();
|
|
72
|
+
signal?.throwIfAborted();
|
|
73
|
+
if (metadata.width * metadata.height > MAX_INPUT_PIXELS) return null;
|
|
74
|
+
if ((metadata.pages ?? 1) > 1 || mediaType === 'image/gif') {
|
|
75
|
+
return data.length <= maxBytes ? { data, mediaType } : null;
|
|
76
|
+
}
|
|
77
|
+
if (data.length <= maxBytes && metadata.width <= 2048 && metadata.height <= 2048) {
|
|
78
|
+
return { data, mediaType };
|
|
79
|
+
}
|
|
80
|
+
for (const [side, quality] of COMPRESSION_STEPS) {
|
|
81
|
+
signal?.throwIfAborted();
|
|
82
|
+
const operation = sharp(data, { limitInputPixels: MAX_INPUT_PIXELS }).rotate()
|
|
83
|
+
.resize({ width: side, height: side, fit: 'inside', withoutEnlargement: true });
|
|
84
|
+
const output = metadata.hasAlpha
|
|
85
|
+
? await operation.png({ compressionLevel: 9 }).toBuffer()
|
|
86
|
+
: await operation.jpeg({ quality }).toBuffer();
|
|
87
|
+
signal?.throwIfAborted();
|
|
88
|
+
if (output.length <= maxBytes) return { data: output,
|
|
89
|
+
mediaType: metadata.hasAlpha ? 'image/png' : 'image/jpeg' };
|
|
90
|
+
}
|
|
91
|
+
} catch (error) {
|
|
92
|
+
signal?.throwIfAborted();
|
|
93
|
+
// A codec failure cannot discard an original already saved for tools.
|
|
94
|
+
}
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export async function imageContentFromStaged(staged, limits, { signal, sharpLoader } = {}) {
|
|
99
|
+
const content = [];
|
|
100
|
+
const fileOnly = [];
|
|
101
|
+
let remaining = limits.maxTotalImageBytes;
|
|
102
|
+
for (const [index, file] of staged.files.entries()) {
|
|
103
|
+
signal?.throwIfAborted();
|
|
104
|
+
// Staging owns both the directory and basename; never read a user-supplied path.
|
|
105
|
+
const data = await readFile(join(staged.directory, basename(file.path)), { signal });
|
|
106
|
+
const result = await modelImageFromOriginal(data, file.mediaType,
|
|
107
|
+
Math.min(limits.maxImageBytes, remaining), { signal, sharpLoader });
|
|
108
|
+
if (!result) { fileOnly.push(index + 1); continue; }
|
|
109
|
+
remaining -= result.data.length;
|
|
110
|
+
content.push({ type: 'text', text: t('图片 {index}(原图文件:{name})', { index: index + 1, name: file.name }) },
|
|
111
|
+
{ type: 'image', mediaType: result.mediaType, data: result.data.toString('base64'),
|
|
112
|
+
name: imageStorageName(file.name.replace(/\.[^.]+$/, ''), result.mediaType, index) });
|
|
113
|
+
}
|
|
114
|
+
content.push({ type: 'text', text: t(IMAGE_ORIGINALS_PROMPT) });
|
|
115
|
+
if (fileOnly.length) content.push({ type: 'text',
|
|
116
|
+
text: t(IMAGE_INPUT_FALLBACK_PROMPT, { indexes: fileOnly.join(', ') }) });
|
|
117
|
+
return content;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function isImageAdmissionRejection(error) {
|
|
121
|
+
return ['attachment-error', 'session/attachment-invalid'].includes(error?.code)
|
|
122
|
+
&& ['MODEL_DOES_NOT_SUPPORT_IMAGES', 'IMAGE_TOO_LARGE', 'IMAGES_TOO_LARGE',
|
|
123
|
+
'IMAGE_TOO_MANY_PIXELS', 'TOO_MANY_IMAGES'].includes(error?.details?.reason);
|
|
124
|
+
}
|
|
@@ -12,6 +12,11 @@ export const DEFAULT_IMAGE_PROMPT = '请分析这张图片。';
|
|
|
12
12
|
*/
|
|
13
13
|
export const IMAGE_FILE_FALLBACK_PROMPT = '当前会话模型不支持直接接收图片输入。用户发送的图片已作为文件保存到工作区(见下方文件清单)。请使用可用工具分析这些图片文件后回答,例如 run_code 或 pwsh 读取字节、解析元数据、调用图像处理或 OCR 库;不要假设自己能直接看到图片内容。';
|
|
14
14
|
|
|
15
|
+
export function imageDownloadLimitMessage(maxBytes) {
|
|
16
|
+
return t('图片超过原图接收上限 {maxMb} MB,请压缩后重试,或在通用设置的附件中调整上限。',
|
|
17
|
+
{ maxMb: Math.round(maxBytes / (1024 * 1024) * 100) / 100 });
|
|
18
|
+
}
|
|
19
|
+
|
|
15
20
|
export class ImagePromptError extends Error {
|
|
16
21
|
constructor(code, message, userMessage, options = {}) {
|
|
17
22
|
super(message, options);
|
|
@@ -92,7 +97,7 @@ export async function fetchImageBuffer(url, {
|
|
|
92
97
|
throw new ImagePromptError(
|
|
93
98
|
'image-too-large',
|
|
94
99
|
`Image response declares ${declaredLength} bytes; the limit is ${maxBytes}`,
|
|
95
|
-
|
|
100
|
+
imageDownloadLimitMessage(maxBytes),
|
|
96
101
|
);
|
|
97
102
|
}
|
|
98
103
|
|
|
@@ -107,7 +112,7 @@ export async function fetchImageBuffer(url, {
|
|
|
107
112
|
throw new ImagePromptError(
|
|
108
113
|
'image-too-large',
|
|
109
114
|
`Image response exceeded ${maxBytes} bytes`,
|
|
110
|
-
|
|
115
|
+
imageDownloadLimitMessage(maxBytes),
|
|
111
116
|
);
|
|
112
117
|
}
|
|
113
118
|
chunks.push(data);
|
|
@@ -120,7 +125,7 @@ export async function fetchImageBuffer(url, {
|
|
|
120
125
|
throw new ImagePromptError(
|
|
121
126
|
'image-too-large',
|
|
122
127
|
`Image response contains ${data.length} bytes; the limit is ${maxBytes}`,
|
|
123
|
-
|
|
128
|
+
imageDownloadLimitMessage(maxBytes),
|
|
124
129
|
);
|
|
125
130
|
}
|
|
126
131
|
return data;
|
|
@@ -146,7 +151,7 @@ function safeName(value) {
|
|
|
146
151
|
return name || undefined;
|
|
147
152
|
}
|
|
148
153
|
|
|
149
|
-
function detectedImageMediaType(data) {
|
|
154
|
+
export function detectedImageMediaType(data) {
|
|
150
155
|
if (data.length >= 8
|
|
151
156
|
&& data[0] === 0x89 && data[1] === 0x50 && data[2] === 0x4e && data[3] === 0x47
|
|
152
157
|
&& data[4] === 0x0d && data[5] === 0x0a && data[6] === 0x1a && data[7] === 0x0a) {
|
|
@@ -167,7 +172,7 @@ function detectedImageMediaType(data) {
|
|
|
167
172
|
return null;
|
|
168
173
|
}
|
|
169
174
|
|
|
170
|
-
function loadedImage(value) {
|
|
175
|
+
export function loadedImage(value) {
|
|
171
176
|
if (Buffer.isBuffer(value) || value instanceof Uint8Array) {
|
|
172
177
|
return { data: Buffer.from(value) };
|
|
173
178
|
}
|
|
@@ -191,12 +196,13 @@ export function hasInboundPrompt(message) {
|
|
|
191
196
|
|
|
192
197
|
export async function promptContentForMessage(message, {
|
|
193
198
|
signal,
|
|
199
|
+
deferImages = false,
|
|
194
200
|
maxImageBytes = DEFAULT_MAX_IMAGE_BYTES,
|
|
195
201
|
maxImages = DEFAULT_MAX_IMAGES,
|
|
196
202
|
maxTotalImageBytes = DEFAULT_MAX_TOTAL_IMAGE_BYTES,
|
|
197
203
|
} = {}) {
|
|
198
204
|
const sources = imageSources(message);
|
|
199
|
-
if (sources.length > maxImages) {
|
|
205
|
+
if (!deferImages && sources.length > maxImages) {
|
|
200
206
|
throw new ImagePromptError(
|
|
201
207
|
'too-many-images',
|
|
202
208
|
`Image message contains ${sources.length} images; the limit is ${maxImages}`,
|
|
@@ -210,6 +216,8 @@ export async function promptContentForMessage(message, {
|
|
|
210
216
|
if (text) content.push({ type: 'text', text });
|
|
211
217
|
else if (sources.length > 0) content.push({ type: 'text', text: t(DEFAULT_IMAGE_PROMPT) });
|
|
212
218
|
|
|
219
|
+
if (deferImages) return content;
|
|
220
|
+
|
|
213
221
|
for (const [index, source] of sources.entries()) {
|
|
214
222
|
signal?.throwIfAborted();
|
|
215
223
|
if (Number.isFinite(source?.size) && source.size > maxImageBytes) {
|
|
@@ -315,7 +323,7 @@ const IMAGE_FILE_EXTENSIONS = new Map([
|
|
|
315
323
|
|
|
316
324
|
const IMAGE_EXTENSION_PATTERN = /\.(?:png|jpe?g|gif|webp)$/i;
|
|
317
325
|
|
|
318
|
-
function imageStorageName(name, mediaType, index) {
|
|
326
|
+
export function imageStorageName(name, mediaType, index) {
|
|
319
327
|
const extension = IMAGE_FILE_EXTENSIONS.get(mediaType) ?? '.img';
|
|
320
328
|
const cleaned = safeName(name);
|
|
321
329
|
if (cleaned && IMAGE_EXTENSION_PATTERN.test(cleaned)) return cleaned;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { extractConnectionEvidence } from './connection-error.mjs';
|
|
2
|
+
import { diagnosticFields } from './diagnostic-details.mjs';
|
|
1
3
|
import { randomUUID } from 'node:crypto';
|
|
2
4
|
|
|
3
5
|
import { t } from './i18n.mjs';
|
|
@@ -189,7 +191,9 @@ export function classifyMessageFailure(error, {
|
|
|
189
191
|
&& userMessage.trim()
|
|
190
192
|
? 'INPUT_INVALID'
|
|
191
193
|
: classifiedCode;
|
|
194
|
+
const details = extractConnectionEvidence(error).details;
|
|
192
195
|
return Object.freeze({
|
|
196
|
+
...(code === 'INTERNAL_UNKNOWN' || details.reason !== 'unknown' || details.httpStatus || details.providerCode ? { details } : {}),
|
|
193
197
|
code,
|
|
194
198
|
reason: safeReason ?? safeFailureReason(error?.code) ?? code,
|
|
195
199
|
message: typeof userMessage === 'string' && userMessage.trim()
|
|
@@ -204,9 +208,16 @@ export function messageFailureText(failure) {
|
|
|
204
208
|
return `${failure.message}\n\n${t('错误码:{code};参考号:{referenceId}', failure)}`;
|
|
205
209
|
}
|
|
206
210
|
|
|
211
|
+
export function messageFailureDiagnostic(error, failure) {
|
|
212
|
+
const evidence = extractConnectionEvidence(error);
|
|
213
|
+
return { code: failure.code, reason: failure.reason, referenceId: failure.referenceId,
|
|
214
|
+
details: evidence.details, errors: evidence.errors };
|
|
215
|
+
}
|
|
216
|
+
|
|
207
217
|
export function setLastMessageFailure(status, error, options) {
|
|
208
218
|
const failure = classifyMessageFailure(error, options);
|
|
209
219
|
status.lastMessageError = failure;
|
|
220
|
+
status.lastError = failure.message;
|
|
210
221
|
return failure;
|
|
211
222
|
}
|
|
212
223
|
|
|
@@ -241,5 +252,6 @@ export function publicMessageFailure(value) {
|
|
|
241
252
|
message: value.message.slice(0, 500),
|
|
242
253
|
referenceId: value.referenceId.slice(0, 40),
|
|
243
254
|
at: value.at,
|
|
255
|
+
...(value.details ? diagnosticFields(value) : {}),
|
|
244
256
|
};
|
|
245
257
|
}
|
|
@@ -138,11 +138,11 @@ export function hasReplyReference(message) {
|
|
|
138
138
|
return objectReference(message?.replyTo);
|
|
139
139
|
}
|
|
140
140
|
|
|
141
|
-
export async function promptContentForInboundMessage(message, { signal } = {}) {
|
|
141
|
+
export async function promptContentForInboundMessage(message, { signal, deferImages = false } = {}) {
|
|
142
142
|
if (!hasReplyReference(message)) {
|
|
143
|
-
return promptContentForMessage(message, { signal });
|
|
143
|
+
return promptContentForMessage(message, { signal, deferImages });
|
|
144
144
|
}
|
|
145
145
|
const reference = normalizeReference(await resolveReference(message.replyTo, signal));
|
|
146
|
-
const currentContent = await promptContentForMessage(message, { signal });
|
|
146
|
+
const currentContent = await promptContentForMessage(message, { signal, deferImages });
|
|
147
147
|
return [{ type: 'text', text: replyBlock(reference) }, ...currentContent];
|
|
148
148
|
}
|