Telegram 图片消息此前一律回复「暂只支持文本消息」。本改动: - photo 取最大尺寸(TG 压成 jpeg),document 仅接受 image/png|jpeg|webp|gif - caption 作为伴随文本块进入同一条消息(不解析为命令) - 相册(同一 media_group_id)去抖 1.2s 合并为一条多图消息,避免 N 连回合 - 派发走宿主 sessions.prompt RPC(Web 端同通道):模型图像能力校验 + 附件服务持久化 + mode queue 排队,与纯文本路径的 agent.followup 语义一致 - 错误映射:MODEL_DOES_NOT_SUPPORT_IMAGES → 提示换多模态模型;其余回退 图片发送失败 + [code] 详情;下载失败即时提示 - 客户端新增 getFile/downloadFile(GET file 端点,复用代理感知 fetch) tests +7(58/58):photo+caption、png document、pdf 拒绝、相册合并、 模型不支持、下载失败、未绑定。无 emoji 文案。
125 lines
4.0 KiB
JavaScript
125 lines
4.0 KiB
JavaScript
import { EnvHttpProxyAgent, fetch as undiciFetch } from 'undici';
|
|
function resolveProxyUrl() {
|
|
return (process.env.HTTPS_PROXY
|
|
|| process.env.HTTP_PROXY
|
|
|| process.env.https_proxy
|
|
|| process.env.http_proxy
|
|
|| undefined);
|
|
}
|
|
/**
|
|
* Node's global `fetch` ignores HTTP(S)_PROXY unless `NODE_USE_ENV_PROXY=1`.
|
|
* Undici's EnvHttpProxyAgent honors the env vars (and NO_PROXY).
|
|
*/
|
|
function createDefaultFetch() {
|
|
if (!resolveProxyUrl())
|
|
return globalThis.fetch;
|
|
const agent = new EnvHttpProxyAgent();
|
|
const proxied = ((input, init) => undiciFetch(input, {
|
|
...init,
|
|
dispatcher: agent,
|
|
}));
|
|
return proxied;
|
|
}
|
|
export class TelegramClient {
|
|
token;
|
|
fetchImpl;
|
|
baseUrl;
|
|
pollingTimeoutSec;
|
|
constructor(token, options = {}) {
|
|
if (!token) {
|
|
throw new Error('bot token is required');
|
|
}
|
|
this.token = token;
|
|
this.fetchImpl = options.fetch ?? createDefaultFetch();
|
|
this.baseUrl = options.baseUrl ?? 'https://api.telegram.org';
|
|
this.pollingTimeoutSec = options.pollingTimeoutSec ?? 30;
|
|
}
|
|
redact(message) {
|
|
return message.split(this.token).join('***');
|
|
}
|
|
async call(method, body) {
|
|
const url = `${this.baseUrl}/bot${this.token}/${method}`;
|
|
try {
|
|
const init = {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
};
|
|
if (body !== undefined) {
|
|
init.body = JSON.stringify(body);
|
|
}
|
|
const response = await this.fetchImpl(url, init);
|
|
const json = (await response.json());
|
|
if (!json.ok) {
|
|
throw new Error(json.description ?? 'Telegram API error');
|
|
}
|
|
return json.result;
|
|
}
|
|
catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
throw new Error(this.redact(message));
|
|
}
|
|
}
|
|
async getMe() {
|
|
return this.call('getMe');
|
|
}
|
|
async getUpdates(offset) {
|
|
const body = {
|
|
timeout: this.pollingTimeoutSec,
|
|
allowed_updates: ['message', 'callback_query'],
|
|
};
|
|
if (offset !== undefined) {
|
|
body.offset = offset;
|
|
}
|
|
return this.call('getUpdates', body);
|
|
}
|
|
async sendMessage(chatId, text, parseMode, replyMarkup) {
|
|
const body = { chat_id: chatId, text };
|
|
if (parseMode !== undefined) {
|
|
body.parse_mode = parseMode;
|
|
}
|
|
if (replyMarkup !== undefined) {
|
|
body.reply_markup = replyMarkup;
|
|
}
|
|
return this.call('sendMessage', body);
|
|
}
|
|
async sendRichMessage(chatId, markdown) {
|
|
const body = {
|
|
chat_id: chatId,
|
|
rich_message: {
|
|
markdown,
|
|
skip_entity_detection: true,
|
|
},
|
|
};
|
|
return this.call('sendRichMessage', body);
|
|
}
|
|
async sendChatAction(chatId, action) {
|
|
return this.call('sendChatAction', { chat_id: chatId, action });
|
|
}
|
|
async answerCallbackQuery(callbackQueryId, text) {
|
|
const body = { callback_query_id: callbackQueryId };
|
|
if (text !== undefined)
|
|
body.text = text;
|
|
return this.call('answerCallbackQuery', body);
|
|
}
|
|
async setMyCommands(commands) {
|
|
return this.call('setMyCommands', { commands });
|
|
}
|
|
async getFile(fileId) {
|
|
return this.call('getFile', { file_id: fileId });
|
|
}
|
|
async downloadFile(filePath) {
|
|
const url = `${this.baseUrl}/file/bot${this.token}/${filePath}`;
|
|
try {
|
|
const response = await this.fetchImpl(url);
|
|
if (!response.ok) {
|
|
throw new Error(`Telegram file download failed: HTTP ${String(response.status)}`);
|
|
}
|
|
return new Uint8Array(await response.arrayBuffer());
|
|
}
|
|
catch (err) {
|
|
const message = err instanceof Error ? err.message : String(err);
|
|
throw new Error(this.redact(message));
|
|
}
|
|
}
|
|
}
|