feat: add Telegram private messaging channel

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 与模型的完整联调仍待完成。
This commit is contained in:
mesalogo
2026-10-05 02:42:28 +08:00
parent 78afec7775
commit cf598121b1
56 changed files with 4144 additions and 194 deletions
+8 -2
View File
@@ -1,14 +1,20 @@
# Telegram 消息通道
Telegram 消息通道计划通过用户自行创建的 Bot,将 Telegram 私聊接入 GoodBuddy 通道项目,支持文字问答和执行任务。首版采用长轮询,运行 GoodBuddy 的电脑需要在线并能访问 Telegram。
Telegram 消息通道通过用户自行创建的 Bot,将授权用户的普通私聊文字接入 GoodBuddy 通道项目,复用项目的模型、Runtime 和工作目录。首版采用长轮询,运行 GoodBuddy 的电脑需要在线;生产驱动通过 Electron `net.fetch` 使用系统代理访问 Telegram。
## 文档导航
| 文档 | 权威职责 |
| --- | --- |
| [Telegram 通道 PRD](./prd.md) | 产品目标、首版范围、配置流程、功能要求、限制与验收标准 |
| [技术设计](./technical-design.md) | 生产模块、身份与持久化、IPC 状态、生命周期及实现边界 |
| [实施与验证进度](./progress.md) | 日期化验证证据、当前阻塞与待验收项目 |
| [API 联调操作说明](./api-testing.md) | 两类探针的命令、网络路径、输出含义及副作用 |
| [独立 Node 探针](./telegram-probe.mjs) | 独立调用 Bot API,不经过生产驱动或模型 |
| [生产探针启动器](./run-live-probe.mjs) | 构建探针、启动隔离 Electron 进程并清理临时目录 |
| [生产探针入口](./live-probe-main.ts) | 生产驱动连接检查及可选 ChannelService 固定回复,不调用模型 |
当前处于 PRD 草案阶段,尚未实现 Telegram 通道。详细逻辑、UI 和技术设计在实现前按需补充,不以本目录的建立作为功能完成证据。
生产接入代码与本地自动化验证已落地,真实 Bot 与模型的完整链路尚未证明,不能记为全面验收通过。当前结果和待办以[进度记录](./progress.md)为准;2026-10-04 独立 Node 探针成功仅是历史 API 证据。
## 术语与依赖
@@ -0,0 +1,109 @@
# Telegram API 联调
## 用途与边界
[run-live-probe.mjs](./run-live-probe.mjs) 启动 Electron,运行 [live-probe-main.ts](./live-probe-main.ts),检查生产驱动及可选的 ChannelService 固定回复。[telegram-probe.mjs](./telegram-probe.mjs) 是独立 Node API 诊断脚本,没有接入 GoodBuddy 项目、会话、白名单、Runtime 或发件箱。
两类探针都不调用模型,成功不能代替真实 Bot 与模型完整链路验收。生产驱动使用 Electron `net.fetch` 和系统代理,没有引入 grammY 或 Telegraf;独立脚本使用 Node `fetch`。当前结果和 2026-10-04 历史记录统一见[进度记录](./progress.md)。本次生产实时探针未运行,原因是 `TELEGRAM_BOT_TOKEN` 不存在。
## 生产驱动探针
从仓库根目录运行,需要已安装项目依赖(Electron、esbuild)。启动器在临时目录构建生产源码,使用隔离的 Electron 用户目录,退出后清理。可用 `--temp-parent PATH` 指定已存在的临时父目录。
```powershell
node "docs/features/telegram-channel/run-live-probe.mjs" --dry-run
```
`--dry-run` 只构建并检查语法,不启动 Electron,不发网络请求,不需要 Token。它不能证明代理路径可用。实时连接检查使用下面命令;隐藏输入仅放入进程环境,不写文件。
```powershell
$telegramProbeSecret = Read-Host 'Telegram Bot Token' -AsSecureString
$env:TELEGRAM_BOT_TOKEN = [System.Net.NetworkCredential]::new('', $telegramProbeSecret).Password
try {
node "docs/features/telegram-channel/run-live-probe.mjs"
} finally {
Remove-Item Env:TELEGRAM_BOT_TOKEN -ErrorAction SilentlyContinue
$telegramProbeSecret = $null
}
```
默认仅通过生产驱动执行 `getMe` / `getWebhookInfo`,不轮询、不发送。`TELEGRAM_BOT_TOKEN` 是探针输入,产品只读环境配置使用 `GOODBUDDY_TELEGRAM_BOT_TOKEN`,两者不能混用。
要验证固定回复,先停用该专用测试 Bot 的其他消费者,在普通私聊发送精确文本 `/goodbuddy_test`,再将上述运行命令替换为:
```powershell
node "docs/features/telegram-channel/run-live-probe.mjs" --send-test
```
发送模式从首批最多 100 条积压中选择最近 24 小时内最新的合格私聊标记,临时授权该发送者,经生产驱动和 ChannelService 返回固定文字。网络适配层只把选中的事件交给驱动,其他事件不会触发帮助或任务;使用临时内存状态和固定 executor,不经过产品设置页、生产模型或持久化发件箱。
发送模式会启动生产轮询、设置 `allowed_updates=['message']` 并可能推进 offset,因而可能确认或消费更早的积压。仅对专用空闲 Bot 使用;这与独立 Node 探针不推进 offset 的行为不同。无匹配标记时失败退出,不等待用户补发。
| 输出 | 含义 |
| --- | --- |
| `PASS: production driver getMe/getWebhookInfo.` | 生产驱动身份及 webhook 检查成功 |
| `PASS: production driver and ChannelService delivered the test reply.` | 固定回复 API 成功,仍需手机核对展示;未调用模型 |
| `FAIL: no eligible marker...` | 首批积压无合格标记,收发未验证 |
| 其他 `FAIL` 或非零退出 | 设置、构建、网络、停止或清理失败,不算通过 |
启动器只转发固定结果码,抑制可能包含 Token URL 的 Electron 输出,并强制 TLS 校验。成功退出要求对应检查完成且子进程正常退出。
## 独立 Node 探针
默认只调用 `getMe` 和 `getWebhookInfo`,不读取消息或发送回复。添加 `--listen` 后,在无 webhook 的情况下进行约 45 秒长轮询,仅对运行开始后收到的私聊 `/goodbuddy_test` 回复一条固定测试文字,然后退出。不处理其他消息、不调用模型、不下载附件。
脚本不推进更新 offset、不修改订阅类型、不删除 webhook。已读取的更新仍可能被后续消费者再次读取;请使用专用 Bot,避免与其他轮询进程同时运行。积压达到单批 100 条时停止测试,不主动确认或清空消息。固定回复发生网络结果不明时不自动重试。
### 运行方法
历史实测环境为 Windows、PowerShell 5.1、Node.js 24.21.0。以下命令从仓库根目录执行,Token 通过隐藏输入传入当前进程环境,不保存在文件中。不要把实际 Token 写进命令、文档或 Git。
```powershell
$telegramProbeSecret = Read-Host 'Telegram Bot Token' -AsSecureString
$env:TELEGRAM_BOT_TOKEN = [System.Net.NetworkCredential]::new('', $telegramProbeSecret).Password
try {
node --use-env-proxy "docs/features/telegram-channel/telegram-probe.mjs"
} finally {
Remove-Item Env:TELEGRAM_BOT_TOKEN -ErrorAction SilentlyContinue
$telegramProbeSecret = $null
}
```
`--use-env-proxy` 需要支持该选项的 Node 版本,脚本不会自动读取 Windows 系统代理。若机器依赖系统代理,先按下面方式将当前代理临时提供给 Node;这不会修改操作系统代理配置。结束后恢复原有环境值。
```powershell
$telegramProbePreviousProxy = $env:HTTPS_PROXY
$telegramProbeProxy = [System.Net.WebRequest]::GetSystemWebProxy().GetProxy([Uri]'https://api.telegram.org')
if ($telegramProbeProxy.Host -ne 'api.telegram.org') {
$env:HTTPS_PROXY = $telegramProbeProxy.AbsoluteUri
}
# 在此执行上面的凭据输入和测试命令。
$env:HTTPS_PROXY = $telegramProbePreviousProxy
```
测试真实收发时,将运行命令改为:
```powershell
node --use-env-proxy "docs/features/telegram-channel/telegram-probe.mjs" --listen
```
看到提示后,在 Telegram 打开测试 Bot 的普通私聊,发送 `/goodbuddy_test`。脚本成功发送固定回复后输出 `privateMessageRoundTrip` 的 `ok: true`。用户还需在手机端确认实际收到回复。
如果已发送测试指令,使用 `--pending` 替代 `--listen`。脚本单次查询排队消息,选择最近一小时内最新的私聊 `/goodbuddy_test` 并回复,无需在监听窗口内发送。该选项同样不推进 offset,重复运行可能再次回复同一条指令,仅在需要复测时使用。
脚本强制开启 TLS 证书校验,不沿用外部环境中的关闭校验设置。输出仅包含检查结果、Bot 用户名、是否配置 webhook、积压数量和有界错误码;不打印 Token、完整请求地址、原始异常、消息正文或聊天 ID。
### 结果解释
| 输出或退出码 | 含义 |
| --- | --- |
| 退出码 0,默认检查 | Bot 身份及 webhook 状态查询成功;未验证收发 |
| 退出码 0,`privateMessageRoundTrip` 成功 | 收到测试指令,发送回复 API 返回成功 |
| 退出码 2,`no_matching_message` | 时间窗口内未收到符合条件的测试指令,收发待验证 |
| 退出码 1 | 网络、凭据、平台错误或测试条件不满足 |
| `UND_ERR_CONNECT_TIMEOUT` | 网络连接超时,优先检查当前进程的代理路径 |
| API error 401 | 凭据无效或已撤销 |
| API error 409 | 检查 webhook 或其他长轮询实例是否占用 |
| API error 429 | 平台限流;脚本报告等待时间,不自动继续请求 |
日期化实测结果见[进度记录](./progress.md)。独立 Node 请求走通不能证明 Electron 生产网络路径走通;固定回复成功也不能证明模型或工具任务成功。
@@ -0,0 +1,134 @@
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()
+24 -21
View File
@@ -4,20 +4,20 @@
| 项目 | 内容 |
| --- | --- |
| 状态 | 草案,待评审;Telegram 通道尚未实现 |
| 版本 | 0.1 |
| 日期 | 2026-10-04 |
| 状态 | 已按生产实现修订;真实 Bot 与模型完整链路待验收 |
| 版本 | 0.2 |
| 日期 | 2026-10-05 |
| 适用产品 | GoodBuddy 桌面端 |
| 首版范围 | 单 Bot、普通私聊、文字问答与工具任务、最终结果回复 |
| 接入方式 | 用户通过 BotFather 创建 Bot,GoodBuddy 使用 Bot API 长轮询 |
本文定义拟交付的产品行为。验收表描述未来交付标准,不代表已经通过测试。BotFather 接入方向来自本次讨论;范围取舍和异常行为是本草案提出的方案。
本文定义首版产品行为和验收标准,已按当前实现修订。验收表不代表已经通过测试;验证结果及剩余工作由[进度记录](./progress.md)维护,模块与协议细节见[技术设计](./technical-design.md)。
## 1. 用户问题与目标
用户离开电脑后,希望通过 Telegram 向 GoodBuddy 提问或发起任务,并在手机上收到结果。任务应使用桌面端已经配置的模型、Runtime 和工作目录,执行过程与对话历史仍可在 GoodBuddy 查看。
当前远程消息通道已有微信 ClawBot、企业微信和钉钉,尚无 Telegram 入口。新增通道应沿用现有“通道项目 + 远程会话”结构,让用户通过创建 Bot、填写凭据和配置允许访问的用户完成接入,无需自行部署消息中转服务。
Telegram 沿用现有“通道项目 + 远程会话”结构,与微信 ClawBot、企业微信和钉钉并列。用户通过创建 Bot、填写凭据和配置允许访问的用户完成接入,无需自行部署消息中转服务。
首版完成以下使用流程:
@@ -35,7 +35,7 @@
| Bot 数量 | 每个 GoodBuddy 用户配置一个 Telegram Bot |
| 会话类型 | 普通私聊;不同用户的会话相互隔离 |
| 输入 | 文字消息和受支持的命令 |
| 执行 | 复用渠道项目的处理后端和统一执行规则,无产品模式 |
| 执行 | 复用渠道项目的处理后端、已启用能力和统一执行规则 |
| 输出 | 任务进行提示、最终文字结果、长回复分段 |
| 管理 | Token 保存和替换、用户 ID 白名单、连接测试、启停和状态展示 |
| 故障处理 | 临时断网重连、平台限流等待、已保存结果的有限重试 |
@@ -63,7 +63,7 @@
1. 用户在 GoodBuddy 远程通道设置中打开 Telegram 卡片,点击官方 BotFather 链接。
2. 用户在 BotFather 发送 `/newbot`,设置显示名称和符合 Telegram 要求的用户名,获得 Token。
3. 用户回到 GoodBuddy 填写 Token,执行“测试连接”。成功后显示 Bot 名称、用户名和打开 Bot 的链接。
3. 用户回到 GoodBuddy 填写 Token,执行“测试连接”。成功后显示 API 返回的 Bot 用户名和打开 Bot 的链接,不展示 Bot 显示名称。
4. 用户先保存凭据并以空白名单启用连接,卡片显示“尚未授权用户”。此时仅提供私聊帮助,不接受业务请求。
5. 用户在 Telegram 打开该 Bot,发送 `/start`,再发送 `/whoami` 获取自己的数字用户 ID。卡片说明必须由用户先发起私聊。
6. 用户将该 ID 填入 GoodBuddy 白名单并保存,随后发送普通文本,收到回复,并在 Telegram 项目中看到对应会话。
@@ -82,9 +82,9 @@
### FR-2 凭据管理
Token 使用现有加密凭据存储,保存后不在普通设置快照中返回明文。用户可以保留、替换或清除凭据。日志、连接错误和会话内容不得包含 Token 或含 Token 的完整请求地址。
Token 使用现有加密凭据存储,保存后不在普通设置快照中返回明文。用户可以保留、替换或清除凭据;环境变量覆盖时配置只读。日志、连接错误和会话内容不得包含 Token 或含 Token 的完整请求地址。
替换 Token 前验证 Bot 身份。若仍为同一个 Bot,继续使用原会话;若为另一个 Bot,新会话与旧 Bot 隔离,旧 Bot 的待发结果不得经新 Bot 发送。清除凭据停止连接,保留项目与历史。
替换 Token 前验证 Bot 身份,验证失败保留原配置与连接。若仍为同一个 Bot,继续使用原会话;若为另一个 Bot,新会话与旧 Bot 隔离,旧 Bot 的待发结果不得经新 Bot 发送。在设置页清除凭据并保存时同时停用通道,保留项目与历史。
### FR-3 连接测试
@@ -102,14 +102,15 @@ Token 使用现有加密凭据存储,保存后不在普通设置快照中返
### FR-5 会话与连续上下文
同一 Bot 下同一普通私聊复用远程会话,不同用户和不同 Bot 的上下文隔离。会话显示来源和发送者名称;名称不可用时使用用户 ID 作为可识别的回退信息。名称变化不产生新身份。
同一 Bot 下同一普通私聊复用远程会话,不同用户和不同 Bot 的上下文隔离。会话归属 Telegram 项目,发送者以数字用户 ID 标识;首版不采集发送者显示名称。
同一会话按接收顺序处理业务消息。用户连续发送消息时,不得使后发请求抢用尚未完成的上一轮上下文。不同会话沿用现有通道并发限制。
### FR-6 执行范围
普通文本使用项目后端与能力配置;`/ask`、`/execute` 和 `/exec` 沿用现有渠道的退役规则,
作为普通正文保留,不切换权限。仅含旧命令的非空消息也按普通消息处理。
授权用户直接发送自然语言请求,使用项目后端与能力配置完成问答或工具任务,无需选择或切换工作模式。通道不提供模式选择器、默认模式设置或模式切换命令。
`/ask`、`/execute` 和 `/exec` 按现有统一执行规则作为普通正文保留,不剥离前缀、不切换权限;仅含旧命令的非空消息也按普通消息处理。帮助文案不再介绍这些前缀。
按[统一执行规则](../unified-execution/logic-design.md)使用已启用工具,不增加 Telegram 专属
逐工具审批。设置页显示默认工作目录和后端,并说明允许访问的用户可发起操作。
@@ -119,7 +120,7 @@ Token 使用现有加密凭据存储,保存后不在普通设置快照中返
`/start` 返回 Bot 用途、授权配置说明和基础用法;`/whoami` 仅返回当前发送者的数字用户 ID。上述命令不写入业务上下文。
授权用户发送未知斜杠命令时,返回支持的命令列表,不将其当成执行请求。编辑历史消息不自动重新执行任务。
除 `/start` 和 `/whoami` 外,授权用户发送的非空文字均按普通请求处理,包括带斜杠前缀的文字,不额外引入命令兼容规则。空白消息按现有输入规则拒绝。编辑历史消息不自动重新执行任务。
授权用户发送媒体时,提示“当前仅支持文字消息,请改用文字描述”。带说明文字的媒体首版同样提示不支持,不丢弃媒体后擅自执行说明文字。未授权媒体不触发处理。
@@ -127,7 +128,7 @@ Token 使用现有加密凭据存储,保存后不在普通设置快照中返
请求进入处理后,Telegram 侧显示输入状态或简短进行提示。该提示不表示任务完成,也不表示任务可以在应用崩溃后恢复。
任务完成后发送最终文字;失败时发送脱敏原因及必要的恢复建议。回复尽量关联原始消息;原消息不可引用时仍应尝试发送结果。长文本按平台上限顺序分段,不破坏 Unicode 字符,不因 Markdown 转义问题造成整条回复失败。
任务完成后发送最终文字;失败时发送脱敏原因及必要的恢复建议。回复作为普通私聊消息发送,首版不引用原始消息。长文本以纯文本顺序分段,保留 Unicode 代理对,不启用 Markdown 解析。
首版不发送附件。若任务产生图片或文件,文字回复说明成果可在 GoodBuddy 中查看,不宣称附件已发送,也不向用户发送仅本机可访问的路径作为下载链接。
@@ -137,7 +138,7 @@ Token 使用现有加密凭据存储,保存后不在普通设置快照中返
已生成并保存的结果沿用现有发件箱有限重试,发送失败与任务执行失败分别记录。补发回复不重新调用模型或重跑任务。用户阻止 Bot 等不可恢复错误应停止无效重试。
多段回复部分成功后,应尽量从未发送部分继续。网络中断导致无法确定平台是否收到某段时,允许重试出现重复段落,首版不承诺平台级发送幂等。
同一驱动实例内且分段进度仍在缓存时,多段回复从未发送部分继续。进度不跨重启持久化,重启或缓存淘汰后补发可能重复已发送段落;网络中断造成平台接收结果不明时也可能重复。首版不承诺平台级发送幂等。
### FR-10 启停与历史保留
@@ -157,7 +158,9 @@ Telegram 待接收更新最多保留 24 小时,超过平台保留期的离线
### FR-12 状态与诊断
设置页区分未配置、已停用、正在连接、已连接、正在重连和连接失败。空白名单作为“尚未授权用户”的配置提示展示,不与网络状态混为一谈。
设置页展示未配置、已停用、正在连接、已连接、已停止和连接失败。重连时显示正在连接,并附带错误原因与重连说明;没有独立的重连状态值。空白名单作为“尚未授权用户”的配置提示展示,不与网络状态混为一谈。
状态和错误推送只更新状态区域,不覆盖尚未保存的 Token 或白名单草稿。解决轮询冲突后可重新保存当前配置以启动连接;单独测试连接成功不改变接收状态。
状态通过文字表达,不能只靠颜色。错误保留可操作原因,如 Token 无效、网络不可达、webhook 占用或另一实例正在轮询。测试成功、发送失败和模型失败使用各自准确文案,避免统一显示“连接失败”。
@@ -174,15 +177,15 @@ Telegram 待接收更新最多保留 24 小时,超过平台保留期的离线
| AC-03 | FR-2、3 | 通道运行时连续测试连接,不争抢轮询、不消费业务消息;无效 Token 与 webhook 占用分别提示 |
| AC-04 | FR-5 | 同一用户连续两轮对话保留上下文;两名授权用户的内容不相互进入上下文 |
| AC-05 | FR-6 | 普通请求可在专用测试目录完成一次无害工具任务;旧模式前缀作为正文保留,结果和活动可在桌面核对,无默认模式字段 |
| AC-06 | FR-4、7 | 未授权用户、群消息、话题消息和媒体不触发任务;授权媒体、未知命令和空命令获得准确提示 |
| AC-06 | FR-4、7 | 未授权用户、群消息、话题消息和媒体不触发任务;授权媒体获得不支持提示;内置帮助命令不调用模型,其他斜杠前缀按正文保留,空白消息被拒绝 |
| AC-07 | FR-8 | 超过单条平台上限的回复按序完整展示,中文和表情字符完整;任务失败返回脱敏原因 |
| AC-08 | FR-8 | 生成文件的任务在 Telegram 中提示桌面查看成果,不出现“附件已发送”的错误表述 |
| AC-09 | FR-9、12 | 模拟短暂断网、429 和永久发送错误,分别重连、等待和停止无效重试;任务执行结果不被误记为执行失败 |
| AC-10 | FR-9 | 已保存结果发送失败后恢复连接可补发,补发不增加模型调用或再次执行任务;验证部分分段成功情形 |
| AC-11 | FR-10 | 停用通道后新消息不启动任务,当前与排队任务取消;历史与成果保留;重新启用不重跑取消任务 |
| AC-11 | FR-2、10 | 停用通道后新消息不启动任务,当前与排队任务取消;启用时清除 Token 并保存会同时停用;项目、历史与成果保留;重新启用不重跑取消任务 |
| AC-12 | FR-2、5 | 同一 Bot 换 Token 保留会话;更换 Bot 后上下文隔离,旧待发结果不会由新 Bot 发出 |
| AC-13 | FR-4、11 | 重投同一更新不重复执行;离线后恢复可接收平台保留消息;已撤销用户的积压消息不执行 |
| AC-14 | FR-2、12 | 设置快照、日志及错误不泄漏 Token;凭据轮换、网络错误和 Runtime 错误均可定位 |
| AC-14 | FR-2、12 | 设置快照、日志及错误不泄漏 Token;凭据轮换、网络错误和 Runtime 错误均可定位;连接错误及重连状态推送保留未保存的 Token 和白名单草稿 |
| AC-15 | FR-1、10 | 新增 Telegram 后微信、企业微信和钉钉的设置、启停与历史读取保持原行为 |
| AC-16 | FR-9、10 | 有已保存回复等待重试时停用通道,确认重试停止;重新启用后补发该 Bot 的结果,任务执行次数不增加 |
| AC-17 | FR-11 | 在专用测试任务执行期间异常退出应用,重启后不自动重跑该中断任务;已有任务记录可查询,不把未取得的完成结果显示为成功;帮助说明保留人工核对后重发的限制 |
@@ -193,9 +196,9 @@ Telegram 待接收更新最多保留 24 小时,超过平台保留期的离线
通道项目、工作目录、Runtime 选择和授权语义复用[现有远程消息通道约定](../wechat-channel/prd.md)。Telegram 的外部网络可达性、Bot 状态和平台限额影响收发,连接测试无法证明后续每次任务或文件生成都能成功。
开发前需核对主进程网络与代理路径,以及现有通道停用、去重和发件箱行为是否满足本文要求。差异应在对应技术设计中解决并关联 FR 编号,不能只增加设置入口后宣称完成。本文不规定新增 SDK、数据库表或通用恢复框架。
生产驱动使用 Electron 网络栈和系统代理。停用、去重、发件箱与状态推送的实现见[技术设计](./technical-design.md),实际覆盖与未完成验证见[进度记录](./progress.md)。没有测量数据时,不宣称性能提升或性能预算通过。
前期讨论提出过“持久化入站后再确认”的方案。本草案将其列为后续可靠性评估,首版采用 FR-11 的明确限制;若后续要求已接收任务跨崩溃恢复,再依据实际故障与业务需求评估存储和执行恢复成本。
持久化入站后再确认仍属于后续可靠性评估,首版采用 FR-11 的限制;若后续要求已接收任务跨崩溃恢复,再依据实际故障与业务需求评估存储和执行恢复成本。
| 后续方向 | 进入设计前需明确的内容 |
| --- | --- |
@@ -0,0 +1,68 @@
# Telegram 实施与验证进度
## 2026-10-05 当前结果
生产设置入口、驱动、项目路由、执行、持久化和状态推送已接入,行为见[技术设计](./technical-design.md)。真实 Telegram Bot 与模型的完整链路尚未证明,[PRD 验收](./prd.md#5-产品验收)仍待完成。
以下计数来自本次开发会话提供的验证结果,本轮文档修订未重跑这些测试。集合可能重叠,不相加计算覆盖总数。
| 验证范围 | 结果 | 证据边界 |
| --- | --- | --- |
| Driver / Service / Manager 聚焦测试 | 74 通过 | 解析、授权、重试、身份隔离与生命周期等自动化覆盖 |
| 集成测试集合 | 251 通过,1 跳过 | 包含 Electron 生产 UI / IPC / store / executor 与本地模型 fixture,Telegram 网络响应为替身 |
| Electron 场景 | 2 次本地模型调用,0 次外部调用 | 配置测试与加密保存、授权和去重、结果入库与回复、重载恢复、停用取消;错误及重连状态推送保留草稿;启用状态清除 Token 后停止连接并保留项目和历史 |
| Typecheck / lint | 最终复跑均通过 | 静态检查,不能替代真实 Bot 联调 |
| 全量测试原始结果 | 6086 通过,86 跳过,2 失败 | 保留当次失败记录;后续修复与复跑见下两行,尚无新的全量通过结论 |
| Schema 断言修复 | 已改用 schema 版本常量;四个 suite 共 301 通过 | 修复原先预期 60、实际 61 的 worker 断言,不将局部复跑计为全量通过 |
| OpenCode cleanup-timeout 复跑 | 单独运行 3 次通过,整个 runtime suite 通过 | 原因尚未证明,记录为全量运行中出现的间歇性失败,不宣称已修复 |
| 最终全量测试 | 6087 通过,87 跳过,1 失败;耗时 879.68 秒 | 唯一失败为 `tests/inline-help.electron.test.ts` 的 Hover Escape 场景;原样单独复跑通过,仍保留间歇性失败记录。此前 schema 和 Runtime 失败未在本轮出现 |
| 生产实时探针 | 本次未运行 | 当前会话缺少 `TELEGRAM_BOT_TOKEN` |
| 生产探针 dry-run | 打包与语法检查通过 | 未启动 Electron,未发出网络请求 |
| 真实本地代理 fixture | 真实 Electron 生产默认传输曾成功运行 1 次;最终运行因临时 CA 信任导入超时失败 | 后续 Windows 临时 CA 信任导入需要操作系统确认;测试已改为显式启用,默认跳过。不宣称可无人值守复现 |
| Main 性能测量 | 基线与当前版本交替各运行 3 轮 | 结果见下节;没有证据证明性能改善或 Telegram 引入回退,不宣称全局预算通过 |
Electron 证据对应 [`tests/telegram-channel.electron.test.ts`](../../../tests/telegram-channel.electron.test.ts) 与 [`tests/support/telegram-main.ts`](../../../tests/support/telegram-main.ts)。它使用生产组件、Preload、IPC、配置存储、executor 和 SQLite;在 `net.fetch` 边界替换 Telegram 响应,模型为本地 HTTP fixture。因此它验证内部产品路径,不能证明外部 Telegram 网络、系统代理或真实模型服务可用。
可复跑的聚焦命令:
```powershell
npx vitest run src/main/channels/telegram-channel-driver.test.ts src/main/channels/channel-service.test.ts src/main/channels/channel-manager.test.ts
npx vitest run tests/telegram-channel.electron.test.ts
npm run typecheck
npm run lint
npm test
```
集成集合及 301 项复跑的完整命令未随计数提供,这里不推测其文件清单。真实本地代理 fixture 见 [`tests/telegram-proxy.electron.test.ts`](../../../tests/telegram-proxy.electron.test.ts);其本地代理结果不证明外部 Telegram Bot 可用。探针操作见 [API 联调](./api-testing.md),探针成功也不包含模型验收。
## Main 性能对比
基线为 `78afec77758f3c175f8e2b80e5be4327e13b4689` 的临时 detached worktree,当前为包含 Telegram 改动的工作区。按基线、当前交替执行 `npm run perf:main` 各 3 次。环境为 Windows x64、Intel Core Ultra X7 358H、Electron 44.5.1、Node 24.21.0;每轮使用隔离数据库,包含 1000 个会话、20008 条生成消息、24000 条活动记录,每个场景预热后测量 15 次,无外部模型调用。
下表为每轮调用耗时 p95,单位为毫秒。对比相同生产路径,不将脚本内部的历史同步算法视为基线版本。
| 场景 | 基线三轮 | 当前三轮 |
| --- | --- | --- |
| 会话摘要 worker | 25.0 / 20.9 / 29.3 | 18.8 / 26.6 / 53.3 |
| 最大会话 worker | 1.1 / 0.6 / 1.1 | 0.8 / 1.0 / 1.2 |
| 活动增量更新 | 1.3 / 2.3 / 2.0 | 1.3 / 1.5 / 1.7 |
| 活动启动 worker | 17.7 / 18.7 / 29.2 | 13.7 / 23.0 / 25.2 |
| 整份活动替换,一条变化 | 61.8 / 84.9 / 73.1 | 52.0 / 84.0 / 73.8 |
worker 读取、活动启动、增量更新和同步首页查询在六轮中均满足 Main 阻塞与事件循环延迟预算;当前这些路径的最大循环间隔为 3.7 ms,最高循环延迟 p99 为 4.4 ms。整份活动替换在两个版本中均超预算,当前最大循环间隔为 47.0–84.2 ms;同步活动汇总也存在超过 16 ms 的情况。这些已有路径不属于本次 Telegram 优化范围。
版本差异与轮次波动交叠,不能据此归因于 Telegram 或声称性能提升。测量在新数据库初始化后进行,不覆盖已填充 schema 60 数据库升级到 61 的耗时、Telegram 流量或完整 UI 响应。原始结果保存在本次机器临时目录 `telegram-main-results-20261005-a`,不作为仓库依赖。
## 2026-10-04 历史 API 证据
独立 `telegram-probe.mjs` 在 Windows / PowerShell 5.1 / Node.js 24.21.0 下测试。首次直连曾有关闭 TLS 校验的环境配置,不作为正常 HTTPS 证据;开启校验后直连超时。将 Windows 系统代理临时传给 Node 后,`getMe`、`getWebhookInfo` 成功,无 webhook。
初次监听窗口未收到匹配指令;随后 `--pending` 从 2 条积压中匹配 `/goodbuddy_test`,`sendMessage` 返回成功,输出 `privateMessageRoundTrip: ok=true`。没有模型调用,手机端实际展示仍需用户确认。该记录只证明当日独立 Node API 路径,不证明当前凭据有效、生产驱动可用或完整产品验收通过。历史凭据曾公开,应撤销后换新,文档不保留 Token。
## 待完成
- 在可完成操作系统信任确认的环境中复跑显式启用的代理测试,记录最终结果;保留单次成功和后续导入超时两项证据,不把默认跳过视为通过。
- 跟踪全量测试中的 Runtime 清理与帮助弹层间歇性失败;两者均有原样复跑通过证据,未为此修改无关产品逻辑。
- 提供有效测试凭据后运行生产实时探针,记录连接及固定回复结果;本次缺少凭据的阻塞不以历史 Node 成功替代。
- 从真实设置页保存并启用专用 Bot,在手机发送授权请求,经过实际模型产生回复并核对桌面历史;再按 PRD 验证连续上下文、隔离、无害工具任务和恢复场景。当前没有真实 Bot 与模型完整链路的通过记录。
- Main 测量未覆盖迁移和完整 UI;后续验收需按[性能原则](../../architecture/performance-principles.md)区分实测路径与未测路径。
@@ -0,0 +1,102 @@
/* global process, console, setTimeout, clearTimeout, URL */
import { spawn, spawnSync } from 'node:child_process'
import { mkdtemp, mkdir, rm, stat } from 'node:fs/promises'
import { createRequire } from 'node:module'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { build } from 'esbuild'
const require = createRequire(import.meta.url)
const messages = {
connection: 'PASS: production driver getMe/getWebhookInfo.',
selected: 'Selected a queued private test marker from the last 24 hours.',
delivered: 'PASS: production driver and ChannelService delivered the test reply.',
missing: 'FAIL: no eligible marker in the pending batch (maximum 100 updates).',
failed: 'FAIL: live probe failed; network details suppressed.',
shutdown: 'FAIL: probe shutdown did not complete.'
}
async function main() {
const args = process.argv.slice(2)
let tempParent = tmpdir()
for (let i = 0; i < args.length; i += 1) {
if (args[i] === '--temp-parent' && args[i + 1]) tempParent = args[++i]
else if (!['--dry-run', '--send-test'].includes(args[i])) {
console.error('Usage: node run-live-probe.mjs [--dry-run | --send-test] [--temp-parent PATH]')
process.exitCode = 1
return
}
}
const dry = args.includes('--dry-run')
const send = args.includes('--send-test')
if (dry && send) throw new Error('Invalid mode')
if (!dry && !/^\d+:[A-Za-z0-9_-]+$/.test(process.env.TELEGRAM_BOT_TOKEN ?? '')) {
console.error('Set TELEGRAM_BOT_TOKEN in the environment before running live.')
process.exitCode = 1
return
}
if (!(await stat(tempParent)).isDirectory()) throw new Error('Invalid temporary parent')
const directory = await mkdtemp(join(tempParent, 'goodbuddy-telegram-live-'))
try {
const outfile = join(directory, 'probe.cjs')
await build({
entryPoints: [fileURLToPath(new URL('./live-probe-main.ts', import.meta.url))],
outfile, bundle: true, platform: 'node', format: 'cjs', target: 'node22',
external: ['electron'], logLevel: 'silent'
})
const check = spawnSync(process.execPath, ['--check', outfile], {
stdio: 'ignore', timeout: 15_000
})
if (check.status !== 0) throw new Error('Bundle syntax check failed')
const electron = require('electron')
if (typeof electron !== 'string') throw new Error('Electron executable unavailable')
if (dry) {
console.log('PASS: bundled production sources and checked syntax; Electron not launched, no network calls.')
return
}
if (send) {
console.log('Send mode: use a dedicated idle bot. Polling may consume/acknowledge earlier queued updates.')
console.log('Only the selected exact /goodbuddy_test event receives a fixed reply; no model or tools run.')
}
const userData = join(directory, 'user-data')
await mkdir(userData)
const env = { ...process.env, GOODBUDDY_PROBE_USER_DATA: userData }
delete env.ELECTRON_RUN_AS_NODE
delete env.ELECTRON_ENABLE_LOGGING
delete env.ELECTRON_LOG_FILE
delete env.NODE_OPTIONS
env.NODE_TLS_REJECT_UNAUTHORIZED = '1'
process.exitCode = await new Promise((resolve) => {
// Never forward Electron/Chromium stderr: it may contain credential-bearing URLs.
const child = spawn(electron, [outfile, ...(send ? ['--send-test'] : [])], {
env, stdio: ['ignore', 'ignore', 'ignore', 'ipc'], windowsHide: true
})
let passed = false
const timer = setTimeout(() => child.kill('SIGKILL'), 125_000)
const interrupt = () => child.kill('SIGKILL')
process.once('SIGINT', interrupt)
process.once('SIGTERM', interrupt)
child.on('message', (code) => {
if (typeof code !== 'string' || !Object.hasOwn(messages, code)) return
console.log(messages[code])
if (code === (send ? 'delivered' : 'connection')) passed = true
})
child.once('error', () => resolve(1))
child.once('close', (code) => {
clearTimeout(timer)
process.removeListener('SIGINT', interrupt)
process.removeListener('SIGTERM', interrupt)
if (code !== 0 || !passed) console.error('FAIL: Electron probe did not complete successfully.')
resolve(code === 0 && passed ? 0 : 1)
})
})
} finally {
await rm(directory, { recursive: true, force: true, maxRetries: 5, retryDelay: 200 })
}
}
main().catch(() => {
console.error('FAIL: probe setup, build, or cleanup failed; details suppressed.')
process.exitCode = 1
})
@@ -0,0 +1,40 @@
# Telegram 通道技术设计
本文对应 [PRD FR-1~12](./prd.md),记录当前生产实现。验证状态见[进度记录](./progress.md)。实现约束沿用[性能原则](../../architecture/performance-principles.md),下述结构不构成性能测量结论。
## 生产路径与网络
设置页经生产 Preload 和 IPC 调用 `ChannelManager`,由 `ChannelSettingsStore` 保存配置。Manager 按需加载 `TelegramChannelDriver`,向其注入 Electron `net.fetch`;驱动默认网络实现也使用 `net.fetch`。请求走 Electron 网络栈及系统代理,不依赖 Node 的 `HTTPS_PROXY` 配置,不使用 grammY 或 Telegraf。
驱动先调用 `getMe` 与 `getWebhookInfo`,拒绝已有 webhook,不删除外部配置。测试连接只做这两项检查,不启动轮询。启用后使用 `getUpdates`,`timeout=30`、`limit=100`、`allowed_updates=['message']`;普通请求超时 15 秒,轮询请求超时 40 秒。临时错误以 1~30 秒退避重连,429 按 `retry_after` 等待;其他 4xx 停止轮询并报告错误。
## 身份、执行与保存
| 对象 | 实现约定 |
| --- | --- |
| 配置 | 单 Bot;Token 加密保存,快照只返回是否已配置;保留、替换、清除三种动作 |
| 环境覆盖 | `GOODBUDDY_TELEGRAM_BOT_TOKEN`、`GOODBUDDY_TELEGRAM_ENABLED`、`GOODBUDDY_TELEGRAM_ALLOWED_SENDERS`;发送者为逗号分隔的数字 ID,空列表只提供帮助;配置只读,群聊不支持 |
| 入站身份 | `accountId=Bot ID`,`conversationId=Bot ID:chat ID`,`eventId=update_id`,授权使用数字发送者 ID |
| 执行 | 普通私聊文字经 `ChannelService` 队列进入现有远程通道 executor,使用 Telegram 项目后端与工作目录;同会话串行 |
| 内置反馈 | `/start`、`/whoami` 和授权媒体提示由驱动直接回复,不调用模型或写业务上下文;每个用户每类反馈冷却 5 秒 |
| 持久化 | 复用 SQLite 远程会话、去重和发件箱;schema 61 将相关 channel 约束扩展到 Telegram,保留既有数据;设置文件仍为版本 3,缺失 Telegram 配置时使用默认值 |
驱动只接收普通私聊,忽略群、频道、话题、编辑更新及无有效发送者的消息。媒体附带文字也不作为业务输入。驱动与服务均检查授权,白名单更新可原位应用,不取消已开始的任务;排队任务执行前再次检查授权。
入站进入内存队列后确认,驱动随后推进内存 offset。去重键为通道、Bot ID、事件 ID;入站队列和 offset 不持久化,不承诺崩溃后自动续跑或零丢失。重连接收平台仍保留的积压消息,工具请求也可能在重新启用后执行。
## 发送与生命周期
结果先进入现有发件箱,发送失败由服务有限重试,不重新调用模型;失败计数达到 5 或遇到永久错误后停止补发。发送前校验目标会话中的 Bot ID,拒绝把旧 Bot 结果交给新 Bot。附件只附加桌面查看提示。
驱动按最多 4096 个 UTF-16 码元拆分纯文本,并避开代理对中间位置;不设置 `parse_mode` 或引用原消息。分段进度只在驱动内存保存,最多 128 条,停止时清空。单次发送的 429 最多等待重试 3 次,仍失败则交回发件箱处理。跨重启补发与不确定网络结果允许产生重复段落。
停用会中止轮询、发送等待、活动执行与排队工作,等待活动操作结束后报告停止。清除凭据时设置页同时提交 `enabled=false`;底层拒绝启用但无 Token 的配置。Token 替换先验证,失败不停止旧服务。项目与历史不因停用或清除凭据删除。
## 状态与界面
Manager 通过 `{ channel, status }` 增量 IPC 推送状态,忽略旧服务实例的迟到事件。Telegram 连接状态由驱动维护,发送成功不会覆盖轮询错误。状态值为 `disabled`、`starting`、`running`、`stopped`、`error`;重连使用 `starting` 加错误说明,未配置由界面结合凭据状态显示。
Renderer 的 `channel-status-store.ts` 订阅事件,selector 只向状态区域提供对应通道数据。首次快照不会覆盖已收到的较新事件;状态更新不重新加载整份设置,不重置 Token 或白名单草稿。配置页复用共享页签、开关、项目设置字段和通知控件。
源码入口:[`telegram-channel-driver.ts`](../../../src/main/channels/telegram-channel-driver.ts)、[`channel-manager.ts`](../../../src/main/channels/channel-manager.ts)、[`channel-service.ts`](../../../src/main/channels/channel-service.ts)、[`channel-settings-store.ts`](../../../src/main/channels/channel-settings-store.ts)、[`channel-status-store.ts`](../../../src/renderer/src/channel-status-store.ts)、[`ChannelSettingsSection.tsx`](../../../src/renderer/src/ChannelSettingsSection.tsx)。
@@ -0,0 +1,86 @@
/* global process, fetch, AbortSignal, console, setTimeout */
// Credentials stay in the process environment; never print raw API errors.
process.env.NODE_TLS_REJECT_UNAUTHORIZED = '1'
const token = process.env.TELEGRAM_BOT_TOKEN?.trim()
const listen = process.argv.includes('--listen')
const pending = process.argv.includes('--pending')
const startedAt = Math.floor(Date.now() / 1000)
const marker = '/goodbuddy_test'
async function call(method, body = {}, timeout = 15000) {
let response
try {
response = await fetch(`https://api.telegram.org/bot${token}/${method}`, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body),
signal: AbortSignal.timeout(timeout)
})
} catch (error) {
const code = error?.cause?.code ?? error?.name
const safeCode = /^[A-Z_a-z0-9]+$/.test(code ?? '') ? code : 'NETWORK_ERROR'
// Raw network errors can embed the credential-bearing request URL.
// eslint-disable-next-line preserve-caught-error
throw new Error(`${method}: ${safeCode}`)
}
let data
try {
data = await response.json()
} catch {
throw new Error(`${method}: invalid JSON (HTTP ${response.status})`)
}
if (!response.ok || !data.ok) {
const code = Number.isInteger(data.error_code) ? data.error_code : response.status
const wait = Number.isInteger(data.parameters?.retry_after)
? `; retry_after=${data.parameters.retry_after}` : ''
throw new Error(`${method}: API error ${code}${wait}`)
}
return data.result
}
async function main() {
if (!token || !/^\d+:[A-Za-z0-9_-]+$/.test(token)) {
throw new Error('Set TELEGRAM_BOT_TOKEN to a valid BotFather token.')
}
const bot = await call('getMe')
console.log(JSON.stringify({ check: 'getMe', ok: true, username: bot.username }))
const webhook = await call('getWebhookInfo')
console.log(JSON.stringify({
check: 'getWebhookInfo', ok: true,
webhookConfigured: Boolean(webhook.url), pendingUpdates: webhook.pending_update_count
}))
if (!listen && !pending) return
if (webhook.url) throw new Error('Webhook is configured; polling skipped. No configuration changed.')
console.log(pending
? `Checking queued private ${marker} messages from the last hour.`
: `Send ${marker} in a private chat with @${bot.username} within 45 seconds.`)
const deadline = Date.now() + 45000
while (Date.now() < deadline) {
// Do not advance offset or change allowed_updates on a diagnostic run.
const updates = await call('getUpdates', { timeout: pending ? 0 : 10, limit: 100 }, 15000)
const message = updates.map((update) => update.message).reverse().find((item) =>
item?.chat?.type === 'private' && !item.from?.is_bot &&
item.date >= startedAt - (pending ? 3600 : 0) && item.text === marker
)
if (message) {
await call('sendMessage', {
chat_id: message.chat.id,
text: 'GoodBuddy Telegram API test passed. This is a connectivity test, not an AI task.'
})
console.log(JSON.stringify({ check: 'privateMessageRoundTrip', ok: true }))
return
}
if (updates.length >= 100) {
throw new Error('Pending update batch is full; stopped without confirming existing updates.')
}
if (pending) break
if (updates.length) await new Promise((resolve) => setTimeout(resolve, 2000))
}
console.log(JSON.stringify({ check: 'privateMessageRoundTrip', ok: false, reason: 'no_matching_message' }))
process.exitCode = 2
}
main().catch((error) => {
console.error(error.message)
process.exitCode = 1
})
+16 -12
View File
@@ -10,6 +10,8 @@
| 适用产品 | GoodBuddy 桌面端 |
| 首期范围 | 微信 ClawBot、企业微信、钉钉的通道项目;微信文字、图片和文件请求 |
2026-10-05 枚举更新:当前通道项目已增加 Telegram,下文同步更新通用项目、界面和契约枚举;首期范围、背景与第 17 节保留原实施范围。Telegram 的产品行为见[Telegram PRD](../telegram-channel/prd.md),验证状态见[进度记录](../telegram-channel/progress.md)。
## 1. 背景
GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入系统管理的通道项目。该信息架构解决了早期通道仅存在于设置页和后台服务中,远程消息进入任务系统后无法持续、明确识别以下信息的问题:
@@ -30,8 +32,8 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
## 2. 已确认的产品决策
1. 微信 ClawBot、企业微信、钉钉分别对应一个系统管理的通道项目。
2. 三个通道项目在通道设置服务初始化时幂等创建,不等待用户完成连接配置。
1. 微信 ClawBot、企业微信、钉钉、Telegram 分别对应一个系统管理的通道项目。
2. 四个通道项目在通道设置服务初始化时幂等创建,不等待用户完成连接配置。
3. Renderer 不直接创建通道项目。Main 进程保证项目存在,设置卡片首次展示时即可引用对应项目。
4. 通道项目默认根目录为当前操作系统用户目录。
5. 同一通道中的不同私聊用户或群聊分别建立独立远程会话。
@@ -48,7 +50,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
### 3.1 用户目标
- 在项目切换器中一眼识别微信、企业微信和钉钉来源。
- 在项目切换器中一眼识别微信、企业微信、钉钉和 Telegram 来源。
- 在通道项目中区分不同发送者和群聊。
- 在设置卡片中完成连接、启停、处理后端和工作目录配置。
- 通过微信问答或发起工具任务。
@@ -89,7 +91,8 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
远程通道
├─ 微信 ClawBot
├─ 企业微信
└─ 钉钉
├─ 钉钉
└─ Telegram
```
选择器收起时显示当前项目名称;展开后每个选项至少显示项目名称和项目类型。本地项目补充目录摘要,远程通道项目补充平台来源和默认目录;接入可复用的通道状态数据后继续补充文字连接状态。未来接入其他远程项目时可以补充远程主机、工作区或服务来源,而不改变菜单的基本结构。
@@ -137,7 +140,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
应用初始化通道设置服务时,Main 进程执行 `ensureChannelProjects()`:
1. 按稳定通道标识查找 `weixin`、`wecom`、`dingtalk` 对应项目。
1. 按稳定通道标识查找 `weixin`、`wecom`、`dingtalk`、`telegram` 对应项目。
2. 缺失时创建系统管理项目。
3. 已存在时复用,不重复创建。
4. 如果存在同名普通项目,不得按名称占用或修改该项目。
@@ -150,6 +153,7 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
| `weixin` | 微信 ClawBot | 用户目录 | 默认直连文本模型 |
| `wecom` | 企业微信 | 用户目录 | 默认直连文本模型 |
| `dingtalk` | 钉钉 | 用户目录 | 默认直连文本模型 |
| `telegram` | Telegram | 用户目录 | 默认直连文本模型 |
### 6.2 系统管理约束
@@ -176,11 +180,11 @@ GoodBuddy 已将企业微信、钉钉和微信 ClawBot 远程消息通道纳入
设置页标签由“企业通信”改为:
- 标题:消息通道
- 说明:微信 ClawBot、企业微信与钉钉
- 说明:微信 ClawBot、企业微信、钉钉与 Telegram
### 7.2 卡片通用结构
三个通道使用与模型设置一致的分段外观页签,并保留
四个通道使用与模型设置一致的分段外观页签,并保留
`tablist`、`tab`、`tabpanel`、游标焦点和方向键语义。每次只显示一个
通道面板,窄窗口下页签单行横向滚动。
@@ -500,7 +504,7 @@ Main 到 Sidecar:
```ts
type ProjectKind = 'user' | 'channel'
type ProjectChannel = 'weixin' | 'wecom' | 'dingtalk'
type ProjectChannel = 'weixin' | 'wecom' | 'dingtalk' | 'telegram'
```
通道项目包含:
@@ -610,7 +614,7 @@ Renderer 快照只返回是否已配置和脱敏标识。
## 15. 可访问性与响应式
- 通道状态同时使用文字和图标。
- 三个通道使用共享 `PageTabs` 的 `segmented` 外观,保留页签语义和方向键切换。
- 四个通道使用共享 `PageTabs` 的 `segmented` 外观,保留页签语义和方向键切换。
- 后端选择保留语义化控件和键盘操作,不留已删除模式控件的焦点目标。
- 消息处理后端使用持久标签和分组选项,并说明当前选择的实际行为。
- 二维码提供状态文字和备用刷新操作,但不把敏感二维码链接作为可复制文本。
@@ -623,7 +627,7 @@ Renderer 快照只返回是否已配置和脱敏标识。
### 16.1 通道项目
- [ ] 新安装首次启动后存在微信 ClawBot、企业微信和钉钉三个通道项目。
- [ ] 新安装首次启动后存在微信 ClawBot、企业微信、钉钉和 Telegram 四个通道项目。
- [ ] 重启应用不会重复创建通道项目。
- [ ] 同名普通项目不会被占用或修改。
- [ ] 通道项目默认根目录为当前用户目录,不保存默认模式。
@@ -636,8 +640,8 @@ Renderer 快照只返回是否已配置和脱敏标识。
### 16.2 设置卡片
- [ ] 设置标签显示为“消息通道”。
- [ ] 三个通道使用与模型设置一致的分段外观页签,并保留完整页签键盘语义。
- [ ] 三个面板均显示项目、根目录、消息处理后端和连接状态。
- [ ] 四个通道使用与模型设置一致的分段外观页签,并保留完整页签键盘语义。
- [ ] 四个面板均显示项目、根目录、消息处理后端和连接状态。
- [ ] 微信卡片可以完成扫码、过期刷新、验证码和连接状态展示。
- [ ] “断开本机绑定”使用共享红色危险操作样式,并明确说明只清除本机凭据、不删除历史或承诺服务端解绑。
- [ ] 不显示“自动”后端;首次创建和旧版 `auto` 配置均落到默认直连文本模型。