Connect a user-created Telegram bot to channel projects with encrypted credentials, numeric sender allowlists, unified execution and text replies. Use Electron networking for system proxy support, with polling recovery, cancellable delivery retries and live status updates. Add schema 61 channel support, lifecycle regression tests, Electron integration probes and feature documentation. Require the performance principles in AGENTS.md and record three alternating baseline/current Main benchmark rounds. Validation: typecheck, lint and diff checks passed. Full suite: 6087 passed, 87 skipped, one intermittent inline-help Electron failure that passed unchanged in isolation. Real Bot plus model acceptance remains pending; the local TLS proxy probe is opt-in. Release note: 新增 Telegram 私聊通道,可配置 Bot Token 和用户白名单,通过通道项目处理文字请求并回复结果。支持系统代理、断线重连和停用取消;真实 Bot 与模型的完整联调仍待完成。
135 lines
5.4 KiB
TypeScript
135 lines
5.4 KiB
TypeScript
import { app, net } from 'electron'
|
|
import { ChannelService } from '../../../src/main/channels/channel-service'
|
|
import { TelegramChannelDriver } from '../../../src/main/channels/telegram-channel-driver'
|
|
|
|
type Update = {
|
|
update_id: number
|
|
message?: {
|
|
date: number
|
|
text?: string
|
|
from?: { id: number; is_bot: boolean }
|
|
chat: { id: number; type: string }
|
|
message_thread_id?: number
|
|
is_topic_message?: boolean
|
|
}
|
|
}
|
|
|
|
function report(code: string): void {
|
|
process.send?.(code)
|
|
}
|
|
|
|
async function bounded<T>(operation: Promise<T>, ms: number): Promise<T> {
|
|
let timer: ReturnType<typeof setTimeout> | undefined
|
|
try {
|
|
return await Promise.race([
|
|
operation,
|
|
new Promise<never>((_resolve, reject) => {
|
|
timer = setTimeout(() => reject(new Error('Probe timeout')), ms)
|
|
})
|
|
])
|
|
} finally {
|
|
clearTimeout(timer)
|
|
}
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
let driver: TelegramChannelDriver | undefined
|
|
let service: ChannelService | undefined
|
|
let exitCode = 1
|
|
try {
|
|
const userData = process.env.GOODBUDDY_PROBE_USER_DATA
|
|
const secret = process.env.TELEGRAM_BOT_TOKEN
|
|
if (!userData || !secret || !process.send) throw new Error('Runner required')
|
|
app.setPath('userData', userData)
|
|
app.setPath('sessionData', userData)
|
|
app.setPath('logs', userData)
|
|
// Keep Chromium caches and diagnostics inside the disposable profile.
|
|
app.commandLine.appendSwitch('disable-logging')
|
|
app.commandLine.appendSwitch('disable-http-cache')
|
|
await bounded(app.whenReady(), 15_000)
|
|
|
|
let selected: Update | undefined = undefined
|
|
const electronFetch: typeof globalThis.fetch = async (input, init) => {
|
|
const url = input instanceof Request ? input.url : String(input)
|
|
const response = await net.fetch(url, init)
|
|
if (!selected || !url.endsWith('/getUpdates') || !response.ok) return response
|
|
const data = await response.json() as { ok?: boolean; result?: Update[] }
|
|
if (data.ok !== true || !Array.isArray(data.result)) throw new Error('Invalid updates')
|
|
// This is the only network adaptation. Receiving, parsing, queuing and sending
|
|
// still run through production code; other queued commands cannot send help.
|
|
return new Response(JSON.stringify({
|
|
ok: true,
|
|
result: data.result.filter((update) =>
|
|
update.update_id === selected?.update_id &&
|
|
update.message?.text === '/goodbuddy_test' &&
|
|
update.message?.from?.id === selected?.message?.from?.id &&
|
|
update.message?.chat.id === selected?.message?.chat.id)
|
|
}), { headers: { 'content-type': 'application/json' } })
|
|
}
|
|
driver = new TelegramChannelDriver({ secret, allowedSenderIds: [], fetch: electronFetch })
|
|
await bounded(driver.testConnection(), 35_000)
|
|
report('connection')
|
|
if (!process.argv.includes('--send-test')) {
|
|
exitCode = 0
|
|
return
|
|
}
|
|
|
|
// No offset: discovery does not acknowledge the queue or alter allowed_updates.
|
|
const response = await net.fetch(`https://api.telegram.org/bot${secret}/getUpdates`, {
|
|
method: 'POST', headers: { 'content-type': 'application/json' },
|
|
body: JSON.stringify({ timeout: 0, limit: 100 }), redirect: 'error',
|
|
signal: AbortSignal.timeout(15_000)
|
|
})
|
|
const data = await response.json() as { ok?: boolean; result?: Update[] }
|
|
if (!response.ok || data.ok !== true || !Array.isArray(data.result)) throw new Error('Discovery failed')
|
|
const now = Math.floor(Date.now() / 1000)
|
|
selected = data.result.filter((update) => {
|
|
const message = update.message
|
|
return Number.isSafeInteger(update.update_id) && update.update_id >= 0 &&
|
|
message?.text === '/goodbuddy_test' && message.chat?.type === 'private' &&
|
|
Number.isSafeInteger(message.from?.id) && message.from!.id > 0 &&
|
|
message.from?.is_bot === false && message.chat.id === message.from.id &&
|
|
message.message_thread_id === undefined && !message.is_topic_message &&
|
|
Number.isSafeInteger(message.date) && message.date >= now - 86_400 && message.date <= now
|
|
}).sort((a, b) => b.message!.date - a.message!.date || b.update_id - a.update_id)[0]
|
|
if (!selected) {
|
|
report('missing')
|
|
return
|
|
}
|
|
report('selected')
|
|
const allowedSenderIds = [String(selected.message!.from!.id)]
|
|
driver.updateAllowedSenderIds(allowedSenderIds)
|
|
let delivered!: () => void
|
|
const delivery = new Promise<void>((resolve) => { delivered = resolve })
|
|
service = new ChannelService(driver, async (message) => {
|
|
if (message.eventId !== String(selected!.update_id) || message.text !== '/goodbuddy_test') {
|
|
throw new Error('Unexpected event')
|
|
}
|
|
return {
|
|
status: 'completed',
|
|
output: 'GoodBuddy Telegram channel test passed using the production driver and ChannelService. No AI model or tools were used.'
|
|
}
|
|
}, { allowedSenderIds, maximumConcurrency: 1, onDeliverySuccess: delivered })
|
|
await service.start()
|
|
await bounded(delivery, 45_000)
|
|
report('delivered')
|
|
exitCode = 0
|
|
} catch {
|
|
report('failed')
|
|
} finally {
|
|
try {
|
|
await bounded(Promise.all([service?.stop(), driver?.stop()]), 5_000)
|
|
} catch {
|
|
report('shutdown')
|
|
exitCode = 1
|
|
}
|
|
process.exitCode = exitCode
|
|
app.quit()
|
|
setTimeout(() => app.exit(exitCode), 1_000).unref()
|
|
}
|
|
}
|
|
|
|
process.on('uncaughtException', () => { report('failed'); app.exit(1) })
|
|
process.on('unhandledRejection', () => { report('failed'); app.exit(1) })
|
|
void main()
|