Files
dsh-telegram-channel/lib/client.js
T
rootandShirobana Agent Bot <tg-bridge> da06485d64 fix(ask): prototype 拦截器替代 waterfall hook + TG 原生按钮作答
根因:dsh 0.1.5-rc.1 把 'user-questions/request' waterfall 分发到
per-fiber events 注册表,其他 fiber 的 ctx.on 监听(含本插件)永远收不到
分发(实测 root/global/agent 作用域监听全部落空)。

修复:包装 UserQuestionService.prototype.ask(类跨实例共享),无论哪个
fiber/实例发起的 ask 都能拦截到 agent+questions;TG 答案与原 ask 竞速,
保留 dual-path 语义;stop 时恢复原型,热重载不叠加。

功能(应用户要求):
- 提问前显式发送预告文本消息
- 选项改用 TG 原生 inline keyboard 按钮(单选即点即答,多选带提交键)
- 文本作答保留并与按钮作答合并,逐题累计、齐题自动提交
- 附带修复:Telegram API 调用加 15s 客户端超时(代理挂死会卡死通知队列)

Co-authored-by: Shirobana Agent Bot <tg-bridge>
2026-09-17 11:15:16 +08:00

131 lines
4.5 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, timeoutMs = 15_000) {
const url = `${this.baseUrl}/bot${this.token}/${method}`;
try {
const init = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
// Without a client-side timeout a stalled proxy connection hangs the
// request forever — and the serialized notice queue wedges behind it,
// silently swallowing every later tool/ask notice. TimeoutError is
// caught below and surfaces as a normal (retryable) failure.
signal: AbortSignal.timeout(timeoutMs),
};
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;
}
// Long-poll: client-side abort must outlive the server-side hold.
return this.call('getUpdates', body, this.pollingTimeoutSec * 1000 + 10_000);
}
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));
}
}
}