diff --git a/package.json b/package.json index 42778838..0d2f0d88 100644 --- a/package.json +++ b/package.json @@ -66,6 +66,7 @@ "@google-cloud/vertexai": "^1.9.0", "@navetacandra/ddg": "^0.0.6", "@rollup/plugin-node-resolve": "^15.3.0", + "@types/fluent-ffmpeg": "^2.1.27", "@types/node": "^22.9.3", "@types/node-cron": "^3.0.11", "@types/react": "^18.3.12", @@ -75,6 +76,7 @@ "ai": "^4.0.3", "eslint": "^9.15.0", "eslint-plugin-format": "^0.1.2", + "fluent-ffmpeg": "^2.1.3", "gts": "^6.0.2", "openai": "^4.73.0", "react-dom": "^18.3.1", diff --git a/src/agent/index.ts b/src/agent/index.ts index d1c9915d..69a0cc5f 100644 --- a/src/agent/index.ts +++ b/src/agent/index.ts @@ -106,7 +106,7 @@ export function customInfo(config: AgentUserConfig): string { MAPPING_KEY: config.MAPPING_KEY, MAPPING_VALUE: config.MAPPING_VALUE, USE_TOOLS: config.USE_TOOLS.join(','), - SUPPORT_PLUGINS: [...Object.keys(ENV.PLUGINS_FUNCTION), ...Object.keys(tools)].join('|'), + SUPPORT_PLUGINS: Object.keys({ ...ENV.PLUGINS_FUNCTION, ...tools }).join('|'), CHAT_TRIGGER_PERFIX: ENV.CHAT_TRIGGER_PERFIX, MESSAGE_REPLACER: Object.keys(ENV.MESSAGE_REPLACER).join('|'), MAX_STEPS: config.MAX_STEPS, diff --git a/src/config/config.ts b/src/config/config.ts index 6428a78b..65bc39bc 100644 --- a/src/config/config.ts +++ b/src/config/config.ts @@ -325,7 +325,7 @@ export class DefineKeys { } export class ExtraUserConfig { - MAPPING_KEY = '-p:SYSTEM_INIT_MESSAGE|-n:MAX_HISTORY_LENGTH|-a:AI_PROVIDER|-ai:AI_IMAGE_PROVIDER|-m:CHAT_MODEL|-md:CURRENT_MODE|-v:OPENAI_VISION_MODEL|-t:OPENAI_TTS_MODEL|-ex:OPENAI_API_EXTRA_PARAMS|-mk:MAPPING_KEY|-mv:MAPPING_VALUE|-asap:FUNCTION_REPLY_ASAP|-tm:TOOL_MODEL|-tool:USE_TOOLS|-oli:IMAGE_MODEL|-vs:VERTEX_SEARCH_GROUNDING'; + MAPPING_KEY = '-p:SYSTEM_INIT_MESSAGE|-n:MAX_HISTORY_LENGTH|-a:AI_PROVIDER|-ai:AI_IMAGE_PROVIDER|-m:CHAT_MODEL|-md:CURRENT_MODE|-v:OPENAI_VISION_MODEL|-t:OPENAI_TTS_MODEL|-ex:OPENAI_API_EXTRA_PARAMS|-mk:MAPPING_KEY|-mv:MAPPING_VALUE|-asap:FUNCTION_REPLY_ASAP|-tm:TOOL_MODEL|-tool:USE_TOOLS|-oli:IMAGE_MODEL|-th:TEXT_HANDLE_TYPE|-to:TEXT_OUTPUT|-ah:AUDIO_HANDLE_TYPE|-ao:AUDIO_OUTPUT|-act:AUDIO_CONTAINS_TEXT'; // /set command mapping value, separated by |, : separates multiple relationships MAPPING_VALUE = ''; // MAPPING_VALUE = "cson:claude-3-5-sonnet-20240620|haiku:claude-3-haiku-20240307|g4m:gpt-4o-mini|g4:gpt-4o|rp+:command-r-plus"; diff --git a/src/telegram/command/system.ts b/src/telegram/command/system.ts index f6ee0a52..00acae3c 100644 --- a/src/telegram/command/system.ts +++ b/src/telegram/command/system.ts @@ -476,7 +476,11 @@ export class SetCommandHandler implements CommandHandler { context: WorkerContext, sender: MessageSender, ): Promise { - let key = keys[flag] || (Object.keys(context.USER_CONFIG).includes(flag.slice(1)) ? flag.slice(1) : null); + let key = keys[flag] + || (Object.values(keys).includes(flag.slice(1)) + || Object.keys(context.USER_CONFIG).includes(flag.slice(1)) + ? flag.slice(1) + : null); let mappedValue = values[value] ?? value; if (!key) { @@ -521,10 +525,12 @@ export class SetCommandHandler implements CommandHandler { if (typeof context.USER_CONFIG[key] === 'boolean') { mappedValue = typeof mappedValue === 'boolean' ? mappedValue : mappedValue === 'true'; } - - context.USER_CONFIG[key] = mappedValue; - if (!context.USER_CONFIG.DEFINE_KEYS.includes(key)) { + // 如果设置的值为空,则使用默认值 + context.USER_CONFIG[key] = mappedValue || ENV.USER_CONFIG[key]; + if (!context.USER_CONFIG.DEFINE_KEYS.includes(key) && mappedValue) { context.USER_CONFIG.DEFINE_KEYS.push(key); + } else if (!mappedValue) { + context.USER_CONFIG.DEFINE_KEYS = context.USER_CONFIG.DEFINE_KEYS.filter(k => k !== key); } log.info(`/set ${key} ${(JSON.stringify(mappedValue) || value).substring(0, 100)}...`); return key; @@ -665,7 +671,7 @@ export class InlineCommandHandler implements CommandHandler { label: 'Tools', data: 'INLINE_FUNCTION_TOOLS', config_key: 'USE_TOOLS', - available_values: [...new Set([...Object.keys(tools), ...Object.keys(ENV.PLUGINS_FUNCTION)])], + available_values: Object.keys({ ...ENV.PLUGINS_FUNCTION, ...tools }), }, }; }; diff --git a/src/telegram/handler/chat.ts b/src/telegram/handler/chat.ts index 02b00c2d..38701c1e 100644 --- a/src/telegram/handler/chat.ts +++ b/src/telegram/handler/chat.ts @@ -14,7 +14,7 @@ import { ENV } from '../../config/env'; import { clearLog, getLog, logSingleton } from '../../log/logDecortor'; import { log } from '../../log/logger'; import { sendToolResult } from '../../tools'; -import { imageToBase64String, renderBase64DataURI } from '../../utils/image'; +import { imageToBase64String } from '../../utils/image'; import { createTelegramBotAPI } from '../api'; import { escape } from '../utils/md2tgmd'; import { MessageSender, sendAction, TelegraphSender } from '../utils/send'; @@ -88,10 +88,10 @@ export class ChatHandler implements MessageHandler { await workflow(context, message, params); return null; } catch (e) { - console.error('Error:', e); + log.error((e as Error).stack); const sender = context.MIDDLE_CONTEXT.sender ?? MessageSender.from(context.SHARE_CONTEXT.botToken, message); const filtered = (e as Error).message.replace(context.SHARE_CONTEXT.botToken, '[REDACTED]'); - return sender.sendRichText(`
Error: ${filtered.substring(0, 4000)}
`, 'HTML'); + return sender.sendRichText(`
Error: ${filtered.substring(0, 2000)}
`, 'HTML'); } }; @@ -204,13 +204,13 @@ export function OnStreamHander(sender: MessageSender | ChosenInlineSender, conte } if (!resp.ok) { - log.error(`send message failed: ${resp.status} ${resp.statusText}`); + log.error(`send message failed: ${resp.status} ${await resp.json().then(j => j.description)}`); sentError = true; log.error(`send message failed: ${escape(data.split('\n'))}`); return sentPromise = sender.sendPlainText(text); } } catch (e) { - console.error(e); + log.error((e as Error).stack); } }; @@ -235,6 +235,7 @@ export function OnStreamHander(sender: MessageSender | ChosenInlineSender, conte } if (sentError || !finalResp.ok) { (sender as MessageSender).context.sentMessageIds.clear(); + log.error(`send message failed: ${finalResp.status} ${await finalResp.json().then(j => j.description)}`); return sendTelegraph(context!, sender, question || 'Redo Question', text, true); } return finalResp; @@ -385,10 +386,8 @@ async function handleAudio( if (isMiddle) { const voice = await asr(resp as string, context.USER_CONFIG); ENV.HIDE_MIDDLE_MESSAGE && sender.api.deleteMessage({ chat_id: sender.context.chat_id, message_id: sender.context.message_id! }); - return sender.api.sendVoice({ - chat_id: sender.context.chat_id, - voice, - }); + sendAction(context.SHARE_CONTEXT.botToken, sender.context.chat_id, 'upload_voice'); + return sender.sendVoice(voice); } return resp; } @@ -407,22 +406,14 @@ async function handleTextToAudio( text = await chatWithLLM(message, params, context, null, streamSender, true) as string; !ENV.HIDE_MIDDLE_MESSAGE && streamSender.send('Chat with LLM done'); } - const agent = new ASR(); - const audio = await agent.hander(text, context.USER_CONFIG); - // const sendPromise = sender.sendPlainText('Audio generation in progress.'); - // sender.update({ message_id: await sendPromise.then(r => r.json()).then(r => r.result.message_id) }); - // const mediaParams: Telegram.InputMediaAudio = { - // type: 'audio', - // media: 'attach://file', - // }; + const audio = await asr(text, context.USER_CONFIG); sendAction(context.SHARE_CONTEXT.botToken, sender.context.chat_id, 'upload_voice'); const resp = await sender.sendVoice(audio, context.USER_CONFIG.AUDIO_CONTAINS_TEXT ? text : undefined); if (resp.ok) { return sender.api.deleteMessage({ chat_id: sender.context.chat_id, message_id: sender.context.message_id! }); } - log.error(`Failed to send voice message: ${resp.status} ${await resp.text()}`); - throw new Error(`Failed to send voice message: ${resp.status} ${resp.statusText}`); - // return sender.editMessageMedia(mediaParams, undefined, audio); + // log.error(`Failed to send voice message: ${resp.status} ${await resp.text()}`); + throw new Error(`Failed to send voice message: ${resp.status} ${await resp.json().then(j => j.description)}`); } export async function sendImages(img: ImageResult, SEND_IMAGE_AS_FILE: boolean, sender: MessageSender, config: AgentUserConfig) { diff --git a/src/telegram/handler/handlers.ts b/src/telegram/handler/handlers.ts index 8e1acfa4..164d02f6 100644 --- a/src/telegram/handler/handlers.ts +++ b/src/telegram/handler/handlers.ts @@ -251,6 +251,9 @@ export class HandlerCallbackQuery implements CallbackQueryHandler void) { + this.bufferCache += chunk.toString('base64'); + callback(); + } + + _flush(callback: () => void) { + if (this.bufferCache.length) { + this.push(this.bufferCache); + } + callback(); + } +} + +async function streamToBase64(audioUrl: string) { + try { + const response = await fetch(audioUrl); + if (!response.ok) { + throw new Error(`Failed to download file: ${response.statusText}`); + } + + return new Promise((resolve, reject) => { + const base64EncodeStream = new Base64Encode(); + let base64Data = ''; + + base64EncodeStream.on('data', chunk => base64Data += chunk); + base64EncodeStream.on('end', () => resolve(base64Data)); + base64EncodeStream.on('error', reject); + + // 将 Web Stream 转换为 Node Stream + Readable.fromWeb(response.body as unknown as WebReadableStream).pipe(base64EncodeStream); + }); + } catch (error) { + console.error('Error processing stream:', error); + throw error; + } +} + +export { streamToBase64 }; diff --git a/wrangler-example.toml b/wrangler-example.toml index 1aa7b208..ba22814d 100644 --- a/wrangler-example.toml +++ b/wrangler-example.toml @@ -1,7 +1,7 @@ # 这里的 name 改成你自己的workers 的名字 name = 'chatgpt-telegram-workers' -compatibility_date = '2023-10-07' -main = './dist/index.js' +compatibility_date = '2024-09-23' +main = './src/index.ts' workers_dev = true compatibility_flags = [ "nodejs_compat" ] @@ -89,7 +89,7 @@ GROUP_CHAT_BOT_ENABLE = 'true' ## -- 通用配置 -- ## -## AI提供商: auto, openai, azure, workers, gemini, mistral, cohere, anthropic +## AI提供商: auto, openai, azure, workers, gemini, mistral, cohere, anthropic, xai AI_PROVIDER = 'auto' ## AI图片提供商: auto, openai, azure, workers AI_IMAGE_PROVIDER = 'auto' diff --git a/yarn.lock b/yarn.lock index 1626e40b..7c3c9bde 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1204,6 +1204,13 @@ resolved "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz" integrity sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw== +"@types/fluent-ffmpeg@^2.1.27": + version "2.1.27" + resolved "https://registry.yarnpkg.com/@types/fluent-ffmpeg/-/fluent-ffmpeg-2.1.27.tgz#c4eac6fbda30bb6316d2220c8faf54f48db0812d" + integrity sha512-QiDWjihpUhriISNoBi2hJBRUUmoj/BMTYcfz+F+ZM9hHWBYABFAE6hjP/TbCZC0GWwlpa3FzvHH9RzFeRusZ7A== + dependencies: + "@types/node" "*" + "@types/json-schema@^7.0.15", "@types/json-schema@^7.0.6", "@types/json-schema@^7.0.9": version "7.0.15" resolved "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz" @@ -1246,7 +1253,7 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@^22.9.1": +"@types/node@*": version "22.9.1" resolved "https://registry.npmjs.org/@types/node/-/node-22.9.1.tgz" integrity sha512-p8Yy/8sw1caA8CdRIQBG5tiLHmxtQKObCijiAa9Ez+d4+PRffM4054xbju0msf+cvhJpnFEeNjxmVT/0ipktrg== @@ -1265,6 +1272,13 @@ dependencies: undici-types "~5.26.4" +"@types/node@^22.9.3": + version "22.9.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.9.3.tgz#08f3d64b3bc6d74b162d36f60213e8a6704ef2b4" + integrity sha512-F3u1fs/fce3FFk+DAxbxc78DF8x0cY09RRL8GnXLmkJ1jvx3TtPdWoTT5/NiYfI5ASqXBmfqJi9dZ3gxMx4lzw== + dependencies: + undici-types "~6.19.8" + "@types/normalize-package-data@^2.4.0": version "2.4.4" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz" @@ -1899,6 +1913,11 @@ async-sema@^3.1.1: resolved "https://registry.npmjs.org/async-sema/-/async-sema-3.1.1.tgz" integrity sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg== +async@^0.2.9: + version "0.2.10" + resolved "https://registry.yarnpkg.com/async/-/async-0.2.10.tgz#b6bbe0b0674b9d719708ca38de8c237cb526c3d1" + integrity sha512-eAkdoKxU6/LkKDBzLpT+t6Ff5EtfSF4wx1WfJiPEEV7WNLnDaRXk0oVysiEPm262roaachGexwUv94WhSgN5TQ== + asynckit@^0.4.0: version "0.4.0" resolved "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" @@ -3413,6 +3432,14 @@ flatted@^3.2.9: resolved "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz" integrity sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw== +fluent-ffmpeg@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/fluent-ffmpeg/-/fluent-ffmpeg-2.1.3.tgz#d6846be257777844249a4adeb320f25326d239f3" + integrity sha512-Be3narBNt2s6bsaqP6Jzq91heDgOEaDCJAXcE3qcma/EJBSy5FB4cvO31XBInuAuKBx8Kptf8dkhjK0IOru39Q== + dependencies: + async "^0.2.9" + which "^1.1.1" + form-data-encoder@1.7.2: version "1.7.2" resolved "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz" @@ -6434,6 +6461,13 @@ whatwg-url@^5.0.0: tr46 "~0.0.3" webidl-conversions "^3.0.0" +which@^1.1.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/which/-/which-1.3.1.tgz#a45043d54f5805316da8d62f9f50918d3da70b0a" + integrity sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ== + dependencies: + isexe "^2.0.0" + which@^2.0.1, which@^2.0.2: version "2.0.2" resolved "https://registry.npmjs.org/which/-/which-2.0.2.tgz"