mirror of
https://github.com/NapNeko/NapCatQQ.git
synced 2026-02-04 06:31:13 +00:00
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:
parent
34ca919c4d
commit
0779628be5
@ -1,28 +1,29 @@
|
|||||||
{
|
{
|
||||||
"name": "napcat-common",
|
"name": "napcat-common",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"main": "src/index.ts",
|
"main": "src/index.ts",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
"typecheck": "tsc --noEmit --skipLibCheck -p tsconfig.json"
|
||||||
|
},
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"import": "./src/index.ts"
|
||||||
},
|
},
|
||||||
"exports": {
|
"./src/*": {
|
||||||
".": {
|
"import": "./src/*"
|
||||||
"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"
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -7,6 +7,7 @@ import { solveProblem } from '@/napcat-common/src/helper';
|
|||||||
export interface HttpDownloadOptions {
|
export interface HttpDownloadOptions {
|
||||||
url: string;
|
url: string;
|
||||||
headers?: Record<string, string> | string;
|
headers?: Record<string, string> | string;
|
||||||
|
proxy?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
type Uri2LocalRes = {
|
type Uri2LocalRes = {
|
||||||
@ -96,6 +97,7 @@ export function calculateFileMD5 (filePath: string): Promise<string> {
|
|||||||
|
|
||||||
async function tryDownload (options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
|
async function tryDownload (options: string | HttpDownloadOptions, useReferer: boolean = false): Promise<Response> {
|
||||||
let url: string;
|
let url: string;
|
||||||
|
let proxy: string | undefined;
|
||||||
let headers: Record<string, string> = {
|
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',
|
'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;
|
headers['Host'] = new URL(url).hostname;
|
||||||
} else {
|
} else {
|
||||||
url = options.url;
|
url = options.url;
|
||||||
|
proxy = options.proxy;
|
||||||
if (options.headers) {
|
if (options.headers) {
|
||||||
if (typeof options.headers === 'string') {
|
if (typeof options.headers === 'string') {
|
||||||
headers = JSON.parse(options.headers);
|
headers = JSON.parse(options.headers);
|
||||||
@ -115,6 +118,25 @@ async function tryDownload (options: string | HttpDownloadOptions, useReferer: b
|
|||||||
if (useReferer && !headers['Referer']) {
|
if (useReferer && !headers['Referer']) {
|
||||||
headers['Referer'] = url;
|
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) => {
|
const fetchRes = await fetch(url, { headers, redirect: 'follow' }).catch((err) => {
|
||||||
if (err.cause) {
|
if (err.cause) {
|
||||||
throw err.cause;
|
throw err.cause;
|
||||||
@ -176,7 +198,7 @@ export async function checkUriType (Uri: string) {
|
|||||||
return { Uri, Type: FileUriType.Unknown };
|
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 { Uri: HandledUri, Type: UriType } = await checkUriType(uri);
|
||||||
|
|
||||||
const filePath = path.join(dir, filename);
|
const filePath = path.join(dir, filename);
|
||||||
@ -191,7 +213,7 @@ export async function uriToLocalFile (dir: string, uri: string, filename: string
|
|||||||
}
|
}
|
||||||
|
|
||||||
case FileUriType.Remote: {
|
case FileUriType.Remote: {
|
||||||
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {} });
|
const buffer = await httpDownload({ url: HandledUri, headers: headers ?? {}, proxy });
|
||||||
fs.writeFileSync(filePath, buffer);
|
fs.writeFileSync(filePath, buffer);
|
||||||
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
return { success: true, errMsg: '', fileName: filename, path: filePath };
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1288,7 +1288,8 @@ export class OneBotMsgApi {
|
|||||||
}
|
}
|
||||||
realUri = await this.handleObfuckName(realUri) ?? realUri;
|
realUri = await this.handleObfuckName(realUri) ?? realUri;
|
||||||
try {
|
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) {
|
if (!success) {
|
||||||
this.core.context.logger.logError('文件处理失败', errMsg);
|
this.core.context.logger.logError('文件处理失败', errMsg);
|
||||||
throw new Error('文件处理失败: ' + errMsg);
|
throw new Error('文件处理失败: ' + errMsg);
|
||||||
|
|||||||
@ -82,6 +82,7 @@ export const OneBotConfigSchema = Type.Object({
|
|||||||
musicSignUrl: Type.String({ default: '' }),
|
musicSignUrl: Type.String({ default: '' }),
|
||||||
enableLocalFile2Url: Type.Boolean({ default: false }),
|
enableLocalFile2Url: Type.Boolean({ default: false }),
|
||||||
parseMultMsg: Type.Boolean({ default: false }),
|
parseMultMsg: Type.Boolean({ default: false }),
|
||||||
|
imageDownloadProxy: Type.String({ default: '' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type OneBotConfig = Static<typeof OneBotConfigSchema>;
|
export type OneBotConfig = Static<typeof OneBotConfigSchema>;
|
||||||
|
|||||||
@ -82,6 +82,7 @@ export const OneBotConfigSchema = Type.Object({
|
|||||||
musicSignUrl: Type.String({ default: '' }),
|
musicSignUrl: Type.String({ default: '' }),
|
||||||
enableLocalFile2Url: Type.Boolean({ default: false }),
|
enableLocalFile2Url: Type.Boolean({ default: false }),
|
||||||
parseMultMsg: Type.Boolean({ default: false }),
|
parseMultMsg: Type.Boolean({ default: false }),
|
||||||
|
imageDownloadProxy: Type.String({ default: '' }),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type OneBotConfig = Static<typeof OneBotConfigSchema>;
|
export type OneBotConfig = Static<typeof OneBotConfigSchema>;
|
||||||
|
|||||||
@ -22,12 +22,14 @@ const OneBotConfigCard = () => {
|
|||||||
musicSignUrl: '',
|
musicSignUrl: '',
|
||||||
enableLocalFile2Url: false,
|
enableLocalFile2Url: false,
|
||||||
parseMultMsg: false,
|
parseMultMsg: false,
|
||||||
|
imageDownloadProxy: '',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const reset = () => {
|
const reset = () => {
|
||||||
setOnebotValue('musicSignUrl', config.musicSignUrl);
|
setOnebotValue('musicSignUrl', config.musicSignUrl);
|
||||||
setOnebotValue('enableLocalFile2Url', config.enableLocalFile2Url);
|
setOnebotValue('enableLocalFile2Url', config.enableLocalFile2Url);
|
||||||
setOnebotValue('parseMultMsg', config.parseMultMsg);
|
setOnebotValue('parseMultMsg', config.parseMultMsg);
|
||||||
|
setOnebotValue('imageDownloadProxy', config.imageDownloadProxy);
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSubmit = handleOnebotSubmit(async (data) => {
|
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
|
<SaveButtons
|
||||||
onSubmit={onSubmit}
|
onSubmit={onSubmit}
|
||||||
reset={reset}
|
reset={reset}
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit';
|
|||||||
import type { RootState } from '@/store';
|
import type { RootState } from '@/store';
|
||||||
|
|
||||||
interface ConfigState {
|
interface ConfigState {
|
||||||
value: OneBotConfig
|
value: OneBotConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: ConfigState = {
|
const initialState: ConfigState = {
|
||||||
@ -18,6 +18,7 @@ const initialState: ConfigState = {
|
|||||||
musicSignUrl: '',
|
musicSignUrl: '',
|
||||||
enableLocalFile2Url: false,
|
enableLocalFile2Url: false,
|
||||||
parseMultMsg: true,
|
parseMultMsg: true,
|
||||||
|
imageDownloadProxy: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -1,64 +1,65 @@
|
|||||||
interface AdapterConfigInner {
|
interface AdapterConfigInner {
|
||||||
name: string
|
name: string;
|
||||||
enable: boolean
|
enable: boolean;
|
||||||
debug: boolean
|
debug: boolean;
|
||||||
token: string
|
token: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AdapterConfig extends AdapterConfigInner {
|
interface AdapterConfig extends AdapterConfigInner {
|
||||||
[key: string]: string | boolean | number
|
[key: string]: string | boolean | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageFormat = 'array' | 'string';
|
type MessageFormat = 'array' | 'string';
|
||||||
|
|
||||||
interface HttpServerConfig extends AdapterConfig {
|
interface HttpServerConfig extends AdapterConfig {
|
||||||
port: number
|
port: number;
|
||||||
host: string
|
host: string;
|
||||||
enableCors: boolean
|
enableCors: boolean;
|
||||||
enableWebsocket: boolean
|
enableWebsocket: boolean;
|
||||||
messagePostFormat: MessageFormat
|
messagePostFormat: MessageFormat;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HttpClientConfig extends AdapterConfig {
|
interface HttpClientConfig extends AdapterConfig {
|
||||||
url: string
|
url: string;
|
||||||
messagePostFormat: MessageFormat
|
messagePostFormat: MessageFormat;
|
||||||
reportSelfMessage: boolean
|
reportSelfMessage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebsocketServerConfig extends AdapterConfig {
|
interface WebsocketServerConfig extends AdapterConfig {
|
||||||
host: string
|
host: string;
|
||||||
port: number
|
port: number;
|
||||||
messagePostFormat: MessageFormat
|
messagePostFormat: MessageFormat;
|
||||||
reportSelfMessage: boolean
|
reportSelfMessage: boolean;
|
||||||
enableForcePushEvent: boolean
|
enableForcePushEvent: boolean;
|
||||||
heartInterval: number
|
heartInterval: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface WebsocketClientConfig extends AdapterConfig {
|
interface WebsocketClientConfig extends AdapterConfig {
|
||||||
url: string
|
url: string;
|
||||||
messagePostFormat: MessageFormat
|
messagePostFormat: MessageFormat;
|
||||||
reportSelfMessage: boolean
|
reportSelfMessage: boolean;
|
||||||
reconnectInterval: number
|
reconnectInterval: number;
|
||||||
token: string
|
token: string;
|
||||||
debug: boolean
|
debug: boolean;
|
||||||
heartInterval: number
|
heartInterval: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface HttpSseServerConfig extends HttpServerConfig {
|
interface HttpSseServerConfig extends HttpServerConfig {
|
||||||
reportSelfMessage: boolean
|
reportSelfMessage: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface NetworkConfig {
|
interface NetworkConfig {
|
||||||
httpServers: Array<HttpServerConfig>
|
httpServers: Array<HttpServerConfig>;
|
||||||
httpClients: Array<HttpClientConfig>
|
httpClients: Array<HttpClientConfig>;
|
||||||
httpSseServers: Array<HttpSseServerConfig>
|
httpSseServers: Array<HttpSseServerConfig>;
|
||||||
websocketServers: Array<WebsocketServerConfig>
|
websocketServers: Array<WebsocketServerConfig>;
|
||||||
websocketClients: Array<WebsocketClientConfig>
|
websocketClients: Array<WebsocketClientConfig>;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface OneBotConfig {
|
interface OneBotConfig {
|
||||||
network: NetworkConfig // 网络配置
|
network: NetworkConfig; // 网络配置
|
||||||
musicSignUrl: string // 音乐签名地址
|
musicSignUrl: string; // 音乐签名地址
|
||||||
enableLocalFile2Url: boolean
|
enableLocalFile2Url: boolean;
|
||||||
parseMultMsg: boolean
|
parseMultMsg: boolean;
|
||||||
|
imageDownloadProxy: string; // 图片下载代理地址
|
||||||
}
|
}
|
||||||
|
|||||||
@ -51,6 +51,9 @@ importers:
|
|||||||
file-type:
|
file-type:
|
||||||
specifier: ^21.0.0
|
specifier: ^21.0.0
|
||||||
version: 21.1.0
|
version: 21.1.0
|
||||||
|
undici:
|
||||||
|
specifier: ^6.19.0
|
||||||
|
version: 6.23.0
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@types/node':
|
'@types/node':
|
||||||
specifier: ^22.0.1
|
specifier: ^22.0.1
|
||||||
@ -6605,6 +6608,10 @@ packages:
|
|||||||
undici-types@6.21.0:
|
undici-types@6.21.0:
|
||||||
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
undici@6.23.0:
|
||||||
|
resolution: {integrity: sha512-VfQPToRA5FZs/qJxLIinmU59u0r7LXqoJkCzinq3ckNJp3vKEh7jTWN589YQ5+aoAC/TGRLyJLCPKcLQbM8r9g==}
|
||||||
|
engines: {node: '>=18.17'}
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==}
|
||||||
|
|
||||||
@ -14154,6 +14161,8 @@ snapshots:
|
|||||||
|
|
||||||
undici-types@6.21.0: {}
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
|
undici@6.23.0: {}
|
||||||
|
|
||||||
unified@11.0.5:
|
unified@11.0.5:
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/unist': 3.0.3
|
'@types/unist': 3.0.3
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user