Add image download proxy support to OneBot

Introduces an 'imageDownloadProxy' config option to OneBot, allowing image downloads via a specified HTTP proxy. Updates the file download logic in napcat-common to use the undici library for proxy support, and propagates the new config through backend, frontend, and type definitions. Also adds undici as a dependency.
This commit is contained in:
手瓜一十雪 2026-01-29 20:32:01 +08:00
parent 34ca919c4d
commit 0779628be5
9 changed files with 119 additions and 64 deletions

View File

@ -1,28 +1,29 @@
{
"name": "napcat-common",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
"name": "napcat-common",
"version": "0.0.1",
"private": true,
"type": "module",
"main": "src/index.ts",
"scripts": {
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
},
"exports": {
".": {
"import": "./src/index.ts"
},
"exports": {
".": {
"import": "./src/index.ts"
},
"./src/*": {
"import": "./src/*"
}
},
"dependencies": {
"ajv": "^8.13.0",
"file-type": "^21.0.0"
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
"./src/*": {
"import": "./src/*"
}
},
"dependencies": {
"ajv": "^8.13.0",
"file-type": "^21.0.0",
"undici": "^6.19.0"
},
"devDependencies": {
"@types/node": "^22.0.1"
},
"engines": {
"node": ">=18.0.0"
}
}

View File

@ -7,6 +7,7 @@ import { solveProblem } from '@/napcat-common/src/helper';
export interface HttpDownloadOptions {
url: string;
headers?: Record<string, string> | string;
proxy?: string;
}
type Uri2LocalRes = {
@ -96,6 +97,7 @@ export function calculateFileMD5 (filePath: string): Promise<string> {
async function tryDownload (options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
let url: string;
let proxy: string | undefined;
let headers: Record<string, string> = {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/94.0.4606.71 Safari/537.36',
};
@ -104,6 +106,7 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
headers['Host'] = new URL(url).hostname;
} else {
url = options.url;
proxy = options.proxy;
if (options.headers) {
if (typeof options.headers === 'string') {
headers = JSON.parse(options.headers);
@ -115,6 +118,25 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
if (useReferer && !headers['Referer']) {
headers['Referer'] = url;
}
// 如果配置了代理,使用代理下载
if (proxy) {
try {
// Node.js 18+ 内置 undici使用动态导入
const undici = await import('undici');
const dispatcher = new undici.ProxyAgent(proxy);
const response = await undici.fetch(url, {
headers,
redirect: 'follow',
dispatcher,
});
return response as unknown as Response;
} catch (proxyError) {
// 如果代理失败,记录错误并尝试直接下载
console.error('代理下载失败,尝试直接下载:', proxyError);
}
}
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
if (err.cause) {
throw err.cause;
@ -176,7 +198,7 @@ export async function checkUriType (Uri: string) {
return { Uri, Type: FileUriType.Unknown };
}
export async function uriToLocalFile (dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>): Promise<Uri2LocalRes> {
export async function uriToLocalFile (dir: string, uri: string, filename: string = randomUUID(), headers?: Record<string, string>, proxy?: string): Promise<Uri2LocalRes> {
const { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
const filePath = path.join(dir, filename);
@ -191,7 +213,7 @@ export async function uriToLocalFile (dir: string, uri: string, filename: string
}
case FileUriType.Remote: {
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {}, proxy });
fs.writeFileSync(filePath, buffer);
return { success: true, errMsg: '', fileName: filename, path: filePath };
}

View File

@ -1288,7 +1288,8 @@ export class OneBotMsgApi {
}
realUri = await this.handleObfuckName(realUri) ?? realUri;
try {
const { path, fileName, errMsg, success } = await uriToLocalFile(this.core.NapCatTempPath, realUri);
const proxy = this.obContext.configLoader.configData.imageDownloadProxy || undefined;
const { path, fileName, errMsg, success } = await uriToLocalFile(this.core.NapCatTempPath, realUri, undefined, undefined, proxy);
if (!success) {
this.core.context.logger.logError('文件处理失败', errMsg);
throw new Error('文件处理失败: ' + errMsg);

View File

@ -82,6 +82,7 @@ export const OneBotConfigSchema = Type.Object({
musicSignUrl: Type.String({ default: '' }),
enableLocalFile2Url: Type.Boolean({ default: false }),
parseMultMsg: Type.Boolean({ default: false }),
imageDownloadProxy: Type.String({ default: '' }),
});
export type OneBotConfig = Static<typeof OneBotConfigSchema>;

View File

@ -82,6 +82,7 @@ export const OneBotConfigSchema = Type.Object({
musicSignUrl: Type.String({ default: '' }),
enableLocalFile2Url: Type.Boolean({ default: false }),
parseMultMsg: Type.Boolean({ default: false }),
imageDownloadProxy: Type.String({ default: '' }),
});
export type OneBotConfig = Static<typeof OneBotConfigSchema>;

View File

@ -22,12 +22,14 @@ const OneBotConfigCard = () => {
musicSignUrl: '',
enableLocalFile2Url: false,
parseMultMsg: false,
imageDownloadProxy: '',
},
});
const reset = () => {
setOnebotValue('musicSignUrl', config.musicSignUrl);
setOnebotValue('enableLocalFile2Url', config.enableLocalFile2Url);
setOnebotValue('parseMultMsg', config.parseMultMsg);
setOnebotValue('imageDownloadProxy', config.imageDownloadProxy);
};
const onSubmit = handleOnebotSubmit(async (data) => {
@ -104,6 +106,22 @@ const OneBotConfigCard = () => {
/>
)}
/>
<Controller
control={control}
name='imageDownloadProxy'
render={({ field }) => (
<Input
{...field}
label='图片下载代理'
placeholder='请输入代理地址,如 http://127.0.0.1:7890'
classNames={{
inputWrapper:
'bg-default-100/50 dark:bg-white/5 backdrop-blur-md border border-transparent hover:bg-default-200/50 dark:hover:bg-white/10 transition-all shadow-sm data-[hover=true]:border-default-300',
input: 'bg-transparent text-default-700 placeholder:text-default-400',
}}
/>
)}
/>
<SaveButtons
onSubmit={onSubmit}
reset={reset}

View File

@ -3,7 +3,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit';
import type { RootState } from '@/store';
interface ConfigState {
value: OneBotConfig
value: OneBotConfig;
}
const initialState: ConfigState = {
@ -18,6 +18,7 @@ const initialState: ConfigState = {
musicSignUrl: '',
enableLocalFile2Url: false,
parseMultMsg: true,
imageDownloadProxy: '',
},
};

View File

@ -1,64 +1,65 @@
interface AdapterConfigInner {
name: string
enable: boolean
debug: boolean
token: string
name: string;
enable: boolean;
debug: boolean;
token: string;
}
interface AdapterConfig extends AdapterConfigInner {
[key: string]: string | boolean | number
[key: string]: string | boolean | number;
}
type MessageFormat = 'array' | 'string';
interface HttpServerConfig extends AdapterConfig {
port: number
host: string
enableCors: boolean
enableWebsocket: boolean
messagePostFormat: MessageFormat
port: number;
host: string;
enableCors: boolean;
enableWebsocket: boolean;
messagePostFormat: MessageFormat;
}
interface HttpClientConfig extends AdapterConfig {
url: string
messagePostFormat: MessageFormat
reportSelfMessage: boolean
url: string;
messagePostFormat: MessageFormat;
reportSelfMessage: boolean;
}
interface WebsocketServerConfig extends AdapterConfig {
host: string
port: number
messagePostFormat: MessageFormat
reportSelfMessage: boolean
enableForcePushEvent: boolean
heartInterval: number
host: string;
port: number;
messagePostFormat: MessageFormat;
reportSelfMessage: boolean;
enableForcePushEvent: boolean;
heartInterval: number;
}
interface WebsocketClientConfig extends AdapterConfig {
url: string
messagePostFormat: MessageFormat
reportSelfMessage: boolean
reconnectInterval: number
token: string
debug: boolean
heartInterval: number
url: string;
messagePostFormat: MessageFormat;
reportSelfMessage: boolean;
reconnectInterval: number;
token: string;
debug: boolean;
heartInterval: number;
}
interface HttpSseServerConfig extends HttpServerConfig {
reportSelfMessage: boolean
reportSelfMessage: boolean;
}
interface NetworkConfig {
httpServers: Array<HttpServerConfig>
httpClients: Array<HttpClientConfig>
httpSseServers: Array<HttpSseServerConfig>
websocketServers: Array<WebsocketServerConfig>
websocketClients: Array<WebsocketClientConfig>
httpServers: Array<HttpServerConfig>;
httpClients: Array<HttpClientConfig>;
httpSseServers: Array<HttpSseServerConfig>;
websocketServers: Array<WebsocketServerConfig>;
websocketClients: Array<WebsocketClientConfig>;
}
interface OneBotConfig {
network: NetworkConfig // 网络配置
musicSignUrl: string // 音乐签名地址
enableLocalFile2Url: boolean
parseMultMsg: boolean
network: NetworkConfig; // 网络配置
musicSignUrl: string; // 音乐签名地址
enableLocalFile2Url: boolean;
parseMultMsg: boolean;
imageDownloadProxy: string; // 图片下载代理地址
}

View File

@ -51,6 +51,9 @@ importers:
file-type:
specifier: ^21.0.0
version: 21.1.0
undici:
specifier: ^6.19.0
version: 6.23.0
devDependencies:
'@types/node':
specifier: ^22.0.1
@ -6605,6 +6608,10 @@ packages:
undici-types@6.21.0:
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
undici@6.23.0:
resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==}
engines: {node: '>=18.17'}
unified@11.0.5:
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
@ -14154,6 +14161,8 @@ snapshots:
undici-types@6.21.0: {}
undici@6.23.0: {}
unified@11.0.5:
dependencies:
'@types/unist': 3.0.3