From a9b0c3ff103771ce04b6531eaa7bdda479197b61 Mon Sep 17 00:00:00 2001 From: sliverp Date: Fri, 30 Jan 2026 15:12:28 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=8F=AF=E9=80=89?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E5=9B=BE=E5=BA=8A=E6=9C=8D=E5=8A=A1=E5=99=A8?= =?UTF-8?q?=E5=9C=B0=E5=9D=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/channel.ts | 1 + src/config.ts | 6 +++- src/gateway.ts | 44 +++++++++++++++++----------- src/onboarding.ts | 73 +++++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 4 +++ 5 files changed, 111 insertions(+), 17 deletions(-) diff --git a/src/channel.ts b/src/channel.ts index 93834e2..ec179d7 100644 --- a/src/channel.ts +++ b/src/channel.ts @@ -63,6 +63,7 @@ export const qqbotPlugin: ChannelPlugin = { clientSecret, clientSecretFile: input.tokenFile, name: input.name, + imageServerBaseUrl: input.imageServerBaseUrl, }); }, }, diff --git a/src/config.ts b/src/config.ts index 0f1a82f..dadcdf2 100644 --- a/src/config.ts +++ b/src/config.ts @@ -63,6 +63,7 @@ export function resolveQQBotAccount( dmPolicy: qqbot?.dmPolicy, allowFrom: qqbot?.allowFrom, systemPrompt: qqbot?.systemPrompt, + imageServerBaseUrl: qqbot?.imageServerBaseUrl, }; appId = qqbot?.appId ?? ""; } else { @@ -97,6 +98,7 @@ export function resolveQQBotAccount( clientSecret, secretSource, systemPrompt: accountConfig.systemPrompt, + imageServerBaseUrl: accountConfig.imageServerBaseUrl || process.env.QQBOT_IMAGE_SERVER_BASE_URL, config: accountConfig, }; } @@ -107,7 +109,7 @@ export function resolveQQBotAccount( export function applyQQBotAccountConfig( cfg: MoltbotConfig, accountId: string, - input: { appId?: string; clientSecret?: string; clientSecretFile?: string; name?: string } + input: { appId?: string; clientSecret?: string; clientSecretFile?: string; name?: string; imageServerBaseUrl?: string } ): MoltbotConfig { const next = { ...cfg }; @@ -124,6 +126,7 @@ export function applyQQBotAccountConfig( ? { clientSecretFile: input.clientSecretFile } : {}), ...(input.name ? { name: input.name } : {}), + ...(input.imageServerBaseUrl ? { imageServerBaseUrl: input.imageServerBaseUrl } : {}), }, }; } else { @@ -144,6 +147,7 @@ export function applyQQBotAccountConfig( ? { clientSecretFile: input.clientSecretFile } : {}), ...(input.name ? { name: input.name } : {}), + ...(input.imageServerBaseUrl ? { imageServerBaseUrl: input.imageServerBaseUrl } : {}), }, }, }, diff --git a/src/gateway.ts b/src/gateway.ts index 2ee6ecb..da678d7 100644 --- a/src/gateway.ts +++ b/src/gateway.ts @@ -96,8 +96,15 @@ export async function startGateway(ctx: GatewayContext): Promise { throw new Error("QQBot not configured (missing appId or clientSecret)"); } - // 尝试启动图床服务器 - const imageServerBaseUrl = await ensureImageServer(log); + // 如果配置了公网 URL,启动图床服务器 + let imageServerBaseUrl: string | null = null; + if (account.imageServerBaseUrl) { + await ensureImageServer(log); + imageServerBaseUrl = account.imageServerBaseUrl; + log?.info(`[qqbot:${account.accountId}] Image server enabled with URL: ${imageServerBaseUrl}`); + } else { + log?.info(`[qqbot:${account.accountId}] Image server disabled (no imageServerBaseUrl configured)`); + } let reconnectAttempts = 0; let isAborted = false; @@ -232,15 +239,17 @@ export async function startGateway(ctx: GatewayContext): Promise { const envelopeOptions = pluginRuntime.channel.reply.resolveEnvelopeFormatOptions(cfg); // 组装消息体,添加系统提示词 - const builtinPrompt = `由于平台限制,你的回复中不可以包含任何URL。 + let builtinPrompt = "由于平台限制,你的回复中不可以包含任何URL。"; + + // 只有配置了图床公网地址,才告诉 AI 可以发送图片 + if (imageServerBaseUrl) { + builtinPrompt += ` 【发送图片】 -如果需要发送本地图片文件给用户,请在回复中使用以下格式: -MEDIA:/绝对路径/图片文件.png - -例如:MEDIA:/home/ubuntu/clawd/downloads/image.png - -系统会自动将该路径的图片发送给用户。支持 png、jpg、gif、webp 格式。`; +你可以发送本地图片文件给用户。只需在回复中直接引用图片的绝对路径即可,系统会自动处理。 +支持 png、jpg、gif、webp 格式。`; + } + const systemPrompts = [builtinPrompt]; if (account.systemPrompt) { systemPrompts.push(account.systemPrompt); @@ -486,13 +495,16 @@ MEDIA:/绝对路径/图片文件.png textWithoutImages = textWithoutImages.replace(match[0], "").trim(); } - // 处理剩余文本中的 URL 点号 - const originalText = textWithoutImages; - textWithoutImages = textWithoutImages.replace(/([a-zA-Z0-9])\.([a-zA-Z0-9])/g, "$1_$2"); - - const hasReplacement = textWithoutImages !== originalText; - if (hasReplacement && textWithoutImages.trim()) { - textWithoutImages += "\n\n(由于平台限制,回复中的部分符号已被替换)"; + // 处理剩余文本中的 URL 点号(只有在没有图片的情况下才替换,避免误伤) + const hasImages = imageUrls.length > 0; + let hasReplacement = false; + if (!hasImages) { + const originalText = textWithoutImages; + textWithoutImages = textWithoutImages.replace(/([a-zA-Z0-9])\.([a-zA-Z0-9])/g, "$1_$2"); + hasReplacement = textWithoutImages !== originalText; + if (hasReplacement && textWithoutImages.trim()) { + textWithoutImages += "\n\n(由于平台限制,回复中的部分符号已被替换)"; + } } try { diff --git a/src/onboarding.ts b/src/onboarding.ts index 189792e..048e591 100644 --- a/src/onboarding.ts +++ b/src/onboarding.ts @@ -29,12 +29,14 @@ interface QQBotChannelConfig { clientSecret?: string; clientSecretFile?: string; name?: string; + imageServerBaseUrl?: string; accounts?: Record; } @@ -195,6 +197,44 @@ export const qqbotOnboardingAdapter: ChannelOnboardingAdapter = { ).trim(); } + // 询问是否配置图片发送功能 + let imageServerBaseUrl: string | null = null; + const existingImageUrl = (next.channels?.qqbot as QQBotChannelConfig)?.imageServerBaseUrl + || process.env.QQBOT_IMAGE_SERVER_BASE_URL; + + const wantImageSupport = await prompter.confirm({ + message: "是否启用图片发送功能?(需要服务器有公网 IP)", + initialValue: Boolean(existingImageUrl), + }); + + if (wantImageSupport) { + imageServerBaseUrl = String( + await prompter.text({ + message: "请输入服务器公网地址(格式: http://公网IP:18765)", + placeholder: "例如: http://123.45.67.89:18765", + initialValue: existingImageUrl || undefined, + validate: (value) => { + if (!value?.trim()) return "公网地址不能为空"; + if (!value.startsWith("http://") && !value.startsWith("https://")) { + return "地址必须以 http:// 或 https:// 开头"; + } + return undefined; + }, + }), + ).trim(); + + await prompter.note( + [ + "图片发送功能已启用。请确保:", + "1. 服务器防火墙已开放 18765 端口", + "2. 云服务器安全组已放行 18765 端口(入站)", + "", + "如果图片发送失败,请检查端口是否可从公网访问。", + ].join("\n"), + "图片功能配置", + ); + } + // 应用配置 if (appId && clientSecret) { if (accountId === DEFAULT_ACCOUNT_ID) { @@ -207,6 +247,7 @@ export const qqbotOnboardingAdapter: ChannelOnboardingAdapter = { enabled: true, appId, clientSecret, + ...(imageServerBaseUrl ? { imageServerBaseUrl } : {}), }, }, }; @@ -225,6 +266,38 @@ export const qqbotOnboardingAdapter: ChannelOnboardingAdapter = { enabled: true, appId, clientSecret, + ...(imageServerBaseUrl ? { imageServerBaseUrl } : {}), + }, + }, + }, + }, + }; + } + } else if (imageServerBaseUrl) { + // 只更新 imageServerBaseUrl + if (accountId === DEFAULT_ACCOUNT_ID) { + next = { + ...next, + channels: { + ...next.channels, + qqbot: { + ...next.channels?.qqbot, + imageServerBaseUrl, + }, + }, + }; + } else { + next = { + ...next, + channels: { + ...next.channels, + qqbot: { + ...next.channels?.qqbot, + accounts: { + ...(next.channels?.qqbot as QQBotChannelConfig)?.accounts, + [accountId]: { + ...(next.channels?.qqbot as QQBotChannelConfig)?.accounts?.[accountId], + imageServerBaseUrl, }, }, }, diff --git a/src/types.ts b/src/types.ts index 69e1cc1..e185263 100644 --- a/src/types.ts +++ b/src/types.ts @@ -19,6 +19,8 @@ export interface ResolvedQQBotAccount { secretSource: "config" | "file" | "env" | "none"; /** 系统提示词 */ systemPrompt?: string; + /** 图床服务器公网地址 */ + imageServerBaseUrl?: string; config: QQBotAccountConfig; } @@ -35,6 +37,8 @@ export interface QQBotAccountConfig { allowFrom?: string[]; /** 系统提示词,会添加在用户消息前面 */ systemPrompt?: string; + /** 图床服务器公网地址,用于发送图片,例如 http://your-ip:18765 */ + imageServerBaseUrl?: string; } /**