├── .gitattributes ├── dist ├── buildinfo.json └── README.md ├── .github ├── secret_scanning.yml ├── ISSUE_TEMPLATE │ └── bug.md └── workflows │ ├── docker.yml │ ├── build.yml │ └── cloudflare.yml ├── pnpm-workspace.yaml ├── doc ├── demo.png ├── cn │ ├── PLATFORM.md │ ├── VERCEL.md │ ├── LOCAL.md │ ├── CHANGELOG.md │ ├── ACTION.md │ ├── DEPLOY.md │ └── PLUGINS.md └── en │ ├── PLATFORM.md │ ├── VERCEL.md │ ├── LOCAL.md │ ├── ACTION.md │ ├── CHANGELOG.md │ └── DEPLOY.md ├── packages ├── lib │ ├── core │ │ ├── src │ │ │ ├── telegram │ │ │ │ ├── index.ts │ │ │ │ ├── callback_query │ │ │ │ │ ├── types.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── command │ │ │ │ │ ├── types.ts │ │ │ │ │ └── auth.ts │ │ │ │ ├── handler │ │ │ │ │ ├── types.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── group.ts │ │ │ │ │ └── handlers.ts │ │ │ │ ├── auth │ │ │ │ │ └── index.ts │ │ │ │ ├── api │ │ │ │ │ └── index.ts │ │ │ │ └── chat │ │ │ │ │ └── index.ts │ │ │ ├── config │ │ │ │ ├── version.ts │ │ │ │ ├── index.ts │ │ │ │ ├── env.test.ts │ │ │ │ ├── binding.ts │ │ │ │ ├── merger.ts │ │ │ │ └── context.ts │ │ │ ├── agent │ │ │ │ ├── index.ts │ │ │ │ ├── agent.test.ts │ │ │ │ ├── message.ts │ │ │ │ ├── openai_agents.ts │ │ │ │ ├── agent.ts │ │ │ │ ├── gemini.ts │ │ │ │ ├── types.ts │ │ │ │ ├── cohere.ts │ │ │ │ ├── utils.ts │ │ │ │ ├── openai.ts │ │ │ │ ├── chat.ts │ │ │ │ ├── azure.ts │ │ │ │ ├── request.ts │ │ │ │ └── anthropic.ts │ │ │ ├── i18n │ │ │ │ ├── zh-hant.ts │ │ │ │ ├── zh-hans.ts │ │ │ │ ├── en.ts │ │ │ │ ├── pt.ts │ │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── utils │ │ │ │ ├── cache │ │ │ │ │ └── index.ts │ │ │ │ ├── resp │ │ │ │ │ └── index.ts │ │ │ │ ├── image │ │ │ │ │ └── index.ts │ │ │ │ └── router │ │ │ │ │ └── index.ts │ │ │ └── route │ │ │ │ └── index.ts │ │ ├── jest.config.js │ │ ├── vite.config.ts │ │ ├── tsconfig.json │ │ └── package.json │ ├── plugins │ │ ├── src │ │ │ ├── index.ts │ │ │ ├── template.test.ts │ │ │ ├── interpolate.test.ts │ │ │ ├── interpolate.ts │ │ │ └── template.ts │ │ ├── jest.config.js │ │ ├── vite.config.ts │ │ ├── tsconfig.json │ │ └── package.json │ └── next │ │ ├── vite.config.ts │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── src │ │ └── index.ts └── apps │ ├── workers │ ├── src │ │ └── index.ts │ ├── vite.config.ts │ ├── tsconfig.json │ └── package.json │ ├── workers-mk2 │ ├── vite.config.ts │ ├── tsconfig.json │ ├── src │ │ └── index.ts │ └── package.json │ ├── workers-next │ ├── vite.config.ts │ ├── src │ │ └── index.ts │ ├── tsconfig.json │ ├── package.json │ └── README.md │ ├── local │ ├── vite.config.ts │ ├── package-docker.json │ ├── tsconfig.json │ ├── scripts │ │ └── docker-package.ts │ ├── package.json │ └── src │ │ ├── telegram.ts │ │ └── index.ts │ ├── vercel │ ├── vite.config.ts │ ├── tsconfig.json │ ├── package.json │ └── src │ │ └── index.ts │ └── interpolate │ ├── tsconfig.json │ ├── vite.config.ts │ ├── package.json │ ├── index.html │ ├── src │ └── index.ts │ └── assets │ └── main.css ├── .dockerignore ├── docker-compose.yaml ├── vercel.json ├── Dockerfile ├── tsconfig.json ├── plugins ├── dicten.json ├── pollinations.json └── dns.json ├── LICENSE ├── scripts ├── gen-version.ts └── vercel-sync-env.ts ├── eslint.config.js ├── README_CN.md ├── wrangler-example.toml ├── vite.config.shared.ts ├── README.md ├── .gitignore └── package.json /.gitattributes: -------------------------------------------------------------------------------- 1 | dist/** linguist-generated=true -------------------------------------------------------------------------------- /dist/buildinfo.json: -------------------------------------------------------------------------------- 1 | {"sha":"87adca1","timestamp":1761036146} -------------------------------------------------------------------------------- /.github/secret_scanning.yml: -------------------------------------------------------------------------------- 1 | paths-ignore: 2 | - "doc/**" 3 | - "dist/**" -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - packages/apps/* 3 | - packages/lib/* -------------------------------------------------------------------------------- /doc/demo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TBXark/ChatGPT-Telegram-Workers/HEAD/doc/demo.png -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/index.ts: -------------------------------------------------------------------------------- 1 | export * from './api'; 2 | export * from './handler'; 3 | -------------------------------------------------------------------------------- /packages/lib/plugins/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './interpolate'; 2 | export * from './template'; 3 | -------------------------------------------------------------------------------- /packages/apps/workers/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Workers } from '@chatgpt-telegram-workers/core'; 2 | 3 | export default Workers; 4 | -------------------------------------------------------------------------------- /packages/lib/core/src/config/version.ts: -------------------------------------------------------------------------------- 1 | export const BUILD_TIMESTAMP = 1761036146; 2 | export const BUILD_VERSION = '87adca1'; 3 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/index.ts: -------------------------------------------------------------------------------- 1 | export * from './agent'; 2 | export * from './chat'; 3 | export * from './request'; 4 | export * from './types'; 5 | -------------------------------------------------------------------------------- /packages/lib/plugins/jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | testEnvironment: 'node', 3 | transform: { 4 | '^.+.tsx?$': ['ts-jest', {}], 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /packages/apps/workers-mk2/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { createShareConfig } from '../../../vite.config.shared'; 2 | 3 | export default createShareConfig({ 4 | root: __dirname, 5 | }); 6 | -------------------------------------------------------------------------------- /packages/apps/workers-next/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { createShareConfig } from '../../../vite.config.shared'; 2 | 3 | export default createShareConfig({ 4 | root: __dirname, 5 | }); 6 | -------------------------------------------------------------------------------- /packages/lib/plugins/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { createShareConfig } from '../../../vite.config.shared'; 2 | 3 | export default createShareConfig({ 4 | root: __dirname, 5 | types: true, 6 | }); 7 | -------------------------------------------------------------------------------- /packages/lib/core/src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './binding'; 2 | export * from './config'; 3 | export * from './context'; 4 | export * from './env'; 5 | export * from './merger'; 6 | export * from './version'; 7 | -------------------------------------------------------------------------------- /packages/lib/next/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { createShareConfig } from '../../../vite.config.shared'; 2 | 3 | export default createShareConfig({ 4 | root: __dirname, 5 | types: true, 6 | nodeExternals: true, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/apps/local/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { createShareConfig } from '../../../vite.config.shared'; 2 | 3 | export default createShareConfig({ 4 | root: __dirname, 5 | nodeExternals: true, 6 | excludeMonoRepoPackages: true, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/apps/vercel/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { createShareConfig } from '../../../vite.config.shared'; 2 | 3 | export default createShareConfig({ 4 | root: __dirname, 5 | nodeExternals: true, 6 | excludeMonoRepoPackages: true, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/apps/workers/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { createShareConfig } from '../../../vite.config.shared'; 2 | 3 | export default createShareConfig({ 4 | root: __dirname, 5 | nodeExternals: true, 6 | excludeMonoRepoPackages: true, 7 | }); 8 | -------------------------------------------------------------------------------- /packages/apps/workers-next/src/index.ts: -------------------------------------------------------------------------------- 1 | import { CHAT_AGENTS, Workers } from '@chatgpt-telegram-workers/core'; 2 | import { injectNextChatAgent } from '@chatgpt-telegram-workers/next'; 3 | 4 | injectNextChatAgent(CHAT_AGENTS); 5 | export default Workers; 6 | -------------------------------------------------------------------------------- /packages/lib/core/jest.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | testEnvironment: 'node', 3 | transform: { 4 | '^.+.tsx?$': ['ts-jest', {}], 5 | }, 6 | moduleNameMapper: { 7 | '^#/(.*)$': '/src/$1', 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /packages/lib/core/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { createShareConfig } from '../../../vite.config.shared'; 2 | 3 | export default createShareConfig({ 4 | root: __dirname, 5 | types: true, 6 | formats: ['es', 'cjs'], 7 | nodeExternals: false, 8 | }); 9 | -------------------------------------------------------------------------------- /packages/lib/plugins/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "types": ["node", "jest"], 6 | "outDir": "./dist" 7 | }, 8 | "include": ["src/**/*"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/apps/workers/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "references": [ 8 | { "path": "../../lib/core" } 9 | ], 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/apps/interpolate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "references": [ 8 | { "path": "../../lib/plugins" } 9 | ], 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /packages/apps/workers-mk2/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "outDir": "./dist" 6 | }, 7 | "references": [ 8 | { "path": "../../lib/core" } 9 | ], 10 | "include": ["src/**/*"] 11 | } 12 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | .env 2 | .DS_Store 3 | .git 4 | .github 5 | .vercel 6 | .wrangler 7 | dist 8 | doc 9 | node_modules 10 | plugins 11 | scripts 12 | wrangler.toml 13 | wrangler-example.toml 14 | config.json 15 | LICENSE 16 | README.md 17 | README_CN.md 18 | packages/**/dist 19 | packages/**/node_modules 20 | packages/**/tsconfig.tsbuildinfo -------------------------------------------------------------------------------- /packages/apps/local/package-docker.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatgpt-telegram-workers/local", 3 | "type": "module", 4 | "version": "1.10.7", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "dependencies": { 8 | "cloudflare-worker-adapter": "^1.3.9", 9 | "telegramify-markdown": "^1.3.0" 10 | } 11 | } -------------------------------------------------------------------------------- /packages/apps/vercel/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "types": ["node", "react"], 6 | "outDir": "./dist" 7 | }, 8 | "references": [ 9 | { "path": "../../lib/core" } 10 | ], 11 | "include": ["src/**/*"] 12 | } 13 | -------------------------------------------------------------------------------- /packages/apps/local/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "types": ["node", "react"], 6 | "outDir": "./dist" 7 | }, 8 | "references": [ 9 | { "path": "../../lib/core" }, 10 | { "path": "../../lib/next" } 11 | ], 12 | "include": ["src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/lib/next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "rootDir": "./src", 6 | "types": ["node", "react"], 7 | "outDir": "./dist" 8 | }, 9 | "references": [ 10 | { "path": "../core" } 11 | ], 12 | "include": ["src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /dist/README.md: -------------------------------------------------------------------------------- 1 | # Dist file 2 | 3 | This directory contains the compiled files for deployment. The files in this directory are generated by the build process and are ready for deployment. 4 | 5 | ## Files 6 | 7 | - [index.js](index.js): The Base version bot. 8 | - [index-mk2.js](index-mk2.js): Telegram MarkdownV2 feature support bot. 9 | - [index-next.js](index-next.js): Integration with Vercel/AI SDK. -------------------------------------------------------------------------------- /packages/apps/workers-next/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "rootDir": "./src", 5 | "types": ["node", "react"], 6 | "outDir": "./dist" 7 | }, 8 | "references": [ 9 | { "path": "../../lib/core" }, 10 | { "path": "../../lib/next" } 11 | ], 12 | "include": ["src/**/*"] 13 | } 14 | -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/callback_query/types.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerContext } from '#/config'; 2 | import type * as Telegram from 'telegram-bot-api-types'; 3 | 4 | export interface CallbackQueryHandler { 5 | prefix: string; 6 | handle: (query: Telegram.CallbackQuery, data: string, context: WorkerContext) => Promise; 7 | needAuth?: (chatType: string) => string[] | null; 8 | } 9 | -------------------------------------------------------------------------------- /packages/apps/workers-mk2/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ENV, Workers } from '@chatgpt-telegram-workers/core'; 2 | import convert from 'telegramify-markdown'; 3 | 4 | ENV.DEFAULT_PARSE_MODE = 'MarkdownV2'; 5 | ENV.CUSTOM_MESSAGE_RENDER = (parse_mode, message) => { 6 | if (parse_mode === 'MarkdownV2') { 7 | return convert(message, 'remove'); 8 | } 9 | return message; 10 | }; 11 | export default Workers; 12 | -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/command/types.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerContext } from '#/config'; 2 | import type * as Telegram from 'telegram-bot-api-types'; 3 | 4 | export interface CommandHandler { 5 | command: string; 6 | scopes?: string[]; 7 | handle: (message: Telegram.Message, subcommand: string, context: WorkerContext) => Promise; 8 | needAuth?: (chatType: string) => string[] | null; 9 | } 10 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | name: chatgpt-telegram-workers 2 | services: 3 | chatgpt-telegram-workers: 4 | build: . 5 | ports: 6 | - "8787:8787" 7 | volumes: 8 | - ./config.json:/app/config.json:ro # change `./config.json` to your local path 9 | - ./wrangler.toml:/app/wrangler.toml:ro # change `./wrangler.toml` to your local path 10 | network_mode: "host" # If you access the proxy port based on the host -------------------------------------------------------------------------------- /packages/lib/core/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../../tsconfig.json", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "rootDir": "./src", 6 | "paths": { 7 | "#/*": ["src/*"] 8 | }, 9 | "types": ["node", "jest"], 10 | "outDir": "./dist" 11 | }, 12 | "references": [ 13 | { "path": "../plugins" } 14 | ], 15 | "include": ["src/**/*"] 16 | } 17 | -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "packages/apps/vercel/dist/index.js", 6 | "use": "@vercel/node", 7 | "config": { 8 | "includeFiles": ["packages/apps/vercel/dist/**"] 9 | } 10 | } 11 | ], 12 | "routes": [ 13 | { 14 | "src": "/(.*)", 15 | "dest": "packages/apps/vercel/dist/index.js" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /doc/cn/PLATFORM.md: -------------------------------------------------------------------------------- 1 | # 支持平台 2 | 3 | ### 1. [Cloudflare Workers](https://workers.cloudflare.com/) 4 | 5 | 最简单的方法,本项目默认支持的部署方式,详情看[部署流程](DEPLOY.md)。免费,无需域名,无需服务器,无需配置本地开发环境。KV存储,无需数据库,但是有一定的存储限制, 6 | 7 | 8 | ### 2. [Vercel](https://vercel.com/) 9 | 10 | 详情看[Vercel](VERCEL.md)。免费,无需域名,无需服务器。需要配置本地开发环境部署,不能通过复制粘贴部署。无存储服务,需要自己配置数据库。可以使用[UpStash Redis](https://upstash.com)的免费redis。可以连接github自动部署,但是需要了解vercel的配置。 11 | 12 | 13 | ### 3. Local 14 | 15 | 详情看[Local](LOCAL.md)。本地的部署方式,需要配置本地开发环境,需要有一定的开发能力。支持docker部署。 16 | -------------------------------------------------------------------------------- /packages/apps/interpolate/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | 3 | export default defineConfig({ 4 | root: '.', 5 | build: { 6 | outDir: 'dist', 7 | emptyOutDir: true, 8 | minify: false, 9 | rollupOptions: { 10 | input: 'index.html', 11 | output: { 12 | entryFileNames: 'index.js', 13 | chunkFileNames: '[name].js', 14 | assetFileNames: '[name].[ext]', 15 | }, 16 | }, 17 | }, 18 | }); 19 | -------------------------------------------------------------------------------- /packages/lib/plugins/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatgpt-telegram-workers/plugins", 3 | "type": "module", 4 | "version": "1.10.3", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "pnpm vite build", 9 | "clean": "rm -rf dist && rm -rf node_modules && rm -rf tsconfig.tsbuildinfo", 10 | "test": "jest" 11 | }, 12 | "devDependencies": { 13 | "@types/jest": "^30.0.0", 14 | "jest": "^30.2.0", 15 | "ts-jest": "^29.4.5" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: BUG反馈 3 | about: 详细填写这个表格让我们更好的修复BUG 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **版本号** 11 | > 你可以在代码编辑器中前几行找到, 它们分别是`BUILD_TIMESTAMP`和`BUILD_VERSION`, 这俩个数据对于我们定位问题非常重要] 12 | - ts: `BUILD_TIMESTAMP ` 13 | - sha: `BUILD_VERSION` 14 | - branch: `当前代码所在的分支` 15 | 16 | **描述问题** 17 | 简要而清晰地解释问题。 18 | 19 | **复现问题** 20 | 重现问题的步骤: 21 | 1. 进入“...” 22 | 2. 点击“...” 23 | 3. 发送到“...” 24 | 25 | **预期行为** 26 | 简要而清晰地说明预期的行为。 27 | 28 | **截图** 29 | 如适用,包括截图以帮助说明问题。 30 | 31 | **其他信息** 32 | 提供与问题相关的任何其他信息。 33 | -------------------------------------------------------------------------------- /packages/apps/interpolate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatgpt-telegram-workers/interpolate", 3 | "type": "module", 4 | "version": "1.10.3", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "pnpm vite build", 9 | "clean": "rm -rf dist && rm -rf node_modules && rm -rf tsconfig.tsbuildinfo", 10 | "dev": "pnpm vite" 11 | }, 12 | "dependencies": { 13 | "@chatgpt-telegram-workers/plugins": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/apps/workers-mk2/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatgpt-telegram-workers/workers-mk2", 3 | "type": "module", 4 | "version": "1.10.3", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "pnpm vite build", 9 | "clean": "rm -rf dist && rm -rf node_modules", 10 | "deploy": "wrangler deploy --config ${TOML_PATH}" 11 | }, 12 | "dependencies": { 13 | "@chatgpt-telegram-workers/core": "workspace:*", 14 | "telegramify-markdown": "^1.3.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/apps/workers/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatgpt-telegram-workers/workers", 3 | "type": "module", 4 | "version": "1.10.3", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "pnpm vite build", 9 | "clean": "rm -rf dist && rm -rf node_modules && rm -rf tsconfig.tsbuildinfo", 10 | "deploy": "wrangler deploy --config ${TOML_PATH}" 11 | }, 12 | "dependencies": { 13 | "@chatgpt-telegram-workers/core": "workspace:*" 14 | }, 15 | "devDependencies": { 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/apps/local/scripts/docker-package.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs/promises'; 2 | 3 | async function main() { 4 | const packageJson = JSON.parse(await fs.readFile('package.json', 'utf-8')); 5 | delete packageJson.scripts; 6 | delete packageJson.devDependencies; 7 | for (const key in packageJson.dependencies) { 8 | if (key.startsWith('@chatgpt-telegram-workers/')) { 9 | delete packageJson.dependencies[key]; 10 | } 11 | } 12 | await fs.writeFile('package-docker.json', JSON.stringify(packageJson, null, 2)); 13 | } 14 | 15 | main().catch(console.error); 16 | -------------------------------------------------------------------------------- /packages/lib/core/src/config/env.test.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import path from 'node:path'; 3 | import { parse } from 'toml'; 4 | import { ENV } from './env'; 5 | 6 | describe('env', () => { 7 | it('should load env', () => { 8 | const toml = path.join(__dirname, '../../../../../wrangler-example.toml'); 9 | const config = parse(readFileSync(toml, 'utf8')); 10 | ENV.merge({ 11 | ...config.vars, 12 | DATABASE: {}, 13 | }); 14 | expect(ENV).toBeDefined(); 15 | expect(ENV.USER_CONFIG.AI_PROVIDER).toBe('auto'); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-slim AS build 2 | 3 | ENV PNPM_HOME="/pnpm" 4 | ENV PATH="$PNPM_HOME:$PATH" 5 | 6 | RUN corepack disable && npm install -g pnpm@latest 7 | 8 | COPY . /app 9 | WORKDIR /app 10 | 11 | RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile 12 | RUN pnpm run build:local 13 | 14 | FROM node:20-slim AS prod 15 | 16 | WORKDIR /app 17 | 18 | COPY --from=build /app/packages/apps/local/dist/index.js /app/dist/index.js 19 | COPY --from=build /app/packages/apps/local/package-docker.json /app/package.json 20 | 21 | RUN npm install 22 | EXPOSE 8787 23 | 24 | CMD ["node", "/app/dist/index.js"] -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/handler/types.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerContext } from '#/config'; 2 | import type * as Telegram from 'telegram-bot-api-types'; 3 | 4 | // 中间件定义 function (message: xxx, context: Context): Promise 5 | // 1. 当函数抛出异常时,结束消息处理,返回异常信息 6 | // 2. 当函数返回 Response 对象时,结束消息处理,返回 Response 对象 7 | // 3. 当函数返回 null 时,继续下一个中间件处理 8 | 9 | export interface UpdateHandler { 10 | handle: (update: Telegram.Update, context: WorkerContext) => Promise; 11 | } 12 | 13 | export interface MessageHandler { 14 | handle: (message: Telegram.Message, context: WorkerContext) => Promise; 15 | } 16 | -------------------------------------------------------------------------------- /packages/apps/workers-next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatgpt-telegram-workers/workers-next", 3 | "type": "module", 4 | "version": "1.10.3", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "pnpm vite build", 9 | "clean": "rm -rf dist && rm -rf node_modules", 10 | "deploy": "wrangler deploy --config ${TOML_PATH}" 11 | }, 12 | "dependencies": { 13 | "@chatgpt-telegram-workers/core": "workspace:*", 14 | "@chatgpt-telegram-workers/next": "workspace:*" 15 | }, 16 | "devDependencies": { 17 | "@types/react": "^19.2.2" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/lib/core/src/i18n/zh-hant.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default {"env":{"system_init_message":"你是一個得力的助手"},"command":{"help":{"summary":"當前支持的命令如下:\n","help":"獲取命令幫助","new":"開始一個新對話","start":"獲取您的ID並開始一個新對話","img":"生成圖片,完整命令格式為`/img 圖片描述`,例如`/img 海灘月光`","version":"獲取當前版本號確認是否需要更新","setenv":"設置用戶配置,完整命令格式為/setenv KEY=VALUE","setenvs":"批量設置用户配置, 命令完整格式為 /setenvs {\"KEY1\": \"VALUE1\", \"KEY2\": \"VALUE2\"}","delenv":"刪除用戶配置,完整命令格式為/delenv KEY","clearenv":"清除所有用戶配置","system":"查看一些系統信息","redo":"重做上一次的對話 /redo 加修改過的內容 或者 直接 /redo","echo":"回显消息","models":"切換對話模式"},"new":{"new_chat_start":"開始一個新對話"}},"callback_query":{"open_model_list":"打開模型清單","select_provider":"選擇一個模型供應商:","select_model":"選擇一個模型:","change_model":"對話模型已經修改至"}} -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "target": "ESNext", 5 | "jsx": "react", 6 | "lib": [ 7 | "ESNext", 8 | "DOM", 9 | "DOM.Iterable" 10 | ], 11 | "rootDir": ".", 12 | "module": "ESNext", 13 | "moduleResolution": "bundler", 14 | "types": ["node"], 15 | "allowJs": true, 16 | "strict": true, 17 | "declaration": true, 18 | "declarationMap": true, 19 | "outDir": "dist", 20 | "removeComments": true, 21 | "sourceMap": true, 22 | "esModuleInterop": true 23 | }, 24 | "exclude": ["**/node_modules", "**/dist"] 25 | } 26 | -------------------------------------------------------------------------------- /packages/lib/core/src/i18n/zh-hans.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default {"env":{"system_init_message":"你是一个得力的助手"},"command":{"help":{"summary":"当前支持以下命令:\n","help":"获取命令帮助","new":"发起新的对话","start":"获取你的ID, 并发起新的对话","img":"生成一张图片, 命令完整格式为 `/img 图片描述`, 例如`/img 月光下的沙滩`","version":"获取当前版本号, 判断是否需要更新","setenv":"设置用户配置,命令完整格式为 /setenv KEY=VALUE","setenvs":"批量设置用户配置, 命令完整格式为 /setenvs {\"KEY1\": \"VALUE1\", \"KEY2\": \"VALUE2\"}","delenv":"删除用户配置,命令完整格式为 /delenv KEY","clearenv":"清除所有用户配置","system":"查看当前一些系统信息","redo":"重做上一次的对话, /redo 加修改过的内容 或者 直接 /redo","echo":"回显消息","models":"切换对话模型"},"new":{"new_chat_start":"新的对话已经开始"}},"callback_query":{"open_model_list":"打开模型列表","select_provider":"选择一个模型提供商:","select_model":"选择一个模型:","change_model":"对话模型已修改至"}} -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Build and Publish Docker Image 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build-and-push: 10 | permissions: 11 | packages: write 12 | contents: read 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Build and push Docker image 16 | uses: TBXark/docker-action@master 17 | with: 18 | docker_registry: ghcr.io 19 | docker_username: ${{ github.actor }} 20 | docker_password: ${{ secrets.GITHUB_TOKEN }} 21 | backup_registry: ${{ secrets.BACKUP_REGISTRY }} 22 | backup_username: ${{ secrets.BACKUP_USERNAME }} 23 | backup_password: ${{ secrets.BACKUP_PASSWORD }} 24 | -------------------------------------------------------------------------------- /packages/lib/core/src/index.ts: -------------------------------------------------------------------------------- 1 | import { ENV } from './config'; 2 | import { createRouter } from './route'; 3 | 4 | export * from './agent'; 5 | export * from './config'; 6 | export * from './i18n'; 7 | export * from './route'; 8 | export * from './telegram'; 9 | 10 | export const Workers = { 11 | async fetch(request: Request, env: any): Promise { 12 | try { 13 | ENV.merge(env); 14 | return createRouter().fetch(request); 15 | } catch (e) { 16 | console.error(e); 17 | return new Response(JSON.stringify({ 18 | message: (e as Error).message, 19 | stack: (e as Error).stack, 20 | }), { status: 500 }); 21 | } 22 | }, 23 | }; 24 | -------------------------------------------------------------------------------- /packages/lib/core/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatgpt-telegram-workers/core", 3 | "type": "module", 4 | "version": "1.10.3", 5 | "imports": { 6 | "#/*": "./src/*" 7 | }, 8 | "main": "./dist/index.js", 9 | "types": "./dist/index.d.ts", 10 | "scripts": { 11 | "build": "pnpm vite build", 12 | "clean": "rm -rf dist && rm -rf node_modules && rm -rf tsconfig.tsbuildinfo", 13 | "test": "jest" 14 | }, 15 | "dependencies": { 16 | "@chatgpt-telegram-workers/plugins": "workspace:*" 17 | }, 18 | "devDependencies": { 19 | "@cloudflare/workers-types": "^4.20251014.0", 20 | "@types/jest": "^30.0.0", 21 | "jest": "^30.2.0", 22 | "toml": "^3.0.0", 23 | "ts-jest": "^29.4.5" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { ENV } from '#/config'; 2 | 3 | export const TELEGRAM_AUTH_CHECKER = { 4 | default(chatType: string): string[] | null { 5 | if (isGroupChat(chatType)) { 6 | return ['administrator', 'creator']; 7 | } 8 | return null; 9 | }, 10 | shareModeGroup(chatType: string): string[] | null { 11 | if (isGroupChat(chatType)) { 12 | // 每个人在群里有上下文的时候,不限制 13 | if (!ENV.GROUP_CHAT_BOT_SHARE_MODE) { 14 | return null; 15 | } 16 | return ['administrator', 'creator']; 17 | } 18 | return null; 19 | }, 20 | }; 21 | 22 | export function isGroupChat(type: string): boolean { 23 | return type === 'group' || type === 'supergroup'; 24 | } 25 | -------------------------------------------------------------------------------- /packages/apps/local/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatgpt-telegram-workers/local", 3 | "type": "module", 4 | "version": "1.10.7", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "pnpm vite build && pnpm tsx scripts/docker-package.ts", 9 | "start": "pnpm tsx src/index.ts", 10 | "start:dist": "node dist/index.js", 11 | "clean": "rm -rf dist && rm -rf node_modules && rm -rf tsconfig.tsbuildinfo" 12 | }, 13 | "dependencies": { 14 | "@chatgpt-telegram-workers/core": "workspace:*", 15 | "@chatgpt-telegram-workers/next": "workspace:*", 16 | "cloudflare-worker-adapter": "^1.3.9", 17 | "telegramify-markdown": "^1.3.0" 18 | }, 19 | "devDependencies": { 20 | "@types/react": "^19.2.2" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/lib/core/src/config/binding.ts: -------------------------------------------------------------------------------- 1 | export interface KVNamespaceBinding { 2 | get: (key: string) => Promise; 3 | put: (key: string, value: string, info?: { expirationTtl?: number; expiration?: number }) => Promise; 4 | delete: (key: string) => Promise; 5 | } 6 | 7 | export interface APIGuardBinding { 8 | fetch: (request: Request) => Promise; 9 | } 10 | 11 | export type AiTextGenerationOutput = ReadableStream | { response?: string }; 12 | export type AiTextToImageOutput = ReadableStream | { image?: string }; 13 | 14 | export abstract class WorkerAIBinding { 15 | abstract run(model: string, body: { messages: any[]; stream: boolean }): Promise; 16 | abstract run(model: string, body: { prompt: string }): Promise; 17 | } 18 | -------------------------------------------------------------------------------- /packages/apps/vercel/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatgpt-telegram-workers/vercel", 3 | "type": "module", 4 | "version": "1.10.3", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "pnpm vite build", 9 | "clean": "rm -rf dist && rm -rf node_modules && rm -rf tsconfig.tsbuildinfo" 10 | }, 11 | "dependencies": { 12 | "@chatgpt-telegram-workers/core": "workspace:*", 13 | "@chatgpt-telegram-workers/next": "workspace:*", 14 | "cloudflare-worker-adapter": "^1.3.9", 15 | "telegramify-markdown": "^1.3.0" 16 | }, 17 | "devDependencies": { 18 | "@types/react": "^19.2.2", 19 | "@vercel/node": "^5.4.1", 20 | "openai": "^6.6.0", 21 | "react-dom": "^19.2.0", 22 | "typescript": "^5.9.3" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/apps/workers-next/README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT-Telegram-Workers-Next 2 | 3 | 4 | ## 中文 5 | 6 | ChatGPT-Telegram-Workers-Next 是一个实验性质的版本, 使用 `https://github.com/vercel/ai` 驱动。比起原始版本支持更多特性。 7 | 8 | 此版本暂不提供`dist`文件,如果你想部署此版本,需要将 `wrangler.toml` 中的 `main` 修改为 `./src/entry/next/gen-vercel-env.ts` 并添加nodejs支持`compatibility_flags = [ "nodejs_compat_v2" ]` 9 | 10 | 然后使用`wrangler deploy`进行部署。 11 | 12 | 13 | ## English 14 | 15 | ChatGPT-Telegram-Workers-Next is an experimental version, driven by `https://github.com/vercel/ai`. It supports more features than the original version. 16 | 17 | This version does not provide the `dist` files. If you want to deploy this version, you need to modify `main` in `wrangler.toml` to `./src/entry/next/gen-vercel-env.ts` and add nodejs support `compatibility_flags = [ "nodejs_compat_v2" ]`. 18 | 19 | Then use `wrangler deploy` to deploy. -------------------------------------------------------------------------------- /plugins/dicten.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://api.dictionaryapi.dev/api/v2/entries/en/{{DATA}}", 3 | "method": "GET", 4 | "input": { 5 | "required": true 6 | }, 7 | "response": { 8 | "content": { 9 | "input_type": "json", 10 | "output_type": "html", 11 | "output": "{{#each word in .}}\n{{word.word}}{{#if word.phonetic}}{{word.phonetic}}{{/if}}\n{{#each:word meanings in word.meanings}}\n + {{meanings.partOfSpeech}}\n {{#each:meanings definitions in meanings.definitions}}\n {{definitions.definition}}\n {{/each:meanings}}\n{{/each:word}}\n{{/each}}\n" 12 | }, 13 | "error": { 14 | "input_type": "json", 15 | "output_type": "text", 16 | "output": "Error: {{message}}" 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/lib/plugins/src/template.test.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as path from 'node:path'; 3 | import { executeRequest } from './template'; 4 | 5 | describe('template', () => { 6 | it.skip('dns', async () => { 7 | const plugin = path.join(__dirname, '../../../../plugins/dns.json'); 8 | const template = JSON.parse(fs.readFileSync(plugin, 'utf8')); 9 | const result = await executeRequest(template, { DATA: ['B', 'google.com'] }); 10 | expect(result.content).toContain('google.com'); 11 | }); 12 | it('dicten', async () => { 13 | const plugin = path.join(__dirname, '../../../../plugins/dicten.json'); 14 | const template = JSON.parse(fs.readFileSync(plugin, 'utf8')); 15 | const result = await executeRequest(template, { DATA: 'example' }); 16 | expect(result.content).toContain('example'); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /packages/lib/next/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatgpt-telegram-workers/next", 3 | "type": "module", 4 | "version": "1.10.3", 5 | "main": "./dist/index.js", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "pnpm vite build", 9 | "clean": "rm -rf dist && rm -rf node_modules && rm -rf tsconfig.tsbuildinfo" 10 | }, 11 | "dependencies": { 12 | "@ai-sdk/anthropic": "^2.0.35", 13 | "@ai-sdk/azure": "^2.0.54", 14 | "@ai-sdk/cohere": "^2.0.14", 15 | "@ai-sdk/google": "^2.0.23", 16 | "@ai-sdk/mistral": "^2.0.19", 17 | "@ai-sdk/openai": "^2.0.53", 18 | "@ai-sdk/provider": "^2.0.0", 19 | "@chatgpt-telegram-workers/core": "workspace:*", 20 | "ai": "^5.0.76" 21 | }, 22 | "devDependencies": { 23 | "@types/react": "^19.2.2", 24 | "openai": "^6.6.0", 25 | "react-dom": "^19.2.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /plugins/pollinations.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://image.pollinations.ai/prompt/{{DATA}}", 3 | "method": "GET", 4 | "headers": { 5 | "accept": "application/json" 6 | }, 7 | "input": { 8 | "type": "text", 9 | "required": true 10 | }, 11 | "query": { 12 | "width": "{{ENV.POLLINATIONS_IMAGE_WIDTH}}", 13 | "height": "{{ENV.POLLINATIONS_IMAGE_HEIGHT}}", 14 | "model": "{{ENV.POLLINATIONS_MODEL}}", 15 | "nologo": "{{ENV.POLLINATIONS_NOLOGO}}", 16 | "private": "{{ENV.POLLINATIONS_PRIVATE}}", 17 | "enhance": "{{ENV.POLLINATIONS_ENHANCE}}" 18 | }, 19 | "response": { 20 | "content": { 21 | "input_type": "blob", 22 | "output_type": "image" 23 | }, 24 | "error": { 25 | "input_type": "text", 26 | "output_type": "text", 27 | "output": "Error: {{.}}" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /doc/cn/VERCEL.md: -------------------------------------------------------------------------------- 1 | # 使用Vercel部署 (实验性) 2 | 3 | `/packages/app/vercel`中提供了示例代码,可以完成Vercel部署,和基础的功能测试。但是无法保证所有功能都能正常工作。 4 | 5 | ### 自动部署 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FTBXark%2FChatGPT-Telegram-Workers&env=UPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKEN,TELEGRAM_AVAILABLE_TOKENS&project-name=chatgpt-telegram-workers&repository-name=ChatGPT-Telegram-Workers&demo-title=ChatGPT-Telegram-Workers&demo-description=Deploy%20your%20own%20Telegram%20ChatGPT%20bot%20on%20Cloudflare%20Workers%20with%20ease.&demo-url=https%3A%2F%2Fchatgpt-telegram-workers.vercel.app) 8 | 9 | ### 手动部署 10 | 11 | ```shell 12 | pnpm install 13 | pnpm deploy:vercel 14 | ``` 15 | 16 | 1. pnpm deploy:vercel 过程中可能需要登陆Vercel账号 17 | 2. 首次部署由于缺少环境变量,页面会报错,需要手动前往Vercel控制台添加环境变量,然后重新部署生效 18 | 3. 你可以复用cloudflare workers的`wrangler.toml`配置文件,只需要执行`pnpm run vercel:syncenv`即可同步环境变量到Vercel, vercel修改环境变量后需要重新部署才能生效 -------------------------------------------------------------------------------- /packages/lib/core/src/agent/agent.test.ts: -------------------------------------------------------------------------------- 1 | import type { LLMChatParams } from './types'; 2 | import { ENV } from '#/config'; 3 | import { loadChatLLM } from './agent'; 4 | import '#/config/env.test'; 5 | 6 | describe('agent', () => { 7 | it.skip('should load agent', async () => { 8 | const agent = loadChatLLM({ 9 | ...ENV.USER_CONFIG, 10 | AI_PROVIDER: 'cohere', 11 | }); 12 | const params: LLMChatParams = { 13 | prompt: 'You are a useful assistant.', 14 | messages: [ 15 | { 16 | role: 'user', 17 | content: 'What is your name?', 18 | }, 19 | ], 20 | }; 21 | expect(agent?.name).toBe('cohere'); 22 | const res = await agent?.request(params, ENV.USER_CONFIG, async (text) => { 23 | console.log(text); 24 | }); 25 | expect(res).toBeDefined(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /plugins/dns.json: -------------------------------------------------------------------------------- 1 | { 2 | "url": "https://cloudflare-dns.com/dns-query", 3 | "method": "GET", 4 | "headers": { 5 | "accept": "application/dns-json" 6 | }, 7 | "input": { 8 | "type": "space-separated", 9 | "required": true 10 | }, 11 | "query": { 12 | "type": "{{DATA[0]}}", 13 | "name": "{{DATA[1]}}" 14 | }, 15 | "response": { 16 | "content": { 17 | "input_type": "json", 18 | "output_type": "html", 19 | "output": "\nDNS query: {{Question[0].name}}\nStatus: {{#if TC}}TC,{{/if}}{{#if RD}}RD,{{/if}}{{#if RA}}RA,{{/if}}{{#if AD}}AD,{{/if}}{{#if CD}}CD,{{/if}}{{Status}}\n\nAnswer{{#each answer in Answer}}\n{{answer.name}}, {{answer.type}}, (TTL: {{answer.TTL}}),{{answer.data}}{{/each}}\n" 20 | }, 21 | "error": { 22 | "input_type": "json", 23 | "output_type": "text", 24 | "output": "Error: {{error}}" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 TBXark 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /packages/lib/core/src/agent/message.ts: -------------------------------------------------------------------------------- 1 | export type DataContent = string | Uint8Array | ArrayBuffer | Buffer; 2 | 3 | export interface TextPart { 4 | type: 'text'; 5 | text: string; 6 | } 7 | 8 | export interface ImagePart { 9 | type: 'image'; 10 | image: DataContent | URL; 11 | mimeType?: string; 12 | } 13 | 14 | export interface FilePart { 15 | type: 'file'; 16 | data: DataContent | URL; 17 | } 18 | 19 | export interface AnyAdapterPart { 20 | type: string; 21 | data: T; 22 | } 23 | 24 | export type AssistantContent = string | Array>; 25 | export type UserContent = string | Array; 26 | 27 | export interface CoreSystemMessage { 28 | role: 'system'; 29 | content: string; 30 | } 31 | 32 | export interface CoreAssistantMessage { 33 | role: 'assistant'; 34 | content: AssistantContent; 35 | } 36 | 37 | export interface CoreUserMessage { 38 | role: 'user'; 39 | content: UserContent; 40 | } 41 | 42 | export interface AdapterMessage { 43 | role: R; 44 | content: T; 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build and Test 2 | 3 | on: 4 | push: 5 | branches: ["*"] 6 | pull_request: 7 | branches: ["*"] 8 | workflow_dispatch: 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | build: 16 | name: Build and Test 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v3 22 | 23 | - name: Setup Node.js 24 | uses: actions/setup-node@v3 25 | with: 26 | node-version: 20 27 | 28 | - name: Setup pnpm cache 29 | uses: actions/cache@v3 30 | with: 31 | path: | 32 | ~/.pnpm-store 33 | node_modules 34 | key: ${{ runner.os }}-pnpm-${{ hashFiles('**/pnpm-lock.yaml') }} 35 | restore-keys: | 36 | ${{ runner.os }}-pnpm- 37 | 38 | - name: Install pnpm 39 | run: npm install -g pnpm 40 | 41 | - name: Install dependencies 42 | run: pnpm install --frozen-lockfile 43 | 44 | - name: Run test 45 | run: pnpm run test 46 | 47 | - name: Build project 48 | run: pnpm run build -------------------------------------------------------------------------------- /packages/apps/interpolate/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ChatGPT-Telegram-Workers 7 | 8 | 9 | 10 | 11 |
12 |
13 | 16 | 17 |
18 |
19 | 20 | 21 |
22 |
23 |

preview

24 |

25 |     
26 |
27 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /packages/lib/core/src/i18n/en.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default {"env":{"system_init_message":"You are a helpful assistant"},"command":{"help":{"summary":"The following commands are currently supported:\n","help":"Get command help","new":"Start a new conversation","start":"Get your ID and start a new conversation","img":"Generate an image, the complete command format is `/img image description`, for example `/img beach at moonlight`","version":"Get the current version number to determine whether to update","setenv":"Set user configuration, the complete command format is /setenv KEY=VALUE","setenvs":"Batch set user configurations, the full format of the command is /setenvs {\"KEY1\": \"VALUE1\", \"KEY2\": \"VALUE2\"}","delenv":"Delete user configuration, the complete command format is /delenv KEY","clearenv":"Clear all user configuration","system":"View some system information","redo":"Redo the last conversation, /redo with modified content or directly /redo","echo":"Echo the message","models":"switch chat model"},"new":{"new_chat_start":"A new conversation has started"}},"callback_query":{"open_model_list":"Open models list","select_provider":"Select a provider:","select_model":"Choose model:","change_model":"Change model to "}} -------------------------------------------------------------------------------- /scripts/gen-version.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process'; 2 | import * as fs from 'node:fs/promises'; 3 | import path from 'node:path'; 4 | 5 | const TIMESTAMP = Math.floor(Date.now() / 1000); 6 | const COMMIT_HASH = ((): string => { 7 | try { 8 | return execSync('git rev-parse --short HEAD').toString().trim(); 9 | } catch (e) { 10 | console.warn(e); 11 | } 12 | return 'unknown'; 13 | })(); 14 | 15 | async function createVersionTs(outDir: string) { 16 | await fs.writeFile( 17 | path.resolve(outDir, 'packages/lib/core/src/config/version.ts'), 18 | `export const BUILD_TIMESTAMP = ${TIMESTAMP};\nexport const BUILD_VERSION = '${COMMIT_HASH}';\n`, 19 | ); 20 | } 21 | 22 | async function createVersionJson(outDir: string) { 23 | await fs.writeFile(path.resolve(outDir, 'dist/buildinfo.json'), JSON.stringify({ 24 | sha: COMMIT_HASH, 25 | timestamp: TIMESTAMP, 26 | })); 27 | } 28 | 29 | export async function createVersion(outDir: string) { 30 | await createVersionTs(outDir); 31 | await createVersionJson(outDir); 32 | } 33 | 34 | const { 35 | TARGET_DIR = '.', 36 | } = process.env; 37 | 38 | createVersion(TARGET_DIR).catch(console.error); 39 | -------------------------------------------------------------------------------- /packages/lib/core/src/i18n/pt.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export default {"env":{"system_init_message":"Você é um assistente útil"},"command":{"help":{"summary":"Os seguintes comandos são suportados atualmente:\n","help":"Obter ajuda sobre comandos","new":"Iniciar uma nova conversa","start":"Obter seu ID e iniciar uma nova conversa","img":"Gerar uma imagem, o formato completo do comando é `/img descrição da imagem`, por exemplo `/img praia ao luar`","version":"Obter o número da versão atual para determinar se é necessário atualizar","setenv":"Definir configuração do usuário, o formato completo do comando é /setenv CHAVE=VALOR","setenvs":"Definir configurações do usuário em lote, o formato completo do comando é /setenvs {\"CHAVE1\": \"VALOR1\", \"CHAVE2\": \"VALOR2\"}","delenv":"Excluir configuração do usuário, o formato completo do comando é /delenv CHAVE","clearenv":"Limpar todas as configurações do usuário","system":"Ver algumas informações do sistema","redo":"Refazer a última conversa, /redo com conteúdo modificado ou diretamente /redo","echo":"Repetir a mensagem","models":"Mudar o modelo de diálogo"},"new":{"new_chat_start":"Uma nova conversa foi iniciada"}},"callback_query":{"open_model_list":"Abra a lista de modelos","select_provider":"Escolha um fornecedor de modelos.:","select_model":"Escolha um modelo:","change_model":"O modelo de diálogo já foi modificado para"}} -------------------------------------------------------------------------------- /doc/en/PLATFORM.md: -------------------------------------------------------------------------------- 1 | # Supported platforms 2 | 3 | ### 1. [Cloudflare Workers](https://workers.cloudflare.com/) 4 | 5 | The easiest way, the deployment method supported by this project by default, see [Deployment Process](DEPLOY.md) for details. Free, no domain, no server, no need to configure local development environment. kv storage, no database, but there are some storage limitations. 6 | 7 | > KV write limit is 1000 times per day, but it should be enough for chatbot. (Debug mode `DEBUG_MODE` will save the latest message to KV, token stats will update the stats every time the conversation is successful, so there will be a certain number of writes. If you regularly use it more than 1000 times, consider turning off debug mode and token usage statistics) 8 | 9 | ### 2. [Vercel](https://vercel.com/) 10 | 11 | See details at [Vercel](VERCEL.md). It is free, does not require a domain name or server. Deployment requires configuring the local development environment and cannot be done by copying and pasting. There is no storage service, so you need to configure your own database. You can use the free Redis from [UpStash Redis](https://upstash.com). It can connect to GitHub for automatic deployment, but you need to understand Vercel's configuration. 12 | 13 | ### 3. Local 14 | 15 | See [Local](LOCAL.md) for details. For local deployment method, you need to configure local development environment. Supports Docker deployment. 16 | -------------------------------------------------------------------------------- /.github/workflows/cloudflare.yml: -------------------------------------------------------------------------------- 1 | name: Deploy to Cloudflare Workers 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | workflow_dispatch: 7 | 8 | jobs: 9 | deploy: 10 | runs-on: ubuntu-latest 11 | name: Deploy to Cloudflare 12 | 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v3 16 | 17 | - name: Setup Node.js 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: 20 21 | 22 | - name: Create Wrangler config 23 | run: | 24 | touch wrangler.toml 25 | cat << 'EOF' > wrangler.toml 26 | ${{secrets.WRANGLER_TOML}} 27 | EOF 28 | 29 | - name: Install dependencies 30 | run: | 31 | npm install -g pnpm 32 | pnpm install 33 | 34 | - name: Build project 35 | run: pnpm run build 36 | 37 | - name: Deploy to Cloudflare 38 | env: 39 | CLOUDFLARE_API_TOKEN: ${{ secrets.CF_API_TOKEN }} 40 | run: pnpm run wrangler deploy --config=wrangler.toml > /dev/null 41 | 42 | - name: Verify deployment 43 | run: | 44 | url=${{ secrets.CF_WORKERS_DOMAIN }} 45 | if [ -z "$url" ]; then 46 | echo "::warning::CF_WORKERS_DOMAIN is not set" 47 | else 48 | echo "Verifying deployment at https://$url/init" 49 | curl -s -o /dev/null https://$url/init 50 | fi -------------------------------------------------------------------------------- /doc/en/VERCEL.md: -------------------------------------------------------------------------------- 1 | # Deploy using Vercel (Experimental) 2 | 3 | The sample code provided in `/packages/app/vercel` can complete the Vercel deployment and basic functionality testing. However, it cannot guarantee that all features will work normally. 4 | 5 | ### Automatic Deployment 6 | 7 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2FTBXark%2FChatGPT-Telegram-Workers&env=UPSTASH_REDIS_REST_URL,UPSTASH_REDIS_REST_TOKEN,TELEGRAM_AVAILABLE_TOKENS&project-name=chatgpt-telegram-workers&repository-name=ChatGPT-Telegram-Workers&demo-title=ChatGPT-Telegram-Workers&demo-description=Deploy%20your%20own%20Telegram%20ChatGPT%20bot%20on%20Cloudflare%20Workers%20with%20ease.&demo-url=https%3A%2F%2Fchatgpt-telegram-workers.vercel.app) 8 | 9 | ### Manual deployment 10 | 11 | ```shell 12 | pnpm install 13 | pnpm deploy:vercel 14 | ``` 15 | 1. You may need to log in to your Vercel account during the pnpm deploy:vercel process. 16 | 2. For the first deployment, due to missing environment variables, the page will report errors. You need to manually go to the Vercel console to add environment variables, and then redeploy to take effect. 17 | 3. You can reuse the `wrangler.toml` configuration file of Cloudflare Workers, just need to execute `pnpm run vercel:syncenv` to synchronize environment variables to Vercel. After Vercel modifies the environment variables, a redeployment is required for them to take effect. -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu, { imports, javascript, jsdoc, node, typescript } from '@antfu/eslint-config'; 2 | 3 | /** @type {import('eslint-flat-config-utils').FlatConfigComposer} */ 4 | const config = antfu( 5 | { 6 | type: 'app', 7 | stylistic: { 8 | indent: 4, 9 | quotes: 'single', 10 | semi: true, 11 | // @ts-ignore 12 | braceStyle: '1tbs', 13 | }, 14 | markdown: false, 15 | typescript: true, 16 | ignores: [ 17 | '.github/**', 18 | '.idea/**', 19 | '.vscode/**', 20 | '.wrangler/**', 21 | 'dist/**', 22 | 'node_modules/**', 23 | ], 24 | }, 25 | imports, 26 | jsdoc, 27 | typescript, 28 | javascript, 29 | node, 30 | { 31 | rules: { 32 | 'jsdoc/no-undefined-types': 'off', 33 | 'jsdoc/require-returns-description': 'off', 34 | 'jsdoc/require-property-description': 'off', 35 | 'jsdoc/require-param-description': 'off', 36 | 'node/prefer-global/process': 'off', 37 | 'node/prefer-global/buffer': 'off', 38 | 'eslint-comments/no-unlimited-disable': 'off', 39 | 'padding-line-between-statements': 'off', 40 | 'no-console': 'off', 41 | 'style/brace-style': ['error', '1tbs', { allowSingleLine: true }], 42 | }, 43 | }, 44 | ); 45 | 46 | export default config; 47 | -------------------------------------------------------------------------------- /packages/lib/core/src/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import en from './en'; 2 | import pt from './pt'; 3 | import zhHans from './zh-hans'; 4 | import zhHant from './zh-hant'; 5 | 6 | interface HelpI18n { 7 | summary: string; 8 | help: string; 9 | new: string; 10 | start: string; 11 | img: string; 12 | version: string; 13 | setenv: string; 14 | setenvs: string; 15 | delenv: string; 16 | system: string; 17 | redo: string; 18 | models: string; 19 | echo: string; 20 | } 21 | 22 | export interface I18n { 23 | env: { 24 | system_init_message: string; 25 | }; 26 | command: { 27 | help: HelpI18n & Record; 28 | new: { 29 | new_chat_start: string; 30 | }; 31 | }; 32 | callback_query: { 33 | open_model_list: string; 34 | select_provider: string; 35 | select_model: string; 36 | change_model: string; 37 | }; 38 | } 39 | 40 | export function loadI18n(lang?: string): I18n { 41 | switch (lang?.toLowerCase()) { 42 | case 'cn': 43 | case 'zh-cn': 44 | case 'zh-hans': 45 | return zhHans; 46 | case 'zh-tw': 47 | case 'zh-hk': 48 | case 'zh-mo': 49 | case 'zh-hant': 50 | return zhHant; 51 | case 'pt': 52 | case 'pt-br': 53 | return pt; 54 | case 'en': 55 | case 'en-us': 56 | return en; 57 | default: 58 | return en; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/lib/core/src/utils/cache/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * A simple cache implementation. 3 | * 主要作用 4 | * 1. 防止本地部署使用base64图片时,重复请求相同的图片 5 | * 2. 上传图片telegraph后又使用base64图片时,重复请求相同的图片 6 | */ 7 | export class Cache { 8 | private readonly maxItems: number; 9 | private readonly maxAge: number; 10 | private readonly cache: Record; 11 | 12 | constructor() { 13 | this.maxItems = 10; 14 | this.maxAge = 1000 * 60 * 60; 15 | this.cache = {}; 16 | this.set = this.set.bind(this); 17 | this.get = this.get.bind(this); 18 | } 19 | 20 | set(key: string, value: T) { 21 | this.trim(); 22 | this.cache[key] = { 23 | value, 24 | time: Date.now(), 25 | }; 26 | } 27 | 28 | get(key: string): T | undefined | null { 29 | this.trim(); 30 | return this.cache[key]?.value; 31 | } 32 | 33 | private trim() { 34 | let keys = Object.keys(this.cache); 35 | for (const key of keys) { 36 | if (Date.now() - this.cache[key].time > this.maxAge) { 37 | delete this.cache[key]; 38 | } 39 | } 40 | keys = Object.keys(this.cache); 41 | if (keys.length > this.maxItems) { 42 | keys.sort((a, b) => this.cache[a].time - this.cache[b].time); 43 | for (let i = 0; i < keys.length - this.maxItems; i++) { 44 | delete this.cache[keys[i]]; 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/command/auth.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerContext } from '#/config'; 2 | import type * as Telegram from 'telegram-bot-api-types'; 3 | import { ENV } from '#/config'; 4 | import { createTelegramBotAPI } from '../api'; 5 | 6 | export async function loadChatRoleWithContext(chatId: number, speakerId: number, context: WorkerContext): Promise { 7 | const { groupAdminsKey } = context.SHARE_CONTEXT; 8 | if (!groupAdminsKey) { 9 | return null; 10 | } 11 | 12 | let groupAdmin: Telegram.ChatMember[] | null = null; 13 | try { 14 | groupAdmin = JSON.parse(await ENV.DATABASE.get(groupAdminsKey)); 15 | } catch (e) { 16 | console.error(e); 17 | } 18 | if (groupAdmin === null || !Array.isArray(groupAdmin) || groupAdmin.length === 0) { 19 | const api = createTelegramBotAPI(context.SHARE_CONTEXT.botToken); 20 | const result = await api.getChatAdministratorsWithReturns({ chat_id: chatId }); 21 | if (result == null) { 22 | return null; 23 | } 24 | groupAdmin = result.result; 25 | // 缓存120s 26 | await ENV.DATABASE.put( 27 | groupAdminsKey, 28 | JSON.stringify(groupAdmin), 29 | { expiration: (Date.now() / 1000) + 120 }, 30 | ); 31 | } 32 | for (let i = 0; i < groupAdmin.length; i++) { 33 | const user = groupAdmin[i]; 34 | if (`${user.user?.id}` === `${speakerId}`) { 35 | return user.status; 36 | } 37 | } 38 | return 'member'; 39 | } 40 | -------------------------------------------------------------------------------- /scripts/vercel-sync-env.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'node:child_process'; 2 | import fs from 'node:fs/promises'; 3 | import { parse } from 'toml'; 4 | 5 | async function main() { 6 | const { 7 | TOML_PATH = 'wrangler.toml', 8 | VERCEL_ENV = 'production', 9 | VERCEL_BIN = './node_modules/.bin/vercel', 10 | } = process.env; 11 | 12 | const envs = execSync(`${VERCEL_BIN} env ls ${VERCEL_ENV}`, { encoding: 'utf-8' }) 13 | .trim() 14 | .split('\n') 15 | .map(l => l.trim()) 16 | .slice(1) 17 | .map(l => l.split(/\s+/)[0]) 18 | .map(l => l.replace(/[^\x20-\x7E]/g, '').replace(/\[\d+m/g, '')); 19 | const usedKeys = new Set(); 20 | usedKeys.add('UPSTASH_REDIS_REST_URL'); 21 | usedKeys.add('UPSTASH_REDIS_REST_TOKEN'); 22 | const { vars } = parse(await fs.readFile(TOML_PATH, 'utf-8')); 23 | for (const [key, value] of Object.entries(vars)) { 24 | try { 25 | usedKeys.add(key); 26 | execSync(`${VERCEL_BIN} env add ${key} ${VERCEL_ENV} --force`, { 27 | input: `${value}`, 28 | encoding: 'utf-8', 29 | }); 30 | } catch (e) { 31 | console.error(e); 32 | } 33 | } 34 | for (const key of envs) { 35 | if (!usedKeys.has(key)) { 36 | console.log(`Delete ${key}?)`); 37 | execSync(`${VERCEL_BIN} env rm ${key} ${VERCEL_ENV}`, { 38 | encoding: 'utf-8', 39 | }); 40 | } 41 | } 42 | } 43 | 44 | main().catch(console.error); 45 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/openai_agents.ts: -------------------------------------------------------------------------------- 1 | import { OpenAICompatibilityAgent } from '#/agent/openai_compatibility'; 2 | 3 | export class DeepSeek extends OpenAICompatibilityAgent { 4 | constructor() { 5 | super('deepseek', { 6 | base: 'DEEPSEEK_API_BASE', 7 | key: 'DEEPSEEK_API_KEY', 8 | model: 'DEEPSEEK_CHAT_MODEL', 9 | modelsList: 'DEEPSEEK_CHAT_MODELS_LIST', 10 | extraParams: 'DEEPSEEK_CHAT_EXTRA_PARAMS', 11 | }); 12 | } 13 | } 14 | 15 | export class Groq extends OpenAICompatibilityAgent { 16 | constructor() { 17 | super('groq', { 18 | base: 'GROQ_API_BASE', 19 | key: 'GROQ_API_KEY', 20 | model: 'GROQ_CHAT_MODEL', 21 | modelsList: 'GROQ_CHAT_MODELS_LIST', 22 | extraParams: 'GROQ_CHAT_EXTRA_PARAMS', 23 | }); 24 | } 25 | } 26 | 27 | export class Mistral extends OpenAICompatibilityAgent { 28 | constructor() { 29 | super('mistral', { 30 | base: 'MISTRAL_API_BASE', 31 | key: 'MISTRAL_API_KEY', 32 | model: 'MISTRAL_CHAT_MODEL', 33 | modelsList: 'MISTRAL_CHAT_MODELS_LIST', 34 | extraParams: 'MISTRAL_CHAT_EXTRA_PARAMS', 35 | }); 36 | } 37 | } 38 | 39 | export class XAi extends OpenAICompatibilityAgent { 40 | constructor() { 41 | super('xai', { 42 | base: 'XAI_API_BASE', 43 | key: 'XAI_API_KEY', 44 | model: 'XAI_CHAT_MODEL', 45 | modelsList: 'XAI_CHAT_MODELS_LIST', 46 | extraParams: 'XAI_CHAT_EXTRA_PARAMS', 47 | }); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/apps/interpolate/src/index.ts: -------------------------------------------------------------------------------- 1 | import { interpolate } from '@chatgpt-telegram-workers/plugins'; 2 | 3 | const templateEle = document.getElementById('template') as HTMLInputElement; 4 | const dataEle = document.getElementById('data') as HTMLInputElement; 5 | const previewEle = document.getElementById('preview') as HTMLElement; 6 | 7 | function updatePreview() { 8 | const template = templateEle.value; 9 | const data = dataEle.value; 10 | try { 11 | previewEle.innerHTML = interpolate(template, JSON.parse(data)); 12 | } catch (e) { 13 | previewEle.innerHTML = `${(e as Error).message}`; 14 | } 15 | } 16 | 17 | templateEle.addEventListener('input', updatePreview); 18 | dataEle.addEventListener('input', updatePreview); 19 | 20 | templateEle.value = ` 21 | DNS query: {{Question[0].name}} 22 | Status: {{#if TC}}TC,{{/if}}{{#if RD}}RD,{{/if}}{{#if RA}}RA,{{/if}}{{#if AD}}AD,{{/if}}{{#if CD}}CD,{{/if}}{{Status}} 23 | 24 | Answer{{#each answer in Answer}} 25 | {{answer.name}}, {{answer.type}}, (TTL: {{answer.TTL}}),{{answer.data}}{{/each}} 26 | `; 27 | 28 | dataEle.value = JSON.stringify({ 29 | Status: 0, 30 | TC: false, 31 | RD: true, 32 | RA: true, 33 | AD: false, 34 | CD: false, 35 | Question: [ 36 | { 37 | name: 'google.com', 38 | type: 1, 39 | }, 40 | ], 41 | Answer: [ 42 | { 43 | name: 'google.com', 44 | type: 1, 45 | TTL: 300, 46 | data: '172.217.24.110', 47 | }, 48 | ], 49 | }, null, 2); 50 | updatePreview(); 51 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 |

2 | ChatGPT-Telegram-Workers 3 |

4 | 5 |

6 |
English | 中文 7 |

8 |

9 | 轻松在Cloudflare Workers上部署您自己的Telegram ChatGPT机器人。 10 |

11 | 12 | 13 | ## 关于 14 | 15 | 最简单快捷部署属于自己的ChatGPT Telegram机器人的方法。使用Cloudflare Workers,单文件,直接复制粘贴一把梭,无需任何依赖,无需配置本地开发环境,不用域名,免服务器。 可以自定义系统初始化信息,让你调试好的性格永远不消失。 16 | 17 |
18 | 查看Demo 19 | image 20 |
21 | 22 | 23 | ## 特性 24 | 25 | - 无服务器部署 26 | - 多平台部署支持(Cloudflare Workers, Vercel, Docker[...](doc/cn/PLATFORM.md)) 27 | - 适配多种AI服务商(OpenAI, Azure OpenAI, Cloudflare AI, Cohere, Anthropic, Mistral...) 28 | - 使用 InlineKeyboards 切换模型 29 | - 自定义指令(可以实现快速切换模型,切换机器人预设) 30 | - 支持多个Telegram机器人 31 | - 流式输出 32 | - 多语言支持 33 | - 文字生成图片 34 | - [插件系统](doc/cn/PLUGINS.md),可以自定义插件 35 | 36 | 37 | ## 文档 38 | 39 | - [部署Cloudflare Workers](./doc/cn/DEPLOY.md) 40 | - [本地(或Docker)部署](./doc/cn/LOCAL.md) 41 | - [部署其他平台](./doc/cn/PLATFORM.md) 42 | - [配置参数和指令](./doc/cn/CONFIG.md) 43 | - [自动更新](./doc/cn/ACTION.md) 44 | - [变更日志](./doc/cn/CHANGELOG.md) 45 | 46 | 47 | ## 关联项目 48 | 49 | - [cloudflare-worker-adapter](https://github.com/TBXark/cloudflare-worker-adapter) 一个简单的Cloudflare Worker适配器,让本项目脱离Cloudflare Worker独立运行 50 | - [telegram-bot-api-types](https://github.com/TBXark/telegram-bot-api-types) 编译后0输出的Telegram Bot API SDK, 文档齐全,支持所有API 51 | 52 | 53 | ## 贡献者 54 | 55 | 这个项目存在是因为所有贡献的人。[贡献](https://github.com/tbxark/ChatGPT-Telegram-Workers/graphs/contributors)。 56 | 57 | 58 | ## 许可证 59 | 60 | **ChatGPT-Telegram-Workers** 以 MIT 许可证发布。[详见 LICENSE](LICENSE) 获取详情。 61 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/agent.ts: -------------------------------------------------------------------------------- 1 | import type { AgentUserConfig } from '#/config'; 2 | import type { ChatAgent, ImageAgent } from './types'; 3 | import { DeepSeek, Groq, Mistral, XAi } from '#/agent/openai_agents'; 4 | import { Anthropic } from './anthropic'; 5 | import { AzureChatAI, AzureImageAI } from './azure'; 6 | import { Cohere } from './cohere'; 7 | import { Gemini } from './gemini'; 8 | import { Dalle, OpenAI } from './openai'; 9 | import { WorkersChat, WorkersImage } from './workersai'; 10 | 11 | export const CHAT_AGENTS: ChatAgent[] = [ 12 | new OpenAI(), 13 | new Anthropic(), 14 | new AzureChatAI(), 15 | new WorkersChat(), 16 | new Cohere(), 17 | new Gemini(), 18 | new Mistral(), 19 | new DeepSeek(), 20 | new Groq(), 21 | new XAi(), 22 | ]; 23 | 24 | export function loadChatLLM(context: AgentUserConfig): ChatAgent | null { 25 | for (const llm of CHAT_AGENTS) { 26 | if (llm.name === context.AI_PROVIDER) { 27 | return llm; 28 | } 29 | } 30 | // 找不到指定的AI,使用第一个可用的AI 31 | for (const llm of CHAT_AGENTS) { 32 | if (llm.enable(context)) { 33 | return llm; 34 | } 35 | } 36 | return null; 37 | } 38 | 39 | export const IMAGE_AGENTS: ImageAgent[] = [ 40 | new AzureImageAI(), 41 | new Dalle(), 42 | new WorkersImage(), 43 | ]; 44 | 45 | export function loadImageGen(context: AgentUserConfig): ImageAgent | null { 46 | for (const imgGen of IMAGE_AGENTS) { 47 | if (imgGen.name === context.AI_IMAGE_PROVIDER) { 48 | return imgGen; 49 | } 50 | } 51 | // 找不到指定的AI,使用第一个可用的AI 52 | for (const imgGen of IMAGE_AGENTS) { 53 | if (imgGen.enable(context)) { 54 | return imgGen; 55 | } 56 | } 57 | return null; 58 | } 59 | -------------------------------------------------------------------------------- /doc/cn/LOCAL.md: -------------------------------------------------------------------------------- 1 | # 本地部署 2 | 3 | 4 | ## 配置 5 | 6 | ### 1. 服务器配置`CONFIG_PATH` 7 | 8 | ```json5 9 | { 10 | "database": { 11 | "type": "local",// memory, local, sqlite, redis 12 | "path": "/app/data.json" // your database path 13 | }, 14 | "server": { // server configuration for webhook mode 15 | "hostname": "0.0.0.0", 16 | "port": 3000, // must 8787 when using docker 17 | "baseURL": "https://example.com" 18 | }, 19 | 'proxy': 'http://127.0.0.1:7890', // proxy for telegram api 20 | "mode": "webhook", // webhook, polling 21 | } 22 | ``` 23 | 24 | ### 2. toml 配置`TOML_PATH` 25 | toml 内容与cloudflare workers配置文件兼容 26 | 27 | 28 | ## 本地运行 29 | 30 | ```shell 31 | pnpm install 32 | pnpm run start:local 33 | ``` 34 | or 35 | 36 | ```shell 37 | pnpm install 38 | pnpm run build:local 39 | CONFIG_PATH=./config.json TOML_PATH=./wrangler.toml pnpm run start:dist 40 | ``` 41 | 42 | 43 | ## Docker 运行 44 | 45 | ### 1. 编译image 46 | 47 | ```bash 48 | docker build -t chatgpt-telegram-workers:latest . 49 | ``` 50 | or 51 | ```shell 52 | pnpm run build:docker # 更快(直接使用本地构建的结果创建镜像) 53 | ``` 54 | 55 | ### 2. 运行容器 56 | 57 | ```bash 58 | docker run -d -p 8787:8787 -v $(pwd)/config.json:/app/config.json:ro -v $(pwd)/wrangler.toml:/app/config.toml:ro chatgpt-telegram-workers:latest 59 | ``` 60 | 61 | 62 | ## docker-compose 运行 63 | 64 | 自行修改docker-compose.yml中的配置文件路径 65 | 66 | ```bash 67 | docker-compose up # edit the docker-compose.yml to change the config file path 68 | ``` 69 | 70 | 71 | ## 使用Docker hub镜像 72 | 73 | https://github.com/TBXark/ChatGPT-Telegram-Workers/pkgs/container/chatgpt-telegram-workers 74 | 75 | ```shell 76 | docker pull ghcr.io/tbxark/chatgpt-telegram-workers:latest 77 | docker run -d -p 8787:8787 -v $(pwd)/config.json:/app/config.json:ro -v $(pwd)/wrangler.toml:/app/config.toml:ro ghcr.io/tbxark/chatgpt-telegram-workers:latest 78 | ``` -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/handler/index.ts: -------------------------------------------------------------------------------- 1 | import type * as Telegram from 'telegram-bot-api-types'; 2 | import type { UpdateHandler } from './types'; 3 | import { WorkerContext } from '#/config'; 4 | import { GroupMention } from './group'; 5 | import { 6 | CallbackQueryHandler, 7 | ChatHandler, 8 | CommandHandler, 9 | EnvChecker, 10 | MessageFilter, 11 | OldMessageFilter, 12 | SaveLastMessage, 13 | Update2MessageHandler, 14 | WhiteListFilter, 15 | } from './handlers'; 16 | 17 | // 消息处理中间件 18 | const SHARE_HANDLER: UpdateHandler[] = [ 19 | // 检查环境是否准备好: DATABASE 20 | new EnvChecker(), 21 | // 过滤非白名单用户, 提前过滤减少KV消耗 22 | new WhiteListFilter(), 23 | // 回调处理 24 | new CallbackQueryHandler(), 25 | // 消息处理 26 | new Update2MessageHandler([ 27 | // 过滤不支持的消息(抛出异常结束消息处理) 28 | new MessageFilter(), 29 | // 处理群消息,判断是否需要响应此条消息 30 | new GroupMention(), 31 | // 忽略旧消息 32 | new OldMessageFilter(), 33 | // DEBUG: 保存最后一条消息,按照需求自行调整此中间件位置 34 | new SaveLastMessage(), 35 | // 处理命令消息 36 | new CommandHandler(), 37 | // 与llm聊天 38 | new ChatHandler(), 39 | ]), 40 | ]; 41 | 42 | export async function handleUpdate(token: string, update: Telegram.Update): Promise { 43 | const context = await WorkerContext.from(token, update); 44 | 45 | for (const handler of SHARE_HANDLER) { 46 | try { 47 | const result = await handler.handle(update, context); 48 | if (result) { 49 | return result; 50 | } 51 | } catch (e) { 52 | return new Response(JSON.stringify({ 53 | message: (e as Error).message, 54 | stack: (e as Error).stack, 55 | }), { status: 500 }); 56 | } 57 | } 58 | return null; 59 | } 60 | -------------------------------------------------------------------------------- /wrangler-example.toml: -------------------------------------------------------------------------------- 1 | # Change the name here to the name of your own workers. 2 | name = 'chatgpt-telegram-workers' 3 | workers_dev = true 4 | compatibility_date = "2024-11-11" 5 | compatibility_flags = ["nodejs_compat_v2"] 6 | 7 | # Deploy dist 8 | main = './dist/index.js' # Default use of dist/index.js 9 | 10 | # Deploy Core version 11 | # > pnpm run deploy:workers 12 | #main = './packages/apps/workers/src/index.ts' 13 | #or 14 | #main = './packages/apps/workers/dist/index.js' 15 | 16 | # Deploy Vercel AI version 17 | # > pnpm run deploy:workersnext 18 | #main = './packages/apps/workers-next/src/index.ts' 19 | #or 20 | #main = './packages/apps/workers-next/dist/index.js' 21 | 22 | # Deploy Telegram MarkdownV2 version 23 | # > pnpm run deploy:workersmk2 24 | #main = './packages/apps/workers-mk2/src/index.ts' 25 | #or 26 | #main = './packages/apps/workers-mk2/dist/index.js' 27 | 28 | 29 | # The id here is a required field 30 | # Please fill in your kv namespace id 31 | # preview_id is only used for debugging, please delete if not needed 32 | # If you want to know how to obtain the ID of kv_namespaces, please check here. 33 | # https://github.com/TBXark/ChatGPT-Telegram-Workers/blob/master/doc/en/DEPLOY.md 34 | kv_namespaces = [ 35 | { binding = 'DATABASE', id = '', preview_id = '' } 36 | ] 37 | 38 | 39 | [vars] 40 | # More parameters usage, please see https://github.com/TBXark/ChatGPT-Telegram-Workers/blob/master/doc/en/CONFIG.md 41 | # All variables must be strings, and the values of multiple parameters are separated by commas, such as "a,b,c" 42 | # If the value is a boolean, it is written as "true" or "false" 43 | # If the value is a number, it is written as a string, such as "1", "2", "3" 44 | # If the value is a JSON string, it is written as a string, such as '["a","b","c"]' 45 | 46 | # TELEGRAM_AVAILABLE_TOKENS = 'token1,token2' 47 | # CHAT_WHITE_LIST = 'chat_id1,chat_id2' -------------------------------------------------------------------------------- /packages/lib/core/src/agent/gemini.ts: -------------------------------------------------------------------------------- 1 | import type { AgentUserConfig } from '#/config'; 2 | import type { 3 | ChatAgent, 4 | } from './types'; 5 | import { 6 | agentConfigFieldGetter, 7 | createAgentEnable, 8 | createAgentModel, 9 | createOpenAIRequest, 10 | defaultOpenAIRequestBuilder, 11 | ImageSupportFormat, 12 | } from '#/agent/openai_compatibility'; 13 | import { getAgentUserConfigFieldName, loadModelsList } from './utils'; 14 | 15 | export class Gemini implements ChatAgent { 16 | readonly name = 'gemini'; 17 | readonly modelKey = getAgentUserConfigFieldName('GOOGLE_CHAT_MODEL'); 18 | 19 | readonly fieldGetter = agentConfigFieldGetter({ 20 | base: 'GOOGLE_API_BASE', 21 | key: 'GOOGLE_API_KEY', 22 | model: 'GOOGLE_CHAT_MODEL', 23 | modelsList: 'GOOGLE_CHAT_MODELS_LIST', 24 | extraParams: 'GOOGLE_CHAT_EXTRA_PARAMS', 25 | }); 26 | 27 | readonly enable = createAgentEnable(this.fieldGetter); 28 | readonly model = createAgentModel(this.fieldGetter); 29 | readonly request = createOpenAIRequest(defaultOpenAIRequestBuilder(this.fieldGetter, '/openai/chat/completions', [ImageSupportFormat.BASE64])); 30 | 31 | readonly modelList = async (context: AgentUserConfig): Promise => { 32 | if (context.GOOGLE_CHAT_MODELS_LIST === '') { 33 | context.GOOGLE_CHAT_MODELS_LIST = `${context.GOOGLE_API_BASE}/models`; 34 | } 35 | return loadModelsList(context.GOOGLE_CHAT_MODELS_LIST, async (url): Promise => { 36 | const data = await fetch(`${url}?key=${context.GOOGLE_API_KEY}`).then(r => r.json()); 37 | return data?.models 38 | ?.filter((model: any) => model.supportedGenerationMethods?.includes('generateContent')) 39 | .map((model: any) => model.name.split('/').pop()) ?? []; 40 | }); 41 | }; 42 | } 43 | -------------------------------------------------------------------------------- /doc/cn/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 更新日志 2 | 3 | - v1.10.7 4 | - 修复 `Groq` 名称拼写错误 5 | - 添加 `Xai` 独立Agent支持 6 | 7 | - v1.10.6 8 | - 添加`DeepSeek` 和 `Groq`独立Agent支持 9 | 10 | - v1.10.5 11 | - 添加`Telegram MarkdownV2`完整支持 12 | 13 | - v1.10.0 14 | - 使用 InlineKeyboards 切换模型 15 | 16 | - v1.9.0 17 | - 添加插件系统 18 | 19 | - v1.8.0 20 | - 支持Cohere,Anthropic Ai 21 | - 支持图片输入 22 | - 适配群组话题模式 23 | - 移除role功能,使用自定义指令代替 24 | - 修复超长文本发送失败BUG 25 | 26 | - v1.7.0 27 | - 修改 worker ai 调用方式为 api 调用,需要设置 account_id 和 token, 原有AI绑定方式失效 28 | - 添加 worker ai 文字对话流模式支持 29 | - 添加 worker ai 文字生成图片功能 30 | - 添加添加AI提供商切换功能 31 | - 添加自定义指令功能,可以实现快速模型切换 32 | - 添加锁定用户自定义配置功能 33 | 34 | - v1.6.0 35 | - 添加workers ai支持,具体配置查看配置文档 36 | - 优化openai流模式解析器 37 | 38 | - v1.5.0 39 | - perf: 调整命令顺序 40 | - perf: openai发送请求前前发送loading消息 41 | - feat: 添加流式输出支持。默认开启。使用`STREAM_MODE=false`关闭 42 | - feat: 增加对多个KEY的适配,随机选择KEY使用 43 | - feat: 增加快捷按钮 `/new`, `/redo` 44 | 45 | - v1.4.0 46 | - 支持多平台部署 47 | - 添加`/redo`指令,重新发送或者修改上一条提问 48 | - 添加`/delenv`指令,删除环境变量恢复默认值 49 | - 添加多语言支持,使用`LANGUAGE`环境变量设置语言,目前支持`zh-CN`,`zh-TW`和`en`。默认为`zh-CN`。 50 | 51 | - v1.3.1 52 | - 优化历史记录裁剪逻辑 53 | - 优化token计算逻辑 54 | - 修复edit消息的bug 55 | 56 | - v1.3.0 57 | - 添加token使用统计指令`/usage` 58 | - 添加系统信息指令`/system` 59 | - 添加command菜单显示范围 60 | - 添加`SYSTEM_INIT_MESSAGE`环境变量 61 | - 添加`CHAT_MODEL`环境变量 62 | - 添加`Github Action`自动更新部署脚本 63 | - 优化`/init`页面 显示更多错误信息 64 | - 修复历史记录裁剪BUG 65 | - 修复`USER_CONFIG`加载异常BUG 66 | - 修复把错误信息存入历史记录BUG 67 | 68 | - v1.2.0 69 | - 修复高危漏洞,必须更新 70 | 71 | - v1.1.0 72 | - 由单文件改为多文件,方便维护,提供dist目录,方便复制粘贴。 73 | - 删除和新增部分配置,提供兼容性代码,方便升级。 74 | - 修改KV key生成逻辑,可能导致之前的数据丢失,可手动修改key或重新配置。 75 | - 修复部分bug 76 | - 自动绑定所有指令 77 | - BREAKING CHANGE: 重大改动,必须把群ID加到白名单`CHAT_GROUP_WHITE_LIST`才能使用, 否则任何人都可以把你的机器人加到群组中,然后消耗你的配额。 78 | 79 | - v1.0.0 80 | - 初始版本 81 | -------------------------------------------------------------------------------- /vite.config.shared.ts: -------------------------------------------------------------------------------- 1 | import type { LibraryFormats, Plugin, UserConfig } from 'vite'; 2 | import * as path from 'node:path'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import cleanup from 'rollup-plugin-cleanup'; 5 | import { nodeExternals } from 'rollup-plugin-node-externals'; 6 | import { defineConfig } from 'vite'; 7 | import checker from 'vite-plugin-checker'; 8 | import dts from 'vite-plugin-dts'; 9 | 10 | export interface Options { 11 | root: string; 12 | types?: boolean; 13 | formats?: LibraryFormats[]; 14 | nodeExternals?: boolean; 15 | excludeMonoRepoPackages?: boolean; 16 | } 17 | 18 | export function createShareConfig(options: Options): UserConfig { 19 | const plugins: Plugin[] = [ 20 | nodeResolve({ 21 | browser: false, 22 | preferBuiltins: true, 23 | }), 24 | cleanup({ 25 | comments: 'none', 26 | extensions: ['js', 'ts'], 27 | }), 28 | checker({ 29 | typescript: true, 30 | }), 31 | ]; 32 | if (options.types) { 33 | plugins.push( 34 | dts({ 35 | rollupTypes: true, 36 | }), 37 | ); 38 | } 39 | if (options.nodeExternals) { 40 | const exclude = new Array(); 41 | if (options.excludeMonoRepoPackages) { 42 | exclude.push(/^@chatgpt-telegram-workers\/.+/); 43 | } 44 | plugins.push( 45 | nodeExternals({ 46 | exclude, 47 | }), 48 | ); 49 | } 50 | return defineConfig({ 51 | plugins, 52 | build: { 53 | target: 'esnext', 54 | lib: { 55 | entry: path.resolve(options.root, 'src/index'), 56 | fileName: 'index', 57 | formats: options.formats || ['es'], 58 | }, 59 | sourcemap: true, 60 | minify: false, 61 | outDir: path.resolve(options.root, 'dist'), 62 | }, 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /doc/en/LOCAL.md: -------------------------------------------------------------------------------- 1 | # Local deployment 2 | 3 | 4 | ## Configuration 5 | 6 | ### 1. Server Configuration`CONFIG_PATH` 7 | 8 | ```json5 9 | { 10 | "database": { 11 | "type": "local",// memory, local, sqlite, redis 12 | "path": "/app/data.json" // your database path 13 | }, 14 | "server": { // server configuration for webhook mode 15 | "hostname": "0.0.0.0", 16 | "port": 3000, // must 8787 when using docker 17 | "baseURL": "https://example.com" 18 | }, 19 | 'proxy': 'http://127.0.0.1:7890', // proxy for telegram api 20 | "mode": "webhook", // webhook, polling 21 | } 22 | ``` 23 | 24 | ### 2. TOML configuration`TOML_PATH` 25 | The toml content is compatible with Cloudflare Workers configuration files. 26 | 27 | 28 | ## Local run 29 | 30 | ```shell 31 | pnpm install 32 | pnpm run start:local 33 | ``` 34 | or 35 | 36 | ```shell 37 | pnpm install 38 | pnpm run build:local 39 | CONFIG_PATH=./config.json TOML_PATH=./wrangler.toml pnpm run start:dist 40 | ``` 41 | 42 | 43 | ## Docker 44 | 45 | ### 1. Build image 46 | 47 | ```bash 48 | docker build -t chatgpt-telegram-workers:latest . 49 | ``` 50 | or 51 | ```shell 52 | pnpm run build:docker # Faster (directly use the locally built results to create the image) 53 | ``` 54 | 55 | ### 2. Run container 56 | 57 | ```bash 58 | docker run -d -p 8787:8787 -v $(pwd)/config.json:/app/config.json:ro -v $(pwd)/wrangler.toml:/app/config.toml:ro chatgpt-telegram-workers:latest 59 | ``` 60 | 61 | 62 | ## docker-compose 63 | 64 | Manually modify the configuration file path in docker-compose.yml. 65 | 66 | ```bash 67 | docker-compose up # edit the docker-compose.yml to change the config file path 68 | ``` 69 | 70 | 71 | ## Use docker hub image 72 | 73 | https://github.com/TBXark/ChatGPT-Telegram-Workers/pkgs/container/chatgpt-telegram-workers 74 | 75 | ```shell 76 | docker pull ghcr.io/tbxark/chatgpt-telegram-workers:latest 77 | docker run -d -p 8787:8787 -v $(pwd)/config.json:/app/config.json:ro -v $(pwd)/wrangler.toml:/app/config.toml:ro ghcr.io/tbxark/chatgpt-telegram-workers:latest 78 | ``` 79 | -------------------------------------------------------------------------------- /packages/lib/core/src/utils/resp/index.ts: -------------------------------------------------------------------------------- 1 | export function renderHTML(body: string): string { 2 | return ` 3 | 4 | 5 | ChatGPT-Telegram-Workers 6 | 7 | 8 | 9 | 10 | 41 | 42 | 43 | ${body} 44 | 45 | 46 | `; 47 | } 48 | 49 | export function errorToString(e: Error | any): string { 50 | return JSON.stringify({ 51 | message: e.message, 52 | stack: e.stack, 53 | }); 54 | } 55 | 56 | export function makeResponse200(resp: Response | null): Response { 57 | if (resp === null) { 58 | return new Response('NOT HANDLED', { status: 200 }); 59 | } 60 | if (resp.status === 200) { 61 | return resp; 62 | } else { 63 | // 如果返回4xx,5xx,Telegram会重试这个消息,后续消息就不会到达,所有webhook的错误都返回200 64 | return new Response(resp.body, { 65 | status: 200, 66 | headers: { 67 | 'Original-Status': `${resp.status}`, 68 | ...resp.headers, 69 | }, 70 | }); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /packages/apps/local/src/telegram.ts: -------------------------------------------------------------------------------- 1 | import type { TelegramBotAPI } from '@chatgpt-telegram-workers/core'; 2 | import type { GetUpdatesResponse } from 'telegram-bot-api-types'; 3 | import type * as Telegram from 'telegram-bot-api-types'; 4 | import { createTelegramBotAPI } from '@chatgpt-telegram-workers/core'; 5 | 6 | export async function runPolling(tokens: string[], handler: (token: string, update: Telegram.Update) => Promise) { 7 | const clients: Record = {}; 8 | const offset: Record = {}; 9 | for (const token of tokens) { 10 | offset[token] = 0; 11 | const api = createTelegramBotAPI(token); 12 | clients[token] = api; 13 | const name = await api.getMeWithReturns(); 14 | await api.deleteWebhook({}); 15 | console.log(`@${name.result.username} Webhook deleted, If you want to use webhook, please set it up again.`); 16 | } 17 | 18 | const keepRunning = true; 19 | // eslint-disable-next-line no-unmodified-loop-condition 20 | while (keepRunning) { 21 | for (const token of tokens) { 22 | try { 23 | const resp = await clients[token].getUpdates({ offset: offset[token] }); 24 | if (resp.status === 429) { 25 | const retryAfter = Number.parseInt(resp.headers.get('Retry-After') || ''); 26 | if (retryAfter) { 27 | console.log(`Rate limited, retry after ${retryAfter} seconds`); 28 | await new Promise(resolve => setTimeout(resolve, retryAfter * 1000)); 29 | continue; 30 | } 31 | } 32 | const { result } = await resp.json() as GetUpdatesResponse; 33 | for (const update of result) { 34 | if (update.update_id >= offset[token]) { 35 | offset[token] = update.update_id + 1; 36 | } 37 | setImmediate(async () => { 38 | await handler(token, update).catch(console.error); 39 | }); 40 | } 41 | } catch (e) { 42 | console.error(e); 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/apps/local/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as fs from 'node:fs'; 2 | import * as process from 'node:process'; 3 | import { CHAT_AGENTS, createRouter, ENV, handleUpdate } from '@chatgpt-telegram-workers/core'; 4 | import { injectNextChatAgent } from '@chatgpt-telegram-workers/next'; 5 | import { createCache, defaultRequestBuilder, initEnv, installFetchProxy, startServerV2 } from 'cloudflare-worker-adapter'; 6 | import convert from 'telegramify-markdown'; 7 | import { runPolling } from './telegram'; 8 | 9 | interface Config { 10 | database: { 11 | type: 'memory' | 'local' | 'sqlite' | 'redis'; 12 | path?: string; 13 | }; 14 | server?: { 15 | hostname?: string; 16 | port?: number; 17 | baseURL: string; 18 | }; 19 | proxy?: string; 20 | mode: 'webhook' | 'polling'; 21 | } 22 | 23 | const { 24 | CONFIG_PATH = '/app/config.json', 25 | TOML_PATH = '/app/wrangler.toml', 26 | NEXT_ENABLE = '0', 27 | } = process.env; 28 | 29 | // 读取配置文件 30 | const config: Config = JSON.parse(fs.readFileSync(CONFIG_PATH, 'utf-8')); 31 | 32 | // 初始化数据库 33 | const cache = createCache(config?.database?.type, { uri: config.database.path || '' }); 34 | console.log(`database: ${config?.database?.type} is ready`); 35 | 36 | // 初始化环境变量 37 | const env = initEnv(TOML_PATH, { DATABASE: cache }); 38 | ENV.DEFAULT_PARSE_MODE = 'MarkdownV2'; 39 | ENV.merge(env); 40 | ENV.CUSTOM_MESSAGE_RENDER = (parse_mode, message) => { 41 | if (parse_mode === 'MarkdownV2') { 42 | return convert(message, 'remove'); 43 | } 44 | return message; 45 | }; 46 | 47 | // 注入 Next.js Chat Agent 48 | if (NEXT_ENABLE !== '0') { 49 | injectNextChatAgent(CHAT_AGENTS); 50 | } 51 | if (config.proxy) { 52 | installFetchProxy(config.proxy); 53 | } 54 | 55 | // 启动服务 56 | if (config.mode === 'webhook' && config.server !== undefined) { 57 | const router = createRouter(); 58 | startServerV2( 59 | config.server.port || 8787, 60 | config.server.hostname || '0.0.0.0', 61 | env, 62 | { baseURL: config.server.baseURL }, 63 | defaultRequestBuilder, 64 | router.fetch, 65 | ); 66 | } else { 67 | runPolling( 68 | ENV.TELEGRAM_AVAILABLE_TOKENS, 69 | handleUpdate, 70 | ).catch(console.error); 71 | } 72 | -------------------------------------------------------------------------------- /doc/cn/ACTION.md: -------------------------------------------------------------------------------- 1 | # Github Action 自动更新流程 2 | 3 | > PS: Cloudflare Workers 已经支持自动构建, 详情见[文档](https://developers.cloudflare.com/workers/ci-cd/builds/) 4 | 5 | ## 1. 手动完成一次部署 6 | 具体部署流程看[部署流程](DEPLOY.md) 7 | 8 | ## 2. Fork 本仓库 9 | 10 | ## 3. 创建Cloudflare API TOKEN 11 | 要创建一个具有 Workers 权限的 Cloudflare API Token,请按照以下步骤操作: 12 | 13 | 1. 登录 Cloudflare 帐户并导航到“我的资料”页面。 14 | 2. 在左侧菜单中选择“API Tokens”。 15 | 3. 点击“Create Token”按钮。 16 | 4. 在 API token templates 中选择 Edit Cloudflare Workers
image 17 | 5. 在“Zone Resources”下拉菜单中,选择要授权的区域。 18 | 6. 在“Account Resources”下拉菜单中,选择要授权的帐户。
image 19 | 7. 点击“Create Token”按钮。 20 | > 现在您已创建一个具有 Workers 权限的 Cloudflare API Token。请记住,API Token 的安全性非常重要,请不要在不必要的情况下共享它,并定期更改 API Token。 21 | 22 | 23 | ## 4. 设置 Action 的 Secrets 24 | image 25 | 26 | 1. 在 Github 仓库的 Settings -> Secrets 中添加以下 Secrets 27 | - CF_API_TOKEN: 你的Cloudflare API TOKEN 28 | - WRANGLER_TOML: 完整的 wrangler.toml 文件内容,可以参考[wrangler-example.toml](../../wrangler-example.toml) 29 | - CF_WORKERS_DOMAIN(可选): 你的Cloudflare Workers 路由(Workers 路由里你的*.workers.dev值,不带https://) 30 | 2. 在 Github 仓库的 Settings -> Actions 中,将 Actions 启用 31 | 32 | 33 | ## 4. 同步我的仓库 34 | 1. 为了安全起见,你fork的仓库不会同步更新我的仓库,所以你需要手动同步我的仓库 35 | 2. 当你手动同步我的仓库后,你的仓库会自动触发 Action,自动部署 36 | 3. 如果你想省略这一步你可以自己加一个自动同步我的仓库的Action 37 | 1. 在你的仓库中创建一个文件,文件名为:`.github/workflows/sync.yml` 38 | 2. 将下面的内容复制到文件中 39 | 3. 下面代码未经测试,仅提供参考 40 | ```yml 41 | name: Sync 42 | on: 43 | schedule: 44 | - cron: '0 0 * * *' 45 | jobs: 46 | sync: 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Sync 50 | uses: repo-sync/github-sync@v2 51 | with: 52 | source_repo: 'https://github.com/TBXark/ChatGPT-Telegram-Workers' 53 | target_repo: 'https://github.com/YOUR_NAME/ChatGPT-Telegram-Workers' 54 | github_token: ${{ secrets.GITHUB_TOKEN }} 55 | source_branch: 'master' 56 | ``` 57 | -------------------------------------------------------------------------------- /packages/apps/interpolate/assets/main.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 3 | margin: 0; 4 | padding: 20px; 5 | background-color: #f5f7fa; 6 | height: 100vh; 7 | box-sizing: border-box; 8 | color: #333; 9 | } 10 | 11 | .container { 12 | display: grid; 13 | grid-template-areas: 14 | "template data" 15 | "preview preview"; 16 | grid-template-columns: 1fr 1fr; 17 | grid-template-rows: 1fr 1fr; 18 | gap: 20px; 19 | height: 100%; 20 | padding-bottom: 60px; 21 | } 22 | 23 | .input-area { 24 | background-color: white; 25 | border-radius: 8px; 26 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 27 | padding: 20px; 28 | display: flex; 29 | flex-direction: column; 30 | } 31 | 32 | #template-area { 33 | grid-area: template; 34 | } 35 | 36 | #data-area { 37 | grid-area: data; 38 | } 39 | 40 | .preview-area { 41 | grid-area: preview; 42 | background-color: white; 43 | border-radius: 8px; 44 | box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1); 45 | padding: 20px; 46 | overflow: auto; 47 | } 48 | 49 | textarea { 50 | flex-grow: 1; 51 | border: 1px solid #e0e0e0; 52 | border-radius: 4px; 53 | padding: 10px; 54 | resize: none; 55 | font-family: 'Courier New', Courier, monospace; 56 | font-size: 14px; 57 | line-height: 1.5; 58 | transition: border-color 0.3s ease; 59 | } 60 | 61 | textarea:focus { 62 | outline: none; 63 | border-color: #4a90e2; 64 | box-shadow: 0 0 0 2px rgba(74, 144, 226, 0.2); 65 | } 66 | 67 | h2 { 68 | margin-top: 0; 69 | margin-bottom: 15px; 70 | color: #2c3e50; 71 | font-size: 18px; 72 | font-weight: 600; 73 | } 74 | 75 | #preview { 76 | background-color: #f9f9f9; 77 | border-radius: 4px; 78 | padding: 15px; 79 | min-height: 100px; 80 | } 81 | 82 | .footer { 83 | text-align: center; 84 | padding: 20px; 85 | background-color: #2c3e50; 86 | color: #ecf0f1; 87 | font-size: 14px; 88 | position: fixed; 89 | bottom: 0; 90 | left: 0; 91 | right: 0; 92 | } 93 | 94 | .footer a { 95 | color: #3498db; 96 | text-decoration: none; 97 | } 98 | 99 | .footer a:hover { 100 | text-decoration: underline; 101 | } -------------------------------------------------------------------------------- /packages/lib/core/src/utils/image/index.ts: -------------------------------------------------------------------------------- 1 | import { Cache } from '../cache'; 2 | 3 | const IMAGE_CACHE = new Cache(); 4 | 5 | async function fetchImage(url: string): Promise { 6 | const cache = IMAGE_CACHE.get(url); 7 | if (cache) { 8 | return cache; 9 | } 10 | return fetch(url) 11 | .then(resp => resp.blob()) 12 | .then((blob) => { 13 | IMAGE_CACHE.set(url, blob); 14 | return blob; 15 | }); 16 | } 17 | 18 | async function urlToBase64String(url: string): Promise { 19 | if (typeof Buffer !== 'undefined') { 20 | return fetchImage(url) 21 | .then(blob => blob.arrayBuffer()) 22 | .then(buffer => Buffer.from(buffer).toString('base64')); 23 | } else { 24 | // 非原生base64编码速度太慢不适合在workers中使用 25 | // 在wrangler.toml中添加 Node.js 选项启用nodejs兼容 26 | // compatibility_flags = [ "nodejs_compat_v2" ] 27 | return fetchImage(url) 28 | .then(blob => blob.arrayBuffer()) 29 | .then(buffer => btoa(String.fromCharCode.apply(null, new Uint8Array(buffer) as unknown as number[]))); 30 | } 31 | } 32 | 33 | function getImageFormatFromBase64(base64String: string): string { 34 | const firstChar = base64String.charAt(0); 35 | switch (firstChar) { 36 | case '/': 37 | return 'jpeg'; 38 | case 'i': 39 | return 'png'; 40 | case 'U': 41 | return 'webp'; 42 | default: 43 | throw new Error('Unsupported image format'); 44 | } 45 | } 46 | 47 | interface Base64DataWithFormat { 48 | data: string; 49 | format: string; 50 | } 51 | 52 | export async function imageToBase64String(url: string): Promise { 53 | const base64String = await urlToBase64String(url); 54 | const format = getImageFormatFromBase64(base64String); 55 | return { 56 | data: base64String, 57 | format: `image/${format}`, 58 | }; 59 | } 60 | 61 | export function renderBase64DataURI(params: Base64DataWithFormat): string { 62 | return `data:${params.format};base64,${params.data}`; 63 | } 64 | 65 | export function extraBase64DataFromBase64URI(dataURI: string): Base64DataWithFormat { 66 | const [format, data] = dataURI.split(';base64,'); 67 | return { 68 | format: format.replace('data:', ''), 69 | data, 70 | }; 71 | } 72 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/types.ts: -------------------------------------------------------------------------------- 1 | import type { AgentUserConfig } from '#/config'; 2 | import type { 3 | AdapterMessage, 4 | CoreAssistantMessage, 5 | CoreSystemMessage, 6 | CoreUserMessage, 7 | DataContent, 8 | FilePart, 9 | ImagePart, 10 | TextPart, 11 | } from './message'; 12 | // 当使用 `ai` 包时,取消注释以下行并注释掉上一行 13 | // import type { CoreAssistantMessage, CoreSystemMessage, CoreToolMessage, CoreUserMessage, DataContent } from 'ai'; 14 | 15 | export type DataItemContent = DataContent; 16 | export type UserContentPart = TextPart | ImagePart | FilePart; 17 | 18 | export type SystemMessageItem = CoreSystemMessage; 19 | export type UserMessageItem = CoreUserMessage; 20 | export type AssistantMessageItem = CoreAssistantMessage; 21 | export type ToolMessageItem = AdapterMessage<'tool', any>; 22 | 23 | export type ResponseMessage = AssistantMessageItem | ToolMessageItem; 24 | export type HistoryItem = SystemMessageItem | UserMessageItem | AssistantMessageItem | ToolMessageItem; 25 | 26 | export interface HistoryModifierResult { 27 | history: HistoryItem[]; 28 | message: CoreUserMessage; 29 | } 30 | 31 | export interface LLMChatParams { 32 | prompt?: string; 33 | messages: HistoryItem[]; 34 | } 35 | 36 | export interface ChatAgentResponse { 37 | text: string; 38 | responses: ResponseMessage[]; 39 | } 40 | 41 | export type ChatStreamTextHandler = (text: string) => Promise; 42 | export type HistoryModifier = (history: HistoryItem[], message: UserMessageItem | null) => HistoryModifierResult; 43 | 44 | export type AgentEnable = (context: AgentUserConfig) => boolean; 45 | export type AgentModel = (ctx: AgentUserConfig) => string | null; 46 | export type AgentModelList = (ctx: AgentUserConfig) => Promise; 47 | export type ChatAgentRequest = (params: LLMChatParams, context: AgentUserConfig, onStream: ChatStreamTextHandler | null) => Promise; 48 | export type ImageAgentRequest = (prompt: string, context: AgentUserConfig) => Promise; 49 | 50 | export interface Agent { 51 | name: string; 52 | enable: AgentEnable; 53 | modelKey: string; 54 | model: AgentModel; 55 | modelList: AgentModelList; 56 | request: AgentRequest; 57 | } 58 | 59 | export interface ChatAgent extends Agent {} 60 | 61 | export interface ImageAgent extends Agent {} 62 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 |

3 | ChatGPT-Telegram-Workers 4 |

5 | 6 |

7 |
English | 中文 8 |

9 |

10 | Deploy your own Telegram ChatGPT bot on Cloudflare Workers with ease. 11 |

12 | 13 | 14 | ## About 15 | 16 | The simplest and fastest way to deploy your own ChatGPT Telegram bot. Use Cloudflare Workers, single file, copy and paste directly, no dependencies required, no need to configure local development environment, no domain name required, serverless. 17 | 18 | You can customize the system initialization information so that your debugged personality never disappears. 19 | 20 |
21 | example 22 | image 23 |
24 | 25 | 26 | ## Features 27 | 28 | - Serverless deployment 29 | - Multi-platform deployment support (Cloudflare Workers, Vercel, Docker[...](doc/en/PLATFORM.md)) 30 | - Adaptation to multiple AI service providers (OpenAI, Azure OpenAI, Cloudflare AI, Cohere, Anthropic, Mistral, DeepSeek, Gemini, Groq[...](doc/en/CONFIG.md)) 31 | - Switching Models with InlineKeyboards 32 | - Custom commands (can achieve quick switching of models, switching of robot presets) 33 | - Support for multiple Telegram bots 34 | - Streaming output 35 | - Multi-language support 36 | - Text-to-image generation 37 | - [Plugin System](doc/en/PLUGINS.md), customizable plugins. 38 | 39 | 40 | ## Documentation 41 | 42 | - [Deploy Cloudflare Workers](./doc/en/DEPLOY.md) 43 | - [Local (or Docker) deployment](./doc/en/LOCAL.md) 44 | - [Deploy other platforms](./doc/en/PLATFORM.md) 45 | - [Configuration and Commands](./doc/en/CONFIG.md) 46 | - [Automatic update](./doc/en/ACTION.md) 47 | - [Change Log](./doc/en/CHANGELOG.md) 48 | 49 | 50 | ## Related Projects 51 | 52 | - [cloudflare-worker-adapter](https://github.com/TBXark/cloudflare-worker-adapter) A simple Cloudflare Worker adapter that allows this project to run independently of Cloudflare Worker. 53 | - [telegram-bot-api-types](https://github.com/TBXark/telegram-bot-api-types) Telegram Bot API SDK with 0 output after compilation, complete documentation, supports all APIs. 54 | 55 | 56 | ## Contributors 57 | 58 | This project exists thanks to all the people who contribute. [Contribute](https://github.com/tbxark/ChatGPT-Telegram-Workers/graphs/contributors). 59 | 60 | 61 | ## License 62 | 63 | **ChatGPT-Telegram-Workers** is released under the MIT license. [See LICENSE](LICENSE) for details. 64 | -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/callback_query/index.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerContext } from '#/config'; 2 | import type * as Telegram from 'telegram-bot-api-types'; 3 | import { loadChatRoleWithContext } from '../command/auth'; 4 | import { MessageSender } from '../sender'; 5 | import { AgentListCallbackQueryHandler, ModelChangeCallbackQueryHandler, ModelListCallbackQueryHandler } from './system'; 6 | 7 | const QUERY_HANDLERS = [ 8 | AgentListCallbackQueryHandler.Chat(), 9 | AgentListCallbackQueryHandler.Image(), 10 | ModelListCallbackQueryHandler.Chat(), 11 | ModelListCallbackQueryHandler.Image(), 12 | ModelChangeCallbackQueryHandler.Chat(), 13 | ModelChangeCallbackQueryHandler.Image(), 14 | ]; 15 | 16 | export async function handleCallbackQuery(callbackQuery: Telegram.CallbackQuery, context: WorkerContext): Promise { 17 | const sender = MessageSender.fromCallbackQuery(context.SHARE_CONTEXT.botToken, callbackQuery); 18 | const answerCallbackQuery = (msg: string): Promise => { 19 | return sender.api.answerCallbackQuery({ 20 | callback_query_id: callbackQuery.id, 21 | text: msg, 22 | }); 23 | }; 24 | try { 25 | if (!callbackQuery.message) { 26 | return null; 27 | } 28 | const chatId = callbackQuery.message.chat.id; 29 | const speakerId = callbackQuery.from?.id || chatId; 30 | const chatType = callbackQuery.message.chat.type; 31 | for (const handler of QUERY_HANDLERS) { 32 | // 如果存在权限条件 33 | if (handler.needAuth) { 34 | const roleList = handler.needAuth(chatType); 35 | if (roleList) { 36 | // 获取身份并判断 37 | const chatRole = await loadChatRoleWithContext(chatId, speakerId, context); 38 | if (chatRole === null) { 39 | return answerCallbackQuery('ERROR: Get chat role failed'); 40 | } 41 | if (!roleList.includes(chatRole)) { 42 | return answerCallbackQuery(`ERROR: Permission denied, need ${roleList.join(' or ')}`); 43 | } 44 | } 45 | } 46 | if (callbackQuery.data) { 47 | if (callbackQuery.data.startsWith(handler.prefix)) { 48 | return handler.handle(callbackQuery, callbackQuery.data, context); 49 | } 50 | } 51 | } 52 | } catch (e) { 53 | console.error('handleCallbackQuery', e); 54 | return answerCallbackQuery(`ERROR: ${(e as Error).message}`); 55 | } 56 | return null; 57 | } 58 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/cohere.ts: -------------------------------------------------------------------------------- 1 | import type { AgentUserConfig } from '#/config'; 2 | import type { SseChatCompatibleOptions } from './request'; 3 | import type { 4 | AgentEnable, 5 | AgentModel, 6 | ChatAgent, 7 | ChatAgentRequest, 8 | ChatAgentResponse, 9 | ChatStreamTextHandler, 10 | LLMChatParams, 11 | } from './types'; 12 | import { renderOpenAIMessages } from '#/agent/openai_compatibility'; 13 | import { requestChatCompletions } from './request'; 14 | import { bearerHeader, convertStringToResponseMessages, getAgentUserConfigFieldName, loadModelsList } from './utils'; 15 | 16 | export class Cohere implements ChatAgent { 17 | readonly name = 'cohere'; 18 | readonly modelKey = getAgentUserConfigFieldName('COHERE_CHAT_MODEL'); 19 | 20 | readonly enable: AgentEnable = ctx => !!(ctx.COHERE_API_KEY); 21 | readonly model: AgentModel = ctx => ctx.COHERE_CHAT_MODEL; 22 | 23 | readonly request: ChatAgentRequest = async (params: LLMChatParams, context: AgentUserConfig, onStream: ChatStreamTextHandler | null): Promise => { 24 | const { prompt, messages } = params; 25 | const url = `${context.COHERE_API_BASE}/chat`; 26 | const header = bearerHeader(context.COHERE_API_KEY, onStream !== null); 27 | const body = { 28 | ...(context.COHERE_CHAT_EXTRA_PARAMS || {}), 29 | messages: await renderOpenAIMessages(prompt, messages, null), 30 | model: context.COHERE_CHAT_MODEL, 31 | stream: onStream != null, 32 | }; 33 | 34 | const options: SseChatCompatibleOptions = {}; 35 | options.contentExtractor = function (data: any) { 36 | return data?.delta?.message?.content?.text; 37 | }; 38 | options.fullContentExtractor = function (data: any) { 39 | return data?.messages?.at(0)?.content; 40 | }; 41 | options.errorExtractor = function (data: any) { 42 | return data?.message; 43 | }; 44 | return convertStringToResponseMessages(requestChatCompletions(url, header, body, onStream, options)); 45 | }; 46 | 47 | readonly modelList = async (context: AgentUserConfig): Promise => { 48 | if (context.COHERE_CHAT_MODELS_LIST === '') { 49 | const { protocol, host } = new URL(context.COHERE_API_BASE); 50 | context.COHERE_CHAT_MODELS_LIST = `${protocol}://${host}/v2/models`; 51 | } 52 | return loadModelsList(context.COHERE_CHAT_MODELS_LIST, async (url): Promise => { 53 | const data = await fetch(url, { 54 | headers: bearerHeader(context.COHERE_API_KEY), 55 | }).then(res => res.json()) as any; 56 | return data.models?.filter((model: any) => model.endpoints?.includes('chat')).map((model: any) => model.name) || []; 57 | }); 58 | }; 59 | } 60 | -------------------------------------------------------------------------------- /packages/lib/core/src/config/merger.ts: -------------------------------------------------------------------------------- 1 | import type { AgentUserConfig } from '#/config/config'; 2 | 3 | export class ConfigMerger { 4 | private static parseArray(raw: string): string[] { 5 | raw = raw.trim(); 6 | if (raw === '') { 7 | return []; 8 | } 9 | if (raw.startsWith('[') && raw.endsWith(']')) { 10 | try { 11 | return JSON.parse(raw); 12 | } catch (e) { 13 | console.error(e); 14 | } 15 | } 16 | return raw.split(','); 17 | } 18 | 19 | static trim(source: AgentUserConfig, lock: string[]): Record { 20 | const config: Record = { ...source }; 21 | const keysSet = new Set(source?.DEFINE_KEYS || []); 22 | for (const key of lock) { 23 | keysSet.delete(key); 24 | } 25 | keysSet.add('DEFINE_KEYS'); 26 | for (const key of Object.keys(config)) { 27 | if (!keysSet.has(key)) { 28 | delete config[key]; 29 | } 30 | } 31 | return config; 32 | }; 33 | 34 | static merge(target: Record, source: Record, exclude?: string[]) { 35 | const sourceKeys = new Set(Object.keys(source)); 36 | for (const key of Object.keys(target)) { 37 | // 不存在的key直接跳过 38 | if (!sourceKeys.has(key)) { 39 | continue; 40 | } 41 | if (exclude && exclude.includes(key)) { 42 | continue; 43 | } 44 | // 默认为字符串类型 45 | const t = (target[key] !== null && target[key] !== undefined) ? typeof target[key] : 'string'; 46 | // 不是字符串直接赋值 47 | if (typeof source[key] !== 'string') { 48 | target[key] = source[key]; 49 | continue; 50 | } 51 | switch (t) { 52 | case 'number': 53 | target[key] = Number.parseInt(source[key], 10); 54 | break; 55 | case 'boolean': 56 | target[key] = (source[key] || 'false') === 'true'; 57 | break; 58 | case 'string': 59 | target[key] = source[key]; 60 | break; 61 | case 'object': 62 | if (Array.isArray(target[key])) { 63 | target[key] = ConfigMerger.parseArray(source[key]); 64 | } else { 65 | try { 66 | target[key] = JSON.parse(source[key]); 67 | } catch (e) { 68 | console.error(e); 69 | } 70 | } 71 | break; 72 | default: 73 | target[key] = source[key]; 74 | break; 75 | } 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /packages/lib/plugins/src/interpolate.test.ts: -------------------------------------------------------------------------------- 1 | import { interpolate } from './interpolate'; 2 | 3 | describe('interpolate', () => { 4 | it('基本变量插值', () => { 5 | const template = 'Hello, {{name}}!'; 6 | const data = { name: 'Alice' }; 7 | expect(interpolate(template, data)).toBe('Hello, Alice!'); 8 | }); 9 | 10 | it('嵌套对象属性插值', () => { 11 | const template = '{{user.name}} is {{user.age}} years old.'; 12 | const data = { user: { name: 'Bob', age: 30 } }; 13 | expect(interpolate(template, data)).toBe('Bob is 30 years old.'); 14 | }); 15 | 16 | it('数组索引插值', () => { 17 | const template = 'The first item is {{items[0]}}.'; 18 | const data = { items: ['apple', 'banana', 'cherry'] }; 19 | expect(interpolate(template, data)).toBe('The first item is apple.'); 20 | }); 21 | 22 | it('条件语句', () => { 23 | const template = '{{#if isAdmin}}Admin{{#else}}User{{/if}}'; 24 | const data1 = { isAdmin: true }; 25 | const data2 = { isAdmin: false }; 26 | expect(interpolate(template, data1)).toBe('Admin'); 27 | expect(interpolate(template, data2)).toBe('User'); 28 | }); 29 | 30 | it('循环语句', () => { 31 | const template = '
    {{#each item in items}}
  • {{item}}
  • {{/each}}
'; 32 | const data = { items: ['a', 'b', 'c'] }; 33 | expect(interpolate(template, data)).toBe('
  • a
  • b
  • c
'); 34 | }); 35 | it('当前上下文插值', () => { 36 | const template = '{{#each item in items}}{{.}},{{/each}}'; 37 | const data = { items: [1, 2, 3] }; 38 | expect(interpolate(template, data)).toBe('1,2,3,'); 39 | }); 40 | it('不存在的变量处理', () => { 41 | const template = 'Hello, {{name}}!'; 42 | const data = {}; 43 | expect(interpolate(template, data)).toBe('Hello, {{name}}!'); 44 | }); 45 | it('复杂模板', () => { 46 | const template = ` 47 | {{title}} 48 | 49 | {{#each item in items}} 50 | {{#each:item i in item}} 51 | {{ i.value }} 52 | {{#if i.enable}} 53 | {{#if:sub i.subEnable}} 54 | sub enable 55 | {{#else:sub}} 56 | sub disable 57 | {{/if:sub}} 58 | {{#else}} 59 | disable 60 | {{/if}} 61 | {{/each:item}} 62 | {{/each}} 63 | 64 | `; 65 | const data = { 66 | title: 'hello', 67 | items: [ 68 | [ 69 | { value: 'a', enable: true, sub: { subEnable: true } }, 70 | { value: 'b', enable: false, sub: { subEnable: false } }, 71 | ], 72 | [ 73 | { value: 'c', enable: true, sub: { subEnable: false } }, 74 | { value: 'd', enable: false, sub: { subEnable: true } }, 75 | ], 76 | ], 77 | }; 78 | console.log(interpolate(template, data)); 79 | }); 80 | }); 81 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/utils.ts: -------------------------------------------------------------------------------- 1 | import type { AgentUserConfig } from '#/config'; 2 | import type { ChatAgentResponse, DataItemContent, HistoryItem } from './types'; 3 | 4 | export interface ImageRealContent { 5 | url?: string; 6 | base64?: string; 7 | } 8 | 9 | export function extractTextContent(history: HistoryItem): string { 10 | if (typeof history.content === 'string') { 11 | return history.content; 12 | } 13 | if (Array.isArray(history.content)) { 14 | return history.content.map((item) => { 15 | if (item.type === 'text') { 16 | return item.text; 17 | } 18 | return ''; 19 | }).join(''); 20 | } 21 | return ''; 22 | } 23 | 24 | export function extractImageContent(imageData: DataItemContent | URL): ImageRealContent { 25 | if (imageData instanceof URL) { 26 | return { url: imageData.href }; 27 | } 28 | // 2. 判断 DataContent 的具体类型 29 | // 检查是否为字符串(包括 base64) 30 | if (typeof imageData === 'string') { 31 | if (imageData.startsWith('http')) { 32 | return { url: imageData }; 33 | } else { 34 | return { base64: imageData }; 35 | } 36 | } 37 | if (typeof Buffer !== 'undefined') { 38 | if (imageData instanceof Uint8Array) { 39 | return { base64: Buffer.from(imageData).toString('base64') }; 40 | } 41 | if (Buffer.isBuffer(imageData)) { 42 | return { base64: Buffer.from(imageData).toString('base64') }; 43 | } 44 | } 45 | return {}; 46 | } 47 | 48 | export async function convertStringToResponseMessages(input: Promise | string): Promise { 49 | const text = typeof input === 'string' ? input : await input; 50 | return { 51 | text, 52 | responses: [{ role: 'assistant', content: text }], 53 | }; 54 | } 55 | 56 | export async function loadModelsList(raw: string, remoteLoader?: (url: string) => Promise): Promise { 57 | if (!raw) { 58 | return []; 59 | } 60 | if (raw.startsWith('[') && raw.endsWith(']')) { 61 | try { 62 | return JSON.parse(raw); 63 | } catch (e) { 64 | console.error(e); 65 | return []; 66 | } 67 | } 68 | if (raw.startsWith('http') && remoteLoader) { 69 | return await remoteLoader(raw); 70 | } 71 | return [raw]; 72 | } 73 | 74 | export function bearerHeader(token: string | null, stream?: boolean): Record { 75 | const res: Record = { 76 | 'Authorization': `Bearer ${token}`, 77 | 'Content-Type': 'application/json', 78 | }; 79 | if (stream !== undefined) { 80 | res.Accept = stream ? 'text/event-stream' : 'application/json'; 81 | } 82 | return res; 83 | } 84 | 85 | type WorkersConfigKeys = keyof AgentUserConfig; 86 | export function getAgentUserConfigFieldName(fieldName: T): T { 87 | return fieldName; 88 | } 89 | -------------------------------------------------------------------------------- /packages/lib/plugins/src/interpolate.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable regexp/no-potentially-useless-backreference */ 2 | const INTERPOLATE_LOOP_REGEXP = /\{\{#each(?::(\w+))?\s+(\w+)\s+in\s+([\w.[\]]+)\}\}([\s\S]*?)\{\{\/each(?::\1)?\}\}/g; 3 | const INTERPOLATE_CONDITION_REGEXP = /\{\{#if(?::(\w+))?\s+([\w.[\]]+)\}\}([\s\S]*?)(?:\{\{#else(?::\1)?\}\}([\s\S]*?))?\{\{\/if(?::\1)?\}\}/g; 4 | const INTERPOLATE_VARIABLE_REGEXP = /\{\{([\w.[\]]+)\}\}/g; 5 | 6 | function evaluateExpression(expr: string, localData: any): undefined | any { 7 | if (expr === '.') { 8 | return localData['.'] ?? localData; 9 | } 10 | try { 11 | return expr.split('.').reduce((value, key) => { 12 | if (key.includes('[') && key.includes(']')) { 13 | const [arrayKey, indexStr] = key.split('['); 14 | const indexExpr = indexStr.slice(0, -1); // 移除最后的 ']' 15 | let index = Number.parseInt(indexExpr, 10); 16 | if (Number.isNaN(index)) { 17 | index = evaluateExpression(indexExpr, localData); 18 | } 19 | return value?.[arrayKey]?.[index]; 20 | } 21 | return value?.[key]; 22 | }, localData); 23 | } catch (error) { 24 | console.error(`Error evaluating expression: ${expr}`, error); 25 | return undefined; 26 | } 27 | } 28 | 29 | export function interpolate(template: string, data: any, formatter?: (value: any) => string): string { 30 | const processConditional = (condition: string, trueBlock: string, falseBlock: string, localData: any): string => { 31 | const result = evaluateExpression(condition, localData); 32 | return result ? trueBlock : (falseBlock || ''); 33 | }; 34 | 35 | const processLoop = (itemName: string, arrayExpr: string, loopContent: string, localData: any): string => { 36 | const array = evaluateExpression(arrayExpr, localData); 37 | if (!Array.isArray(array)) { 38 | console.warn(`Expression "${arrayExpr}" did not evaluate to an array`); 39 | return ''; 40 | } 41 | return array.map((item) => { 42 | const itemData = { ...localData, [itemName]: item, '.': item }; 43 | return interpolate(loopContent, itemData); 44 | }).join(''); 45 | }; 46 | 47 | const processTemplate = (tmpl: string, localData: any) => { 48 | tmpl = tmpl.replace(INTERPOLATE_LOOP_REGEXP, (_, alias, itemName, arrayExpr, loopContent) => 49 | processLoop(itemName, arrayExpr, loopContent, localData)); 50 | 51 | tmpl = tmpl.replace(INTERPOLATE_CONDITION_REGEXP, (_, alias, condition, trueBlock, falseBlock) => 52 | processConditional(condition, trueBlock, falseBlock, localData)); 53 | 54 | return tmpl.replace(INTERPOLATE_VARIABLE_REGEXP, (_, expr) => { 55 | const value = evaluateExpression(expr, localData); 56 | if (value === undefined) { 57 | return `{{${expr}}}`; 58 | } 59 | if (formatter) { 60 | return formatter(value); 61 | } 62 | return String(value); 63 | }); 64 | }; 65 | 66 | return processTemplate(template, data); 67 | } 68 | -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/api/index.ts: -------------------------------------------------------------------------------- 1 | import type * as Telegram from 'telegram-bot-api-types'; 2 | import { ENV } from '#/config'; 3 | 4 | class APIClientBase { 5 | readonly token: string; 6 | readonly baseURL: string = ENV.TELEGRAM_API_DOMAIN; 7 | 8 | constructor(token: string, baseURL?: string) { 9 | this.token = token; 10 | if (baseURL) { 11 | this.baseURL = baseURL.replace(/\/+$/, ''); 12 | } 13 | this.request = this.request.bind(this); 14 | this.requestJSON = this.requestJSON.bind(this); 15 | } 16 | 17 | private uri(method: Telegram.BotMethod): string { 18 | return `${this.baseURL}/bot${this.token}/${method}`; 19 | } 20 | 21 | private jsonRequest(method: Telegram.BotMethod, params: T): Promise { 22 | return fetch(this.uri(method), { 23 | method: 'POST', 24 | headers: { 25 | 'Content-Type': 'application/json', 26 | }, 27 | body: JSON.stringify(params), 28 | }); 29 | } 30 | 31 | private formDataRequest(method: Telegram.BotMethod, params: T): Promise { 32 | const formData = new FormData(); 33 | for (const key in params) { 34 | const value = params[key]; 35 | if (value instanceof File) { 36 | formData.append(key, value, value.name); 37 | } else if (value instanceof Blob) { 38 | formData.append(key, value, 'blob'); 39 | } else if (typeof value === 'string') { 40 | formData.append(key, value); 41 | } else { 42 | formData.append(key, JSON.stringify(value)); 43 | } 44 | } 45 | return fetch(this.uri(method), { 46 | method: 'POST', 47 | body: formData, 48 | }); 49 | } 50 | 51 | request(method: Telegram.BotMethod, params: T): Promise { 52 | for (const key in params) { 53 | if (params[key] instanceof File || params[key] instanceof Blob) { 54 | return this.formDataRequest(method, params); 55 | } 56 | } 57 | return this.jsonRequest(method, params); 58 | } 59 | 60 | async requestJSON(method: Telegram.BotMethod, params: T): Promise { 61 | return this.request(method, params).then(res => res.json() as R); 62 | } 63 | } 64 | 65 | export type TelegramBotAPI = APIClientBase & Telegram.AllBotMethods; 66 | 67 | export function createTelegramBotAPI(token: string): TelegramBotAPI { 68 | const client = new APIClientBase(token); 69 | return new Proxy(client, { 70 | get(target, prop, receiver) { 71 | if (prop in target) { 72 | return Reflect.get(target, prop, receiver); 73 | } 74 | return (...args: any[]) => { 75 | if (typeof prop === 'string' && prop.endsWith('WithReturns')) { 76 | const method = prop.slice(0, -11) as Telegram.BotMethod; 77 | return Reflect.apply(target.requestJSON, target, [method, ...args]); 78 | } 79 | return Reflect.apply(target.request, target, [prop as Telegram.BotMethod, ...args]); 80 | }; 81 | }, 82 | }) as TelegramBotAPI; 83 | } 84 | -------------------------------------------------------------------------------- /doc/en/ACTION.md: -------------------------------------------------------------------------------- 1 | # Github Action Auto-update 2 | 3 | > PS: Cloudflare Workers now supports automatic builds, for details see [Documentation](https://developers.cloudflare.com/workers/ci-cd/builds/) 4 | 5 | ## 1. Complete one deployment manually 6 | Refer to [Deployment Process](DEPLOY.md) for specific deployment steps. 7 | 8 | ## 2. Fork this repository 9 | 10 | 11 | ## 3. Create Cloudflare API TOKEN 12 | To create a Cloudflare API Token with Workers permissions, follow these steps: 13 | 14 | 1. Log in to your Cloudflare account and navigate to the "My Profile" page. 15 | 2. Select "API Tokens" from the left-hand menu. 16 | 3. Click the "Create Token" button. 17 | 4. Choose "Edit Cloudflare Workers" from the API token templates.
image 18 | 5. In the "Zone Resources" dropdown menu, select the zone you want to authorize. 19 | 6. In the "Account Resources" dropdown menu, select the account you want to authorize.
image 20 | 7. Click the "Create Token" button. 21 | > You have now created a Cloudflare API Token with Workers permissions. Remember, API Token security is very important. Do not share it unnecessarily and change your API Token regularly. 22 | 23 | 24 | ## 4. Set up Secrets for Actions 25 | image 26 | 27 | 1. Add the following Secrets to the Github repository's Settings -> Secrets 28 | - CF_API_TOKEN: Your Cloudflare API TOKEN 29 | - WRANGLER_TOML: The full content of the wrangler.toml file. Refer to [wrangler-example.toml](../../wrangler-example.toml) for an example. 30 | - CF_WORKERS_DOMAIN (optional): Your Cloudflare Workers route (the value of your *.workers.dev in the Workers route, without https://) 31 | 2. Enable Actions in the Github repository's Settings -> Actions 32 | 33 | 34 | ## 4. Synchronize my repository 35 | 1. For security reasons, the repository you forked will not automatically sync with my repository. Therefore, you need to manually sync with my repository. 36 | 2. When you manually sync with my repository, your repository will automatically trigger the Action and deploy automatically. 37 | 3. If you want to skip this step, you can add an Action to automatically sync with my repository. 38 | 1. Create a file in your repository named: `.github/workflows/sync.yml` 39 | 2. Copy the following content to the file. 40 | 3. The code below has not been tested and is for reference only. 41 | ```yml 42 | name: Sync 43 | on: 44 | schedule: 45 | - cron: '0 0 * * *' 46 | jobs: 47 | sync: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - name: Sync 51 | uses: repo-sync/github-sync@v2 52 | with: 53 | source_repo: 'https://github.com/TBXark/ChatGPT-Telegram-Workers' 54 | target_repo: 'Fill in your repository address' 55 | github_token: ${{ secrets.GITHUB_TOKEN }} 56 | source_branch: 'master' 57 | ``` 58 | -------------------------------------------------------------------------------- /packages/apps/vercel/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { VercelRequest, VercelResponse } from '@vercel/node'; 2 | import * as process from 'node:process'; 3 | import { CHAT_AGENTS, createRouter, ENV } from '@chatgpt-telegram-workers/core'; 4 | import { injectNextChatAgent } from '@chatgpt-telegram-workers/next'; 5 | import { UpStashRedis } from 'cloudflare-worker-adapter'; 6 | import convert from 'telegramify-markdown'; 7 | 8 | export default async function (request: VercelRequest, response: VercelResponse) { 9 | try { 10 | const { 11 | UPSTASH_REDIS_REST_URL = '', 12 | UPSTASH_REDIS_REST_TOKEN = '', 13 | VERCEL_PROJECT_PRODUCTION_URL = '', 14 | } = process.env; 15 | for (const [KEY, VALUE] of Object.entries({ UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN })) { 16 | if (!VALUE) { 17 | response.status(500).json({ 18 | error: `${KEY} is required`, 19 | message: 'Set environment variables and redeploy', 20 | }); 21 | return; 22 | } 23 | } 24 | const cache = UpStashRedis.create(UPSTASH_REDIS_REST_URL, UPSTASH_REDIS_REST_TOKEN); 25 | ENV.DEFAULT_PARSE_MODE = 'MarkdownV2'; 26 | ENV.merge({ 27 | ...process.env, 28 | DATABASE: cache, 29 | }); 30 | ENV.CUSTOM_MESSAGE_RENDER = (parse_mode, message) => { 31 | if (parse_mode === 'MarkdownV2') { 32 | return convert(message, 'remove'); 33 | } 34 | return message; 35 | }; 36 | injectNextChatAgent(CHAT_AGENTS); // remove this line if you don't use vercel ai sdk 37 | const router = createRouter(); 38 | let body: any | null = null; 39 | if (request.body) { 40 | body = JSON.stringify(request.body); 41 | } 42 | if (request.url === '/vercel/debug') { 43 | response.status(200).json({ 44 | message: 'OK', 45 | base: VERCEL_PROJECT_PRODUCTION_URL, 46 | }); 47 | return; 48 | } 49 | const url = `https://${VERCEL_PROJECT_PRODUCTION_URL}${request.url}`; 50 | console.log(`Forwarding request to ${url}`); 51 | const newReq = new Request(url, { 52 | method: request.method, 53 | headers: Object.entries(request.headers).reduce((acc, [key, value]) => { 54 | if (value === undefined) { 55 | return acc; 56 | } 57 | if (Array.isArray(value)) { 58 | for (const v of value) { 59 | acc.append(key, v); 60 | } 61 | return acc; 62 | } 63 | acc.set(key, value); 64 | return acc; 65 | }, new Headers()), 66 | body, 67 | }); 68 | const res = await router.fetch(newReq); 69 | for (const [key, value] of res.headers.entries()) { 70 | response.setHeader(key, value); 71 | } 72 | response.status(res.status).send(await res.text()); 73 | } catch (e) { 74 | response.status(500).json({ 75 | message: (e as Error).message, 76 | stack: (e as Error).stack, 77 | }); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /doc/en/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | - v1.10.7 4 | - Fixed a typo in "Groq" 5 | - Added support for independent Xai Agents 6 | 7 | - v1.10.6 8 | - Add `DeepSeek` and `Groq` independent Agent support 9 | 10 | - v1.10.5 11 | - Add full support for `Telegram MarkdownV2` 12 | 13 | - v1.10.0 14 | - Switching Models with InlineKeyboards 15 | 16 | - v1.9.0 17 | - Add plugin system 18 | 19 | - v1.8.0 20 | - Support Cohere, Anthropic Ai 21 | - Support image input. 22 | - Adapt to group topic mode 23 | - Remove the role function and use custom commands instead. 24 | - Fix the bug of failure to send super long text. 25 | 26 | - v1.7.0 27 | - Modify the worker AI invocation method to API invocation, requiring the setting of account_id and token. The original AI binding method is invalid. 28 | - Add support for worker AI text conversation flow mode. 29 | - Add functionality for worker AI to generate images from text. 30 | - Add switch AI providers 31 | - Add custom commands, which allows for quick model switching 32 | - Add lock user-defined configurations 33 | 34 | - v1.6.0 35 | - Add workers AI support, please refer to the configuration document for specific settings. 36 | - Optimize the parser for openai stream mode. 37 | 38 | - v1.5.0 39 | - perf: Adjust command order 40 | - perf: Send loading message before sending request to OpenAI 41 | - feat: Add support for streaming output. Enabled by default. Use `STREAM_MODE=false` to disable. 42 | - feat: Add compatibility for multiple keys, randomly select a key to use. 43 | - feat: Add shortcut buttons `/new`, `/redo`. 44 | 45 | - v1.4.0 46 | - Support deployment on multiple platforms 47 | - Added `/redo` command to resend or modify the previous question 48 | - Added multi-language support. Use the `LANGUAGE` environment variable to set the language. Currently supports `zh-CN`, `zh-TW`, and `en`. The default language is `zh-CN`. 49 | 50 | - v1.3.1 51 | - Optimized history trimming logic 52 | - Optimized token calculation logic 53 | - Fixed a bug in edit messages. 54 | 55 | - v1.3.0 56 | - Added command `/usage` to show token usage statistics. 57 | - Added command `/system` to show system information. 58 | - Added option to show command menu only in specific scopes. 59 | - Added environment variable `SYSTEM_INIT_MESSAGE`. 60 | - Added environment variable `CHAT_MODEL`. 61 | - Added automatic deployment script using `Github Action`. 62 | - Improved `/init` page to display more error information. 63 | - Fixed bug with historical record clipping. 64 | 65 | - v1.2.0 66 | - Fixed critical vulnerability, update is mandatory. 67 | 68 | - v1.1.0 69 | - Changed from single file to multiple files for easier maintenance, provided "dist" directory for easier copying and pasting. 70 | - Removed and added some configurations, provided compatibility code for easier upgrading. 71 | - Modified KV key generation logic, which may cause data loss, manual modification of keys or reconfiguration is required. 72 | - Fixed some bugs. 73 | - Automatically bind all commands. 74 | - BREAKING CHANGE: Major changes, the group ID must be added to the whitelist "CHAT_GROUP_WHITE_LIST" in order to use it. Otherwise, anyone can add your bot to the group and consume your quota. 75 | 76 | - v1.0.0 77 | - Initial version. 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.toptal.com/developers/gitignore/api/node 2 | # Edit at https://www.toptal.com/developers/gitignore?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | .pnpm-debug.log* 13 | 14 | # Diagnostic reports (https://nodejs.org/api/report.html) 15 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 16 | 17 | # Runtime data 18 | pids 19 | *.pid 20 | *.seed 21 | *.pid.lock 22 | 23 | # Directory for instrumented libs generated by jscoverage/JSCover 24 | lib-cov 25 | 26 | # Coverage directory used by tools like istanbul 27 | coverage 28 | *.lcov 29 | 30 | # nyc test coverage 31 | .nyc_output 32 | 33 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 34 | .grunt 35 | 36 | # Bower dependency directory (https://bower.io/) 37 | bower_components 38 | 39 | # node-waf configuration 40 | .lock-wscript 41 | 42 | # Compiled binary addons (https://nodejs.org/api/addons.html) 43 | dist/Release 44 | 45 | # Dependency directories 46 | node_modules/ 47 | jspm_packages/ 48 | 49 | # Snowpack dependency directory (https://snowpack.dev/) 50 | web_modules/ 51 | 52 | # TypeScript cache 53 | *.tsbuildinfo 54 | 55 | # Optional npm cache directory 56 | .npm 57 | 58 | # Optional eslint cache 59 | .eslintcache 60 | 61 | # Optional stylelint cache 62 | .stylelintcache 63 | 64 | # Microbundle cache 65 | .rpt2_cache/ 66 | .rts2_cache_cjs/ 67 | .rts2_cache_es/ 68 | .rts2_cache_umd/ 69 | 70 | # Optional REPL history 71 | .node_repl_history 72 | 73 | # Output of 'npm pack' 74 | *.tgz 75 | 76 | # Yarn Integrity file 77 | .yarn-integrity 78 | 79 | # dotenv environment variable files 80 | .env 81 | .env.development.local 82 | .env.test.local 83 | .env.production.local 84 | .env.local 85 | 86 | # parcel-bundler cache (https://parceljs.org/) 87 | .cache 88 | .parcel-cache 89 | 90 | # Next.js dist output 91 | .next 92 | out 93 | 94 | # Nuxt.js dist / generate output 95 | .nuxt 96 | #dist 97 | 98 | # Gatsby files 99 | .cache/ 100 | # Comment in the public line in if your project uses Gatsby and not Next.js 101 | # https://nextjs.org/blog/next-9-1#public-directory-support 102 | # public 103 | 104 | # vuepress dist output 105 | .vuepress/dist 106 | 107 | # vuepress v2.x temp and cache directory 108 | .temp 109 | 110 | # Docusaurus cache and generated files 111 | .docusaurus 112 | 113 | # Serverless directories 114 | .serverless/ 115 | 116 | # FuseBox cache 117 | .fusebox/ 118 | 119 | # DynamoDB Local files 120 | .dynamodb/ 121 | 122 | # TernJS port file 123 | .tern-port 124 | 125 | # Stores VSCode versions used for testing VSCode extensions 126 | .vscode-test 127 | 128 | # yarn v2 129 | .yarn/cache 130 | .yarn/unplugged 131 | .yarn/build-state.yml 132 | .yarn/install-state.gz 133 | .pnp.* 134 | 135 | ### Node Patch ### 136 | # Serverless Webpack directories 137 | .webpack/ 138 | 139 | # Optional stylelint cache 140 | 141 | # SvelteKit dist / generate output 142 | .svelte-kit 143 | 144 | # End of https://www.toptal.com/developers/gitignore/api/node 145 | 146 | .idea 147 | .vscode 148 | .vercel 149 | .wrangler 150 | config.json 151 | wrangler.toml 152 | /dist/index.cjs 153 | /dist/index.d.ts 154 | /dist/src 155 | /packages/**/dist 156 | .env*.local 157 | -------------------------------------------------------------------------------- /doc/cn/DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Workers 部署流程 2 | 3 | > 如果你需要本地部署或者docker部署,请查看[本地部署](LOCAL.md)文档 4 | > 5 | > 如果你需要部署到Vercel,请查看[Vercel部署示例](VERCEL.md)文档 6 | 7 | ## 视频教程 8 | 9 | image 10 | 11 | 感谢 [**科技小白堂**](https://www.youtube.com/@lipeng0820) 提供此视频教程 12 | 13 | 14 | ## 手动部署 15 | 16 | ### 1. 新建Telegram机器人, 获得Token 17 | image 18 | 19 | 1. 打开Telegram并向 BotFather 发送 `/start` 命令 20 | 2. 发送 `/newbot` 命令,并给你的机器人起一个名字 21 | 3. 给你的机器人取一个唯一的用户名以`_bot`结尾 22 | 4. BotFather 会生成一个 Token,复制下来保存好,这个 Token 是和你的机器人绑定的密钥,不要泄露给他人! 23 | 5. 稍后再Cloudflare Workers 的设置里 将这个 Token 填入 `TELEGRAM_AVAILABLE_TOKENS` 变量中 24 | 6. 如果你需要支持群聊或者设置其他Telegram Bot API,请查看[配置文档](CONFIG.md)设置对应变量 25 | 26 | ### 2. 注册OpenAI账号并创建API Key 27 | image 28 | 29 | 1. 打开 [OpenAI](https://platform.openai.com) 注册账号 30 | 2. 点击右上角的头像,进入个人设置页面 31 | 3. 点击 API Keys,创建一个新的 API Key 32 | 4. 稍后再Cloudflare Workers 的设置里 将这个 Token 填入 `OPENAI_API_KEY` 变量中 33 | 5. 如果你使用第三方AI服务,请查看[配置文档](CONFIG.md)设置对应变量 34 | 35 | ### 3. 部署Workers 36 | image 37 | 38 | 1. 打开 [Cloudflare Workers](https://dash.cloudflare.com/?to=/:account/workers) 注册账号 39 | 2. 点击右上角的 `Create a Service` 40 | 3. 进入新建的workers, 选择`Quick Edit`, 将[`../dist/index.js`](../../dist/index.js)代码复制到编辑器中,保存 41 | 42 | 43 | ### 4. 配置环境变量 44 | image 45 | 46 | 1. 打开 [Cloudflare Workers](https://dash.cloudflare.com/?to=/:account/workers) 点击你的Workers,点击右上角的 Setting -> Variables 47 | 2. 查看[配置文档](CONFIG.md)设置必须填写的环境变量 48 | 49 | ### 5. 绑定KV数据 50 | 1. 在`首页-Workers-KV`, 点击右上角的 `Create a Namespace`, 名字随便取, 但是绑定的时候必须设定为`DATABASE`
image 51 | 2. 打开 [Cloudflare Workers](https://dash.cloudflare.com/?to=/:account/workers) 点击你的Workers 52 | 3. 点击右上角的 Setting -> Variables
image 53 | 4. 在 `KV Namespace Bindings` 中点击 `Edit variables` 54 | 5. 点击 `Add variable` 55 | 6. 设置名字为`DATABASE` 并选择刚刚创建的KV数据 56 | 57 | ### 6. 初始化 58 | 1. 运行 `https://workers_name.username.workers.dev/init` 自动绑定telegram的webhook和设定所有指令 59 | 60 | 61 | ### 七. 开始聊天 62 | image 63 | 64 | 1. 开始新对话,使用`/new`指令开始,之后每次都会将聊天上下文发送到ChatGPT 65 | 2. 如果想了解其他指令的使用办法,请查看[配置文档](CONFIG.md) 66 | 67 | 68 | ## 命令行部署 69 | 1. 准备部署所需的 Telegram Bot Token 和 OpenAI API Key 70 | 2. `mv wrangler-example.toml wrangler.toml`, 然后修改相应配置 71 | 3. `pnpm install` 72 | 4. `pnpm run deploy:dist` 73 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/openai.ts: -------------------------------------------------------------------------------- 1 | import type { AgentUserConfig } from '#/config'; 2 | import type { 3 | AgentEnable, 4 | AgentModel, 5 | AgentModelList, 6 | ChatAgent, 7 | ChatAgentRequest, 8 | ChatAgentResponse, 9 | ChatStreamTextHandler, 10 | ImageAgent, 11 | ImageAgentRequest, 12 | LLMChatParams, 13 | } from './types'; 14 | import { ImageSupportFormat, loadOpenAIModelList, renderOpenAIMessages } from '#/agent/openai_compatibility'; 15 | import { requestChatCompletions } from './request'; 16 | import { bearerHeader, convertStringToResponseMessages, getAgentUserConfigFieldName, loadModelsList } from './utils'; 17 | 18 | function openAIApiKey(context: AgentUserConfig): string { 19 | const length = context.OPENAI_API_KEY.length; 20 | return context.OPENAI_API_KEY[Math.floor(Math.random() * length)]; 21 | } 22 | 23 | export class OpenAI implements ChatAgent { 24 | readonly name = 'openai'; 25 | readonly modelKey = getAgentUserConfigFieldName('OPENAI_CHAT_MODEL'); 26 | 27 | readonly enable: AgentEnable = ctx => ctx.OPENAI_API_KEY.length > 0; 28 | readonly model: AgentModel = ctx => ctx.OPENAI_CHAT_MODEL; 29 | readonly modelList: AgentModelList = ctx => loadOpenAIModelList(ctx.OPENAI_CHAT_MODELS_LIST, ctx.OPENAI_API_BASE, bearerHeader(openAIApiKey(ctx))); 30 | 31 | readonly request: ChatAgentRequest = async (params: LLMChatParams, context: AgentUserConfig, onStream: ChatStreamTextHandler | null): Promise => { 32 | const { prompt, messages } = params; 33 | const url = `${context.OPENAI_API_BASE}/chat/completions`; 34 | const header = bearerHeader(openAIApiKey(context)); 35 | const body = { 36 | ...(context.OPENAI_API_EXTRA_PARAMS || {}), 37 | model: context.OPENAI_CHAT_MODEL, 38 | messages: await renderOpenAIMessages(prompt, messages, [ImageSupportFormat.URL, ImageSupportFormat.BASE64]), 39 | stream: onStream != null, 40 | }; 41 | return convertStringToResponseMessages(requestChatCompletions(url, header, body, onStream, null)); 42 | }; 43 | } 44 | 45 | export class Dalle implements ImageAgent { 46 | readonly name = 'openai'; 47 | readonly modelKey = getAgentUserConfigFieldName('DALL_E_MODEL'); 48 | 49 | readonly enable: AgentEnable = ctx => ctx.OPENAI_API_KEY.length > 0; 50 | readonly model: AgentModel = ctx => ctx.DALL_E_MODEL; 51 | readonly modelList: AgentModelList = ctx => loadModelsList(ctx.DALL_E_MODELS_LIST); 52 | 53 | readonly request: ImageAgentRequest = async (prompt: string, context: AgentUserConfig): Promise => { 54 | const url = `${context.OPENAI_API_BASE}/images/generations`; 55 | const header = bearerHeader(openAIApiKey(context)); 56 | const body: any = { 57 | prompt, 58 | n: 1, 59 | size: context.DALL_E_IMAGE_SIZE, 60 | model: context.DALL_E_MODEL, 61 | }; 62 | if (body.model === 'dall-e-3') { 63 | body.quality = context.DALL_E_IMAGE_QUALITY; 64 | body.style = context.DALL_E_IMAGE_STYLE; 65 | } 66 | const resp = await fetch(url, { 67 | method: 'POST', 68 | headers: header, 69 | body: JSON.stringify(body), 70 | }).then(res => res.json()) as any; 71 | 72 | if (resp.error?.message) { 73 | throw new Error(resp.error.message); 74 | } 75 | return resp?.data?.at(0)?.url; 76 | }; 77 | } 78 | -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/handler/group.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerContext } from '#/config'; 2 | import type * as Telegram from 'telegram-bot-api-types'; 3 | import type { MessageHandler } from './types'; 4 | import { createTelegramBotAPI } from '../api'; 5 | import { isGroupChat } from '../auth'; 6 | 7 | function checkMention(content: string, entities: Telegram.MessageEntity[], botName: string, botId: number): { 8 | isMention: boolean; 9 | content: string; 10 | } { 11 | let isMention = false; 12 | for (const entity of entities) { 13 | const entityStr = content.slice(entity.offset, entity.offset + entity.length); 14 | switch (entity.type) { 15 | case 'mention': // "mention"适用于有用户名的普通用户 16 | if (entityStr === `@${botName}`) { 17 | isMention = true; 18 | content = content.slice(0, entity.offset) + content.slice(entity.offset + entity.length); 19 | } 20 | break; 21 | case 'text_mention': // "text_mention"适用于没有用户名的用户或需要通过ID提及用户的情况 22 | if (`${entity.user?.id}` === `${botId}`) { 23 | isMention = true; 24 | content = content.slice(0, entity.offset) + content.slice(entity.offset + entity.length); 25 | } 26 | break; 27 | case 'bot_command': // "bot_command"适用于命令 28 | if (entityStr.endsWith(`@${botName}`)) { 29 | isMention = true; 30 | const newEntityStr = entityStr.replace(`@${botName}`, ''); 31 | content = content.slice(0, entity.offset) + newEntityStr + content.slice(entity.offset + entity.length); 32 | } 33 | break; 34 | default: 35 | break; 36 | } 37 | } 38 | return { 39 | isMention, 40 | content, 41 | }; 42 | } 43 | 44 | export class GroupMention implements MessageHandler { 45 | handle = async (message: Telegram.Message, context: WorkerContext): Promise => { 46 | // 非群组消息不作判断,交给下一个中间件处理 47 | if (!isGroupChat(message.chat.type)) { 48 | return null; 49 | } 50 | 51 | // 处理回复消息, 如果回复的是当前机器人的消息交给下一个中间件处理 52 | const replyMe = `${message.reply_to_message?.from?.id}` === `${context.SHARE_CONTEXT.botId}`; 53 | if (replyMe) { 54 | return null; 55 | } 56 | 57 | // 处理群组消息,过滤掉AT部分 58 | let botName = context.SHARE_CONTEXT.botName; 59 | if (!botName) { 60 | const res = await createTelegramBotAPI(context.SHARE_CONTEXT.botToken).getMeWithReturns(); 61 | botName = res.result.username || null; 62 | context.SHARE_CONTEXT.botName = botName; 63 | } 64 | if (!botName) { 65 | throw new Error('Not set bot name'); 66 | } 67 | let isMention = false; 68 | // 检查text中是否有机器人的提及 69 | if (message.text && message.entities) { 70 | const res = checkMention(message.text, message.entities, botName, context.SHARE_CONTEXT.botId); 71 | isMention = res.isMention; 72 | message.text = res.content.trim(); 73 | } 74 | // 检查caption中是否有机器人的提及 75 | if (message.caption && message.caption_entities) { 76 | const res = checkMention(message.caption, message.caption_entities, botName, context.SHARE_CONTEXT.botId); 77 | isMention = res.isMention || isMention; 78 | message.caption = res.content.trim(); 79 | } 80 | if (!isMention) { 81 | throw new Error('Not mention'); 82 | } 83 | 84 | return null; 85 | }; 86 | } 87 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/chat.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerContext } from '#/config'; 2 | import type { ChatAgent, HistoryItem, HistoryModifier, LLMChatParams, UserMessageItem } from './types'; 3 | import { ENV } from '#/config'; 4 | import { extractTextContent } from './utils'; 5 | 6 | function tokensCounter(): (text: string) => number { 7 | return (text) => { 8 | return text.length; 9 | }; 10 | } 11 | 12 | async function loadHistory(key: string): Promise { 13 | // 加载历史记录 14 | let history = []; 15 | try { 16 | history = JSON.parse(await ENV.DATABASE.get(key)); 17 | } catch (e) { 18 | console.error(e); 19 | } 20 | if (!history || !Array.isArray(history)) { 21 | history = []; 22 | } 23 | 24 | const counter = tokensCounter(); 25 | 26 | const trimHistory = (list: HistoryItem[], initLength: number, maxLength: number, maxToken: number) => { 27 | // 历史记录超出长度需要裁剪, 小于0不裁剪 28 | if (maxLength >= 0 && list.length > maxLength) { 29 | list = list.splice(list.length - maxLength); 30 | } 31 | // 处理token长度问题, 小于0不裁剪 32 | if (maxToken > 0) { 33 | let tokenLength = initLength; 34 | for (let i = list.length - 1; i >= 0; i--) { 35 | const historyItem = list[i]; 36 | let length = 0; 37 | if (historyItem.content) { 38 | length = counter(extractTextContent(historyItem)); 39 | } else { 40 | historyItem.content = ''; 41 | } 42 | // 如果最大长度超过maxToken,裁剪history 43 | tokenLength += length; 44 | if (tokenLength > maxToken) { 45 | list = list.splice(i + 1); 46 | break; 47 | } 48 | } 49 | } 50 | return list; 51 | }; 52 | 53 | // 裁剪 54 | if (ENV.AUTO_TRIM_HISTORY && ENV.MAX_HISTORY_LENGTH > 0) { 55 | history = trimHistory(history, 0, ENV.MAX_HISTORY_LENGTH, ENV.MAX_TOKEN_LENGTH); 56 | } 57 | 58 | return history; 59 | } 60 | 61 | export type StreamResultHandler = (text: string) => Promise; 62 | 63 | export async function requestCompletionsFromLLM(params: UserMessageItem | null, context: WorkerContext, agent: ChatAgent, modifier: HistoryModifier | null, onStream: StreamResultHandler | null): Promise { 64 | const historyDisable = ENV.AUTO_TRIM_HISTORY && ENV.MAX_HISTORY_LENGTH <= 0; 65 | const historyKey = context.SHARE_CONTEXT.chatHistoryKey; 66 | if (!historyKey) { 67 | throw new Error('History key not found'); 68 | } 69 | let history = await loadHistory(historyKey); 70 | if (modifier) { 71 | const modifierData = modifier(history, params || null); 72 | history = modifierData.history; 73 | params = modifierData.message; 74 | } 75 | if (!params) { 76 | throw new Error('Message is empty'); 77 | } 78 | const llmParams: LLMChatParams = { 79 | prompt: context.USER_CONFIG.SYSTEM_INIT_MESSAGE || undefined, 80 | messages: [...history, params], 81 | }; 82 | const { text, responses } = await agent.request(llmParams, context.USER_CONFIG, onStream); 83 | if (!historyDisable) { 84 | const editParams = { ...params }; 85 | if (ENV.HISTORY_IMAGE_PLACEHOLDER) { 86 | if (Array.isArray(editParams.content)) { 87 | const imageCount = editParams.content.filter(i => i.type === 'image').length; 88 | const textContent = editParams.content.findLast(i => i.type === 'text'); 89 | if (textContent) { 90 | editParams.content = editParams.content.filter(i => i.type !== 'image'); 91 | textContent.text = textContent.text + ` ${ENV.HISTORY_IMAGE_PLACEHOLDER}`.repeat(imageCount); 92 | } 93 | } 94 | } 95 | await ENV.DATABASE.put(historyKey, JSON.stringify([...history, editParams, ...responses])).catch(console.error); 96 | } 97 | return text; 98 | } 99 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/azure.ts: -------------------------------------------------------------------------------- 1 | import type { AgentUserConfig } from '#/config'; 2 | import type { 3 | AgentEnable, 4 | AgentModel, 5 | AgentModelList, 6 | ChatAgent, 7 | ChatAgentRequest, 8 | ChatAgentResponse, 9 | ChatStreamTextHandler, 10 | ImageAgent, 11 | ImageAgentRequest, 12 | LLMChatParams, 13 | } from './types'; 14 | import { ImageSupportFormat, renderOpenAIMessages } from '#/agent/openai_compatibility'; 15 | import { requestChatCompletions } from './request'; 16 | import { convertStringToResponseMessages, getAgentUserConfigFieldName, loadModelsList } from './utils'; 17 | 18 | function azureHeader(context: AgentUserConfig): Record { 19 | return { 20 | 'Content-Type': 'application/json', 21 | 'api-key': context.AZURE_API_KEY || '', 22 | }; 23 | } 24 | 25 | export class AzureChatAI implements ChatAgent { 26 | readonly name = 'azure'; 27 | readonly modelKey = getAgentUserConfigFieldName('AZURE_CHAT_MODEL'); 28 | 29 | readonly enable: AgentEnable = ctx => !!(ctx.AZURE_API_KEY && ctx.AZURE_RESOURCE_NAME); 30 | readonly model: AgentModel = ctx => ctx.AZURE_CHAT_MODEL; 31 | 32 | readonly request: ChatAgentRequest = async (params: LLMChatParams, context: AgentUserConfig, onStream: ChatStreamTextHandler | null): Promise => { 33 | const { prompt, messages } = params; 34 | const url = `https://${context.AZURE_RESOURCE_NAME}.openai.azure.com/openai/deployments/${context.AZURE_CHAT_MODEL}/chat/completions?api-version=${context.AZURE_API_VERSION}`; 35 | const header = azureHeader(context); 36 | const body = { 37 | ...(context.AZURE_CHAT_EXTRA_PARAMS || {}), 38 | messages: await renderOpenAIMessages(prompt, messages, [ImageSupportFormat.URL, ImageSupportFormat.BASE64]), 39 | stream: onStream != null, 40 | }; 41 | return convertStringToResponseMessages(requestChatCompletions(url, header, body, onStream, null)); 42 | }; 43 | 44 | readonly modelList = async (context: AgentUserConfig): Promise => { 45 | if (context.AZURE_CHAT_MODELS_LIST === '') { 46 | context.AZURE_CHAT_MODELS_LIST = `https://${context.AZURE_RESOURCE_NAME}.openai.azure.com/openai/models?api-version=${context.AZURE_API_VERSION}`; 47 | } 48 | return loadModelsList(context.AZURE_CHAT_MODELS_LIST, async (url): Promise => { 49 | const data = await fetch(url, { 50 | headers: azureHeader(context), 51 | }).then(res => res.json()) as any; 52 | return data.data?.map((model: any) => model.id) || []; 53 | }); 54 | }; 55 | } 56 | 57 | export class AzureImageAI implements ImageAgent { 58 | readonly name = 'azure'; 59 | readonly modelKey = getAgentUserConfigFieldName('AZURE_IMAGE_MODEL'); 60 | 61 | readonly enable: AgentEnable = ctx => !!(ctx.AZURE_API_KEY && ctx.AZURE_RESOURCE_NAME); 62 | readonly model: AgentModel = ctx => ctx.AZURE_IMAGE_MODEL; 63 | readonly modelList: AgentModelList = ctx => Promise.resolve([ctx.AZURE_IMAGE_MODEL]); 64 | 65 | readonly request: ImageAgentRequest = async (prompt: string, context: AgentUserConfig): Promise => { 66 | const url = `https://${context.AZURE_RESOURCE_NAME}.openai.azure.com/openai/deployments/${context.AZURE_IMAGE_MODEL}/images/generations?api-version=${context.AZURE_API_VERSION}`; 67 | const header = azureHeader(context); 68 | const body = { 69 | prompt, 70 | n: 1, 71 | size: context.DALL_E_IMAGE_SIZE, 72 | style: context.DALL_E_IMAGE_STYLE, 73 | quality: context.DALL_E_IMAGE_QUALITY, 74 | }; 75 | const validSize = ['1792x1024', '1024x1024', '1024x1792']; 76 | if (!validSize.includes(body.size)) { 77 | body.size = '1024x1024'; 78 | } 79 | const resp = await fetch(url, { 80 | method: 'POST', 81 | headers: header, 82 | body: JSON.stringify(body), 83 | }).then(res => res.json()) as any; 84 | 85 | if (resp.error?.message) { 86 | throw new Error(resp.error.message); 87 | } 88 | return resp?.data?.at(0)?.url; 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-telegram-workers", 3 | "type": "module", 4 | "version": "1.10.7", 5 | "description": "The easiest and quickest way to deploy your own ChatGPT Telegram bot is to use a single file and simply copy and paste it. There is no need for any dependencies, local development environment configuration, domain names, or servers.", 6 | "author": "tbxark ", 7 | "license": "MIT", 8 | "repository": "git@github.com:TBXark/ChatGPT-Telegram-Workers.git", 9 | "exports": { 10 | ".": { 11 | "types": "./packages/lib/core/dist/index.d.ts", 12 | "import": "./packages/lib/core/dist/index.js", 13 | "require": "./packages/lib/core/dist/index.cjs" 14 | } 15 | }, 16 | "main": "./packages/lib/core/dist/index.cjs", 17 | "module": "./packages/lib/core/dist/index.js", 18 | "types": "./packages/lib/core/dist/index.d.ts", 19 | "files": [ 20 | "./packages/lib/core/dist/index.cjs", 21 | "./packages/lib/core/dist/index.d.ts", 22 | "./packages/lib/core/dist/index.js" 23 | ], 24 | "scripts": { 25 | "lint": "eslint --fix *.js *.ts packages plugins scripts", 26 | "version": "tsx scripts/gen-version.ts", 27 | "build": "pnpm -r run build", 28 | "test": "pnpm -r run test", 29 | "build:plugins": "pnpm run --filter @chatgpt-telegram-workers/plugins... build", 30 | "build:core": "pnpm run --filter @chatgpt-telegram-workers/core... build", 31 | "build:next": "pnpm run --filter @chatgpt-telegram-workers/next... build", 32 | "build:local": "pnpm run --filter @chatgpt-telegram-workers/local... build", 33 | "build:vercel": "pnpm run --filter @chatgpt-telegram-workers/vercel... build", 34 | "build:workers": "pnpm run --filter @chatgpt-telegram-workers/workers... build", 35 | "build:workersmk2": "pnpm run --filter @chatgpt-telegram-workers/workers-mk2... build", 36 | "build:workersnext": "pnpm run --filter @chatgpt-telegram-workers/workers-next... build", 37 | "build:interpolate": "pnpm run --filter @chatgpt-telegram-workers/interpolate... build", 38 | "build:docker": "docker build -t chatgpt-telegram-workers:latest .", 39 | "build:dockerx": "docker buildx build --platform linux/amd64,linux/arm64 -t chatgpt-telegram-workers:latest .", 40 | "build:dist": "pnpm run version && pnpm run build && cp -r packages/apps/workers/dist/index.js dist/ && cp -r packages/apps/workers-mk2/dist/index.js dist/index-mk2.js && cp -r packages/apps/workers-next/dist/index.js dist/index-next.js", 41 | "deploy:dist": "pnpm run build:dist && wrangler deploy", 42 | "deploy:default": "pnpm run build && wrangler deploy", 43 | "deploy:workers": "pnpm run build:workers && TOML_PATH=$INIT_CWD/wrangler.toml pnpm run --filter @chatgpt-telegram-workers/workers deploy", 44 | "deploy:workersmk2": "pnpm run build:workers && TOML_PATH=$INIT_CWD/wrangler.toml pnpm run --filter @chatgpt-telegram-workers/workers-mk2 deploy", 45 | "deploy:workersnext": "pnpm run build:workersnext && TOML_PATH=$INIT_CWD/wrangler.toml pnpm run --filter @chatgpt-telegram-workers/workers-next deploy", 46 | "deploy:vercel": "pnpm run build:vercel && vercel --prod", 47 | "start:local": "pnpm run build:local && CONFIG_PATH=$INIT_CWD/config.json TOML_PATH=$INIT_CWD/wrangler.toml pnpm run --filter @chatgpt-telegram-workers/local start", 48 | "vercel:syncenv": "tsx scripts/vercel-sync-env.ts && vercel --prod", 49 | "clean": "pnpm -r run clean", 50 | "wrangler": "wrangler" 51 | }, 52 | "devDependencies": { 53 | "@antfu/eslint-config": "^5.4.1", 54 | "@rollup/plugin-node-resolve": "^16.0.3", 55 | "@types/node": "^24.9.1", 56 | "eslint": "^9.38.0", 57 | "eslint-plugin-format": "^1.0.2", 58 | "rollup-plugin-cleanup": "^3.2.1", 59 | "rollup-plugin-node-externals": "^8.1.1", 60 | "stylelint": "^16.25.0", 61 | "telegram-bot-api-types": "^9.2.0", 62 | "toml": "^3.0.0", 63 | "tsx": "^4.20.6", 64 | "typescript": "^5.9.3", 65 | "vercel": "^48.4.1", 66 | "vite": "^7.1.11", 67 | "vite-plugin-checker": "^0.11.0", 68 | "vite-plugin-dts": "^4.5.4", 69 | "wrangler": "^4.43.0" 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /doc/cn/PLUGINS.md: -------------------------------------------------------------------------------- 1 | # 插件系统 2 | 3 | > 插件系统还在开发中,功能可能会有变动但是文档会尽量保持更新 4 | 5 | 6 | ## 插件系统是什么? 7 | 8 | 插件系统是一种允许用户自定义功能的系统。用户可以通过插件系统添加新的功能,插件的定义是一个json文件,用户通过绑定对应的命令来调用插件。 9 | 10 | 11 | ## 插件的结构 12 | 13 | ```typescript 14 | 15 | /** 16 | * TemplateInputType: 输入数据的类型,将Telegram输入的数据转换为对应的数据类型 17 | * json: JSON格式 18 | * space-separated: 以空格分隔的字符串 19 | * comma-separated: 以逗号分隔的字符串 20 | * text: 文本,不分割(默认值) 21 | */ 22 | export type TemplateInputType = 'json' | 'space-separated' | 'comma-separated' | 'text'; 23 | 24 | /** 25 | * TemplateBodyType: 请求体的类型 26 | * json: JSON格式, 此时对于content字段的值应该为一个对象,其中的key为固定值,Value支持插值 27 | * form: 表单格式, 此时对于content字段的值应该为一个对象,其中的key为固定值,Value支持插值 28 | * text: 文本格式, 此时对于content字段的值应该为一个字符串,支持插值 29 | */ 30 | export type TemplateBodyType = 'json' | 'form' | 'text'; 31 | 32 | /** 33 | * TemplateResponseType: 响应体的类型 34 | * json: JSON格式, 此时会将响应体解析为JSON格式交给下一个模板渲染 35 | * text: 文本格式, 此时会将响应体解析为文本格式交给下一个模板渲染 36 | * blob: 二进制格式, 此时会将响应体直接返回 37 | */ 38 | export type TemplateResponseType = 'json' | 'text' | 'blob'; 39 | 40 | /** 41 | * TemplateOutputType: 输出数据的类型 42 | * text: 文本格式, 将渲染结果作为纯文本发送到telegram 43 | * image: 图片格式, 将渲染结果作为图片url发送到telegram 44 | * html: HTML格式, 将渲染结果作为HTML格式发送到telegram 45 | * markdown: Markdown格式, 将渲染结果作为Markdown格式发送到telegram 46 | */ 47 | export type TemplateOutputType = 'text' | 'image' | 'html' | 'markdown'; 48 | 49 | export interface RequestTemplate { 50 | url: string; // 必选, 支持插值 51 | method: string; // 必选, 固定值 52 | headers: { [key: string]: string }; // 可选, Key为固定值,Value支持插值 53 | input: { 54 | type: TemplateInputType; 55 | required: boolean; // 必选, 是否必须输入 56 | }; 57 | query: { [key: string]: string }; // 可选, Key为固定值,Value支持插值 58 | body: { 59 | type: TemplateBodyType; 60 | content: { [key: string]: string } | string; // content为对象时Key为固定值,Value支持插值。content为字符串时支持插值 61 | }; 62 | response: { 63 | content: { // 必选, 当请求成功时的处理 64 | input_type: TemplateResponseType; 65 | output_type: TemplateOutputType; 66 | output: string; 67 | }; 68 | error: { // 必选, 当请求失败时的处理 69 | input_type: TemplateResponseType; 70 | output_type: TemplateOutputType; 71 | output: string; 72 | }; 73 | }; 74 | } 75 | ``` 76 | 77 | ## 插件的使用 78 | 79 | 例如在环境变量中定义如下变量 80 | 81 | ```toml 82 | PLUGIN_COMMAND_dns = "https://raw.githubusercontent.com/TBXark/ChatGPT-Telegram-Workers/dev/plugins/dns.json" 83 | PLUGIN_DESCRIPTION_dns = "DNS查询 /dns <类型> <域名>" 84 | ``` 85 | 86 | 然后在命令行中输入`/dns A www.baidu.com`即可调用插件 87 | 88 | 其中`PLUGIN_COMMAND_dns`是插件的json文件地址,`PLUGIN_DESCRIPTION_dns`是插件的描述。 89 | `PLUGIN_COMMAND_dns`可以是完整的json也可以是一个json的url。 90 | 91 | 如果你想将插件命令绑定到telegram的菜单中,你可以添加如下环境变量`PLUGIN_SCOPE_dns = "all_private_chats,all_group_chats,all_chat_administrators"`,这样插件就会在所有的私聊,群聊和群组中生效。 92 | 93 | 94 | ## 插值模板 95 | 96 | 您可以在[插值模板测试页面](https://interpolate-test.pages.dev)中测试插值模板。 97 | 98 | ```html 99 | {{title}} 100 | 101 | {{#each item in items}} 102 | {{#each:item i in item}} 103 | {{i.value}} 104 | {{#if i.enable}} 105 | {{#if:sub i.subEnable}} 106 | sub enable 107 | {{#else:sub}} 108 | sub disable 109 | {{/if:sub}} 110 | {{#else}} 111 | disable 112 | {{/if}} 113 | {{/each:item}} 114 | {{/each}} 115 | 116 | ``` 117 | 118 | 1. `{{title}}` 代表插值模板的变量,支持键路径和数组下标 119 | 2. `{{#each item in items}}` 代表遍历数组items, item是数组的每一个元素,必须包含结尾 `{{/each}}` 120 | 3. `{{#each:item i in item}}` 嵌套遍历操作需要给遍历添加别名,结尾也需要对应的别名 `{{/each:item}}` 121 | 4. `{{#if i.enable}}` 代表条件判断,判断条件不支持表达式,只能判断非,非空,非0,必须包含结尾 `{{/if}}` 122 | 5. `{{#else}}` 代表条件判断的否定分支 123 | 6. `{{#if:sub i.subEnable}}` 嵌套代表条件判断需要给条件添加别名,结尾也需要对应的别名 `{{/if:sub}}` 124 | 7. 所有`{{}}`中的插值或者表达式不能有空格,否则会被解析为字符串,比如这个就是一个错误的插值 `{{ title }}` 125 | 8. `{{.}}` 代表当前的数据, 可以在`#each`中使用或者全局使用 126 | 127 | 128 | ## 插值的变量 129 | 130 | 默认传入插值的数据结构如下 131 | 132 | ```json 133 | { 134 | "DATA": [], 135 | "ENV": {} 136 | } 137 | ``` 138 | 139 | 1. 其中`DATA`为用户输入的数据,根据`TemplateInputType`的不同,DATA的数据结构也不同 140 | 2. `ENV`为环境变量,用户可以通过环境变量传入数据,插件的环境变量与全局的环境变量隔离,需要不同的语法传入 141 | 142 | 143 | ## 插件环境变量 144 | 145 | 你可以在插件环境变量中保存请求所需的token,插件环境变量必须以`PLUGIN_ENV_`开头,例如 146 | 147 | ```toml 148 | PLUGIN_ENV_access_token = "xxxx" 149 | ``` 150 | 151 | 就会被解析成 152 | 153 | ```json 154 | { 155 | "DATA": [], 156 | "ENV": { 157 | "access_token": "xxxx" 158 | } 159 | } 160 | ``` 161 | 162 | ## 插件示例 163 | 164 | 1. [DNS查询插件示例](../../plugins/dns.json) 165 | 2. [字典查询插件示例](../../plugins/dicten.json) 166 | -------------------------------------------------------------------------------- /doc/en/DEPLOY.md: -------------------------------------------------------------------------------- 1 | # Cloudflare Workers Deployment Guide 2 | 3 | > If you need local deployment or Docker deployment, please refer to the [Local Deployment](LOCAL.md) documentation. 4 | > 5 | > If you need to deploy to Vercel, please check the [Vercel deployment example](VERCEL.md) documentation. 6 | 7 | 8 | ## Video tutorial 9 | 10 | image 11 | 12 | Thank [**科技小白堂**](https://www.youtube.com/@lipeng0820) for providing this video tutorial. 13 | 14 | 15 | ## Manual deployment 16 | 17 | 18 | ### 1. Create a new Telegram bot, obtain the Token. 19 | image 20 | 21 | 1. Open Telegram and send the `/start` command to BotFather. 22 | 2. Send the `/newbot` command and give your bot a name. 23 | 3. Give your bot a unique username ending with `_bot`. 24 | 4. BotFather will generate a Token; copy it and save it. This Token is the key linked to your bot, do not disclose it to others! 25 | 5. Later, in the Cloudflare Workers settings, fill this Token into the `TELEGRAM_AVAILABLE_TOKENS` variable. 26 | 6. If you need to support group chats or set up other Telegram Bot APIs, please refer to the [configuration documentation](CONFIG.md) to set the corresponding variables. 27 | 28 | 29 | ### 2. Register an OpenAI account and create an API Key. 30 | image 31 | 32 | 1. Open [OpenAI](https://platform.openai.com) to register an account. 33 | 2. Click on the avatar in the upper right corner to enter the personal settings page. 34 | 3. Click on API Keys to create a new API Key. 35 | 4. Later, in the Cloudflare Workers settings, fill this Token into the `OPENAI_API_KEY` variable. 36 | 5. If you are using third-party AI services, please refer to the [configuration document](CONFIG.md) to set the corresponding variables. 37 | 38 | 39 | ### 3. Deploy Workers 40 | image 41 | 42 | 1. Open [Cloudflare Workers](https://dash.cloudflare.com/?to=/:account/workers) and register an account. 43 | 2. Click on `Create a Service` in the upper right corner. 44 | 3. Enter the newly created workers, select `Quick Edit`, copy the code from [`../dist/index.js`](../../dist/index.js) into the editor, and save. 45 | 46 | 47 | ### 4. Configure environment variables 48 | image 49 | 50 | 1. Open [Cloudflare Workers](https://dash.cloudflare.com/?to=/:account/workers), click on your Workers, then click on the top right corner's Setting -> Variables. 51 | 2. Check the [configuration document](CONFIG.md) for the required environment variables that must be filled in. 52 | 53 | ### 5. Bind KVNamespace 54 | 55 | 1. In `Home-Workers-KV`, click on the `Create a Namespace` in the upper right corner, name it whatever you like, but it must be set to `DATABASE` when binding.
image 56 | 2. Open [Cloudflare Workers](https://dash.cloudflare.com/?to=/:account/workers) and click on your Workers. 57 | 3. Click in the upper right corner Setting -> Variables
image 58 | 4. In `KV Namespace Bindings`, click `Edit variables`. 59 | 5. Click `Add variable`. 60 | 6. Set the name to `DATABASE` and select the KV data you just created. 61 | 62 | 63 | ### 6. Initialization 64 | 1. Run `https://workers_name.username.workers.dev/init` to automatically bind the Telegram webhook and set all commands. 65 | 66 | 67 | ### 七. Start chatting 68 | image 69 | 70 | 1. Start a new conversation by using the `/new` command, and thereafter, the chat context will be sent to ChatGPT each time. 71 | 2. If you want to learn how to use other commands, please refer to the [configuration document](CONFIG.md). 72 | 73 | 74 | ## Command Line Deployment 75 | 76 | 1. Prepare the required Telegram Bot Token and OpenAI API Key 77 | 2. `mv wrangler-example.toml wrangler.toml`, then modify the corresponding configuration 78 | 3. `pnpm install` 79 | 4. `pnpm run deploy:dist` 80 | -------------------------------------------------------------------------------- /packages/lib/core/src/utils/router/index.ts: -------------------------------------------------------------------------------- 1 | import { errorToString } from '#/utils/resp'; 2 | 3 | export type QueryParams = Record; 4 | export type RouterRequest = Request & { 5 | query?: QueryParams; 6 | params?: Record; 7 | route?: string; 8 | }; 9 | 10 | type RouterHandler = (req: RouterRequest, ...args: any) => Promise | Response | null; 11 | 12 | export class Router { 13 | private readonly routes: [string, RegExp, RouterHandler[], string][]; 14 | private readonly base: string; 15 | errorHandler: (req: RouterRequest, error: Error) => Promise | Response = async (req, error) => new Response(errorToString(error), { status: 500 }); 16 | 17 | constructor({ base = '', routes = [], ...other } = {}) { 18 | this.routes = routes; 19 | this.base = base; 20 | Object.assign(this, other); 21 | this.fetch = this.fetch.bind(this); 22 | this.route = this.route.bind(this); 23 | this.get = this.get.bind(this); 24 | this.post = this.post.bind(this); 25 | this.put = this.put.bind(this); 26 | this.delete = this.delete.bind(this); 27 | this.patch = this.patch.bind(this); 28 | this.head = this.head.bind(this); 29 | this.options = this.options.bind(this); 30 | this.all = this.all.bind(this); 31 | } 32 | 33 | private parseQueryParams(searchParams: URLSearchParams): QueryParams { 34 | const query: QueryParams = {}; 35 | searchParams.forEach((v, k) => { 36 | query[k] = k in query ? [...(Array.isArray(query[k]) ? query[k] : [query[k]]), v] : v; 37 | }); 38 | return query; 39 | } 40 | 41 | private normalizePath(path: string): string { 42 | return path.replace(/\/+(\/|$)/g, '$1'); 43 | } 44 | 45 | private createRouteRegex(path: string): RegExp { 46 | return new RegExp(`^${path 47 | .replace(/\\/g, '\\\\') // escape backslashes 48 | .replace(/(\/?\.?):(\w+)\+/g, '($1(?<$2>*))') // greedy params 49 | .replace(/(\/?\.?):(\w+)/g, '($1(?<$2>[^$1/]+?))') // named params and image format 50 | .replace(/\./g, '\\.') // dot in path 51 | .replace(/(\/?)\*/g, '($1.*)?') // wildcard 52 | }/*$`); 53 | } 54 | 55 | async fetch(request: RouterRequest, ...args: any): Promise { 56 | try { 57 | const url = new URL(request.url); 58 | const reqMethod = request.method.toUpperCase(); 59 | request.query = this.parseQueryParams(url.searchParams); 60 | for (const [method, regex, handlers, path] of this.routes) { 61 | let match = null; 62 | // eslint-disable-next-line no-cond-assign 63 | if ((method === reqMethod || method === 'ALL') && (match = url.pathname.match(regex))) { 64 | request.params = match?.groups || {}; 65 | request.route = path; 66 | for (const handler of handlers) { 67 | const response = await handler(request, ...args); 68 | if (response != null) { 69 | return response; 70 | } 71 | } 72 | } 73 | } 74 | return new Response('Not Found', { status: 404 }); 75 | } catch (e) { 76 | return this.errorHandler(request, e as Error); 77 | } 78 | } 79 | 80 | route(method: string, path: string, ...handlers: RouterHandler[]): Router { 81 | const route = this.normalizePath(this.base + path); 82 | const regex = this.createRouteRegex(route); 83 | this.routes.push([method.toUpperCase(), regex, handlers, route]); 84 | return this; 85 | } 86 | 87 | get(path: string, ...handlers: RouterHandler[]): Router { 88 | return this.route('GET', path, ...handlers); 89 | } 90 | 91 | post(path: string, ...handlers: RouterHandler[]): Router { 92 | return this.route('POST', path, ...handlers); 93 | } 94 | 95 | put(path: string, ...handlers: RouterHandler[]): Router { 96 | return this.route('PUT', path, ...handlers); 97 | } 98 | 99 | delete(path: string, ...handlers: RouterHandler[]): Router { 100 | return this.route('DELETE', path, ...handlers); 101 | } 102 | 103 | patch(path: string, ...handlers: RouterHandler[]): Router { 104 | return this.route('PATCH', path, ...handlers); 105 | } 106 | 107 | head(path: string, ...handlers: RouterHandler[]): Router { 108 | return this.route('HEAD', path, ...handlers); 109 | } 110 | 111 | options(path: string, ...handlers: RouterHandler[]): Router { 112 | return this.route('OPTIONS', path, ...handlers); 113 | } 114 | 115 | all(path: string, ...handlers: RouterHandler[]): Router { 116 | return this.route('ALL', path, ...handlers); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/request.ts: -------------------------------------------------------------------------------- 1 | import type { ChatStreamTextHandler } from './types'; 2 | import { ENV } from '#/config'; 3 | import { Stream } from './stream'; 4 | 5 | export interface SseChatCompatibleOptions { 6 | streamBuilder?: (resp: Response, controller: AbortController) => Stream; 7 | contentExtractor?: (data: object) => string | null; 8 | fullContentExtractor?: (data: object) => string | null; 9 | errorExtractor?: (data: object) => string | null; 10 | } 11 | 12 | function fixOpenAICompatibleOptions(options: SseChatCompatibleOptions | null): SseChatCompatibleOptions { 13 | options = options || {}; 14 | options.streamBuilder = options.streamBuilder || function (r, c) { 15 | return new Stream(r, c); 16 | }; 17 | options.contentExtractor = options.contentExtractor || function (d: any) { 18 | return d?.choices?.at(0)?.delta?.content; 19 | }; 20 | options.fullContentExtractor = options.fullContentExtractor || function (d: any) { 21 | return d.choices?.at(0)?.message.content; 22 | }; 23 | options.errorExtractor = options.errorExtractor || function (d: any) { 24 | return d.error?.message; 25 | }; 26 | return options; 27 | } 28 | 29 | export function isJsonResponse(resp: Response): boolean { 30 | const contentType = resp.headers.get('content-type'); 31 | return contentType?.toLowerCase().includes('application/json') ?? false; 32 | } 33 | 34 | export function isEventStreamResponse(resp: Response): boolean { 35 | const types = ['application/stream+json', 'text/event-stream']; 36 | const content = resp.headers.get('content-type')?.toLowerCase() || ''; 37 | for (const type of types) { 38 | if (content.includes(type)) { 39 | return true; 40 | } 41 | } 42 | return false; 43 | } 44 | 45 | export async function streamHandler(stream: AsyncIterable, contentExtractor: (data: T) => string | null, onStream?: (text: string) => Promise): Promise { 46 | let contentFull = ''; 47 | let lengthDelta = 0; 48 | let updateStep = 50; 49 | let lastUpdateTime = Date.now(); 50 | try { 51 | for await (const part of stream) { 52 | const textPart = contentExtractor(part); 53 | if (!textPart) { 54 | continue; 55 | } 56 | lengthDelta += textPart.length; 57 | contentFull = contentFull + textPart; 58 | if (lengthDelta > updateStep) { 59 | if (ENV.TELEGRAM_MIN_STREAM_INTERVAL > 0) { 60 | const delta = Date.now() - lastUpdateTime; 61 | if (delta < ENV.TELEGRAM_MIN_STREAM_INTERVAL) { 62 | continue; 63 | } 64 | lastUpdateTime = Date.now(); 65 | } 66 | lengthDelta = 0; 67 | updateStep += 20; 68 | await onStream?.(`${contentFull}\n...`); 69 | } 70 | } 71 | } catch (e) { 72 | contentFull += `\nError: ${(e as Error).message}`; 73 | } 74 | return contentFull; 75 | } 76 | 77 | export async function mapResponseToAnswer(resp: Response, controller: AbortController, options: SseChatCompatibleOptions | null, onStream: ((text: string) => Promise) | null): Promise { 78 | options = fixOpenAICompatibleOptions(options || null); 79 | if (onStream && resp.ok && isEventStreamResponse(resp)) { 80 | const stream = options.streamBuilder?.(resp, controller || new AbortController()); 81 | if (!stream) { 82 | throw new Error('Stream builder error'); 83 | } 84 | return streamHandler(stream, options.contentExtractor!, onStream); 85 | } 86 | if (!isJsonResponse(resp)) { 87 | throw new Error(resp.statusText); 88 | } 89 | 90 | const result = await resp.json() as any; 91 | if (!result) { 92 | throw new Error('Empty response'); 93 | } 94 | if (options.errorExtractor?.(result)) { 95 | throw new Error(options.errorExtractor?.(result) || 'Unknown error'); 96 | } 97 | 98 | return options.fullContentExtractor?.(result) || ''; 99 | } 100 | 101 | export async function requestChatCompletions(url: string, header: Record, body: any, onStream: ChatStreamTextHandler | null, options: SseChatCompatibleOptions | null): Promise { 102 | const controller = new AbortController(); 103 | const { signal } = controller; 104 | 105 | let timeoutID = null; 106 | if (ENV.CHAT_COMPLETE_API_TIMEOUT > 0) { 107 | timeoutID = setTimeout(() => controller.abort(), ENV.CHAT_COMPLETE_API_TIMEOUT); 108 | } 109 | 110 | const resp = await fetch(url, { 111 | method: 'POST', 112 | headers: header, 113 | body: JSON.stringify(body), 114 | signal, 115 | }); 116 | if (timeoutID) { 117 | clearTimeout(timeoutID); 118 | } 119 | 120 | return await mapResponseToAnswer(resp, controller, options, onStream); 121 | } 122 | -------------------------------------------------------------------------------- /packages/lib/core/src/route/index.ts: -------------------------------------------------------------------------------- 1 | import type { RouterRequest } from '#/utils/router'; 2 | import type * as Telegram from 'telegram-bot-api-types'; 3 | import { ENV } from '#/config'; 4 | import { createTelegramBotAPI, handleUpdate } from '#/telegram'; 5 | import { commandsBindScope, commandsDocument } from '#/telegram/command'; 6 | import { errorToString, makeResponse200, renderHTML } from '#/utils/resp'; 7 | import { Router } from '#/utils/router'; 8 | 9 | const helpLink = 'https://github.com/TBXark/ChatGPT-Telegram-Workers/blob/master/doc/en/DEPLOY.md'; 10 | const issueLink = 'https://github.com/TBXark/ChatGPT-Telegram-Workers/issues'; 11 | const initLink = './init'; 12 | const footer = ` 13 |
14 |

For more information, please visit ${helpLink}

15 |

If you have any questions, please visit ${issueLink}

16 | `; 17 | 18 | async function bindWebHookAction(request: RouterRequest): Promise { 19 | const result: Record> = {}; 20 | const domain = new URL(request.url).host; 21 | const hookMode = ENV.API_GUARD ? 'safehook' : 'webhook'; 22 | const scope = commandsBindScope(); 23 | for (const token of ENV.TELEGRAM_AVAILABLE_TOKENS) { 24 | const api = createTelegramBotAPI(token); 25 | const url = `https://${domain}/telegram/${token.trim()}/${hookMode}`; 26 | const id = token.split(':')[0]; 27 | result[id] = {}; 28 | result[id].webhook = await api.setWebhook({ url }).then(res => res.json()).catch(e => errorToString(e)); 29 | for (const [s, data] of Object.entries(scope)) { 30 | result[id][s] = await api.setMyCommands(data).then(res => res.json()).catch(e => errorToString(e)); 31 | } 32 | } 33 | let html = `

ChatGPT-Telegram-Workers

`; 34 | html += `

${domain}

`; 35 | if (ENV.TELEGRAM_AVAILABLE_TOKENS.length === 0) { 36 | html += `

Please set the TELEGRAM_AVAILABLE_TOKENS environment variable in Cloudflare Workers.

`; 37 | } else { 38 | for (const [key, res] of Object.entries(result)) { 39 | html += `

Bot: ${key}

`; 40 | for (const [s, data] of Object.entries(res)) { 41 | html += `

${s}: ${JSON.stringify(data)}

`; 42 | } 43 | } 44 | } 45 | html += footer; 46 | const HTML = renderHTML(html); 47 | return new Response(HTML, { status: 200, headers: { 'Content-Type': 'text/html' } }); 48 | } 49 | 50 | async function telegramWebhook(request: RouterRequest): Promise { 51 | try { 52 | const { token } = request.params as any; 53 | const body = await request.json() as Telegram.Update; 54 | return makeResponse200(await handleUpdate(token, body)); 55 | } catch (e) { 56 | console.error(e); 57 | return new Response(errorToString(e), { status: 200 }); 58 | } 59 | } 60 | 61 | /** 62 | *用API_GUARD处理Telegram回调 63 | * @param {Request} request 64 | * @returns {Promise} 65 | */ 66 | async function telegramSafeHook(request: RouterRequest): Promise { 67 | try { 68 | if (ENV.API_GUARD === undefined || ENV.API_GUARD === null) { 69 | return telegramWebhook(request); 70 | } 71 | console.log('API_GUARD is enabled'); 72 | const url = new URL(request.url); 73 | url.pathname = url.pathname.replace('/safehook', '/webhook'); 74 | const newRequest = new Request(url, request); 75 | return makeResponse200(await ENV.API_GUARD.fetch(newRequest)); 76 | } catch (e) { 77 | console.error(e); 78 | return new Response(errorToString(e), { status: 200 }); 79 | } 80 | } 81 | 82 | async function defaultIndexAction(): Promise { 83 | const HTML = renderHTML(` 84 |

ChatGPT-Telegram-Workers

85 |
86 |

Deployed Successfully!

87 |

Version (ts:${ENV.BUILD_TIMESTAMP},sha:${ENV.BUILD_VERSION})

88 |
89 |

You must >>>>> click here <<<<< to bind the webhook.

90 |
91 |

After binding the webhook, you can use the following commands to control the bot:

92 | ${ 93 | commandsDocument().map(item => `

${item.command} - ${item.description}

`).join('') 94 | } 95 |
96 |

You can get bot information by visiting the following URL:

97 |

/telegram/:token/bot - Get bot information

98 | ${footer} 99 | `); 100 | return new Response(HTML, { status: 200, headers: { 'Content-Type': 'text/html' } }); 101 | } 102 | 103 | export function createRouter(): Router { 104 | const router = new Router(); 105 | router.get('/', defaultIndexAction); 106 | router.get('/init', bindWebHookAction); 107 | router.post('/telegram/:token/webhook', telegramWebhook); 108 | router.post('/telegram/:token/safehook', telegramSafeHook); 109 | router.all('*', () => new Response('Not Found', { status: 404 })); 110 | return router; 111 | } 112 | -------------------------------------------------------------------------------- /packages/lib/core/src/agent/anthropic.ts: -------------------------------------------------------------------------------- 1 | import type { AgentUserConfig } from '#/config'; 2 | import type { SseChatCompatibleOptions } from './request'; 3 | import type { SSEMessage, SSEParserResult } from './stream'; 4 | import type { 5 | AgentEnable, 6 | AgentModel, 7 | AgentModelList, 8 | ChatAgent, 9 | ChatAgentRequest, 10 | ChatAgentResponse, 11 | ChatStreamTextHandler, 12 | HistoryItem, 13 | LLMChatParams, 14 | } from './types'; 15 | import { loadOpenAIModelList } from '#/agent/openai_compatibility'; 16 | import { ENV } from '#/config'; 17 | import { imageToBase64String } from '#/utils/image'; 18 | import { requestChatCompletions } from './request'; 19 | import { Stream } from './stream'; 20 | import { convertStringToResponseMessages, extractImageContent, getAgentUserConfigFieldName } from './utils'; 21 | 22 | function anthropicHeader(context: AgentUserConfig): Record { 23 | return { 24 | 'x-api-key': context.ANTHROPIC_API_KEY || '', 25 | 'anthropic-version': '2023-06-01', 26 | 'content-type': 'application/json', 27 | }; 28 | } 29 | 30 | export class Anthropic implements ChatAgent { 31 | readonly name = 'anthropic'; 32 | readonly modelKey = getAgentUserConfigFieldName('ANTHROPIC_CHAT_MODEL'); 33 | 34 | readonly enable: AgentEnable = ctx => !!(ctx.ANTHROPIC_API_KEY); 35 | readonly model: AgentModel = ctx => ctx.ANTHROPIC_CHAT_MODEL; 36 | readonly modelList: AgentModelList = ctx => loadOpenAIModelList(ctx.ANTHROPIC_CHAT_MODELS_LIST, ctx.ANTHROPIC_API_BASE, anthropicHeader(ctx)); 37 | 38 | private static render = async (item: HistoryItem): Promise => { 39 | const res: Record = { 40 | role: item.role, 41 | content: item.content, 42 | }; 43 | if (item.role === 'system') { 44 | return null; 45 | } 46 | if (Array.isArray(item.content)) { 47 | const contents = []; 48 | for (const content of item.content) { 49 | switch (content.type) { 50 | case 'text': 51 | contents.push({ type: 'text', text: content.text }); 52 | break; 53 | case 'image': { 54 | const data = extractImageContent(content.image); 55 | if (data.url) { 56 | contents.push(await imageToBase64String(data.url).then(({ format, data }) => { 57 | return { type: 'image', source: { type: 'base64', media_type: format, data } }; 58 | })); 59 | } else if (data.base64) { 60 | contents.push({ type: 'image', source: { type: 'base64', media_type: 'image/jpeg', data: data.base64 } }); 61 | } 62 | break; 63 | } 64 | default: 65 | break; 66 | } 67 | } 68 | res.content = contents; 69 | } 70 | return res; 71 | }; 72 | 73 | private static parser(sse: SSEMessage): SSEParserResult { 74 | // example: 75 | // event: content_block_delta 76 | // data: {"type": "content_block_delta", "index": 0, "delta": {"type": "text_delta", "text": "Hello"}} 77 | // event: message_stop 78 | // data: {"type": "message_stop"} 79 | switch (sse.event) { 80 | case 'content_block_delta': 81 | try { 82 | return { data: JSON.parse(sse.data || '') }; 83 | } catch (e) { 84 | console.error(e, sse.data); 85 | return {}; 86 | } 87 | case 'message_start': 88 | case 'content_block_start': 89 | case 'content_block_stop': 90 | return {}; 91 | case 'message_stop': 92 | return { finish: true }; 93 | default: 94 | return {}; 95 | } 96 | } 97 | 98 | readonly request: ChatAgentRequest = async (params: LLMChatParams, context: AgentUserConfig, onStream: ChatStreamTextHandler | null): Promise => { 99 | const { prompt, messages } = params; 100 | const url = `${context.ANTHROPIC_API_BASE}/messages`; 101 | const header = anthropicHeader(context); 102 | 103 | if (messages.length > 0 && messages[0].role === 'system') { 104 | messages.shift(); 105 | } 106 | 107 | const body = { 108 | ...(context.ANTHROPIC_CHAT_EXTRA_PARAMS || {}), 109 | system: prompt, 110 | model: context.ANTHROPIC_CHAT_MODEL, 111 | messages: (await Promise.all(messages.map(item => Anthropic.render(item)))).filter(i => i !== null), 112 | stream: onStream != null, 113 | max_tokens: ENV.MAX_TOKEN_LENGTH > 0 ? ENV.MAX_TOKEN_LENGTH : 2048, 114 | }; 115 | if (!body.system) { 116 | delete body.system; 117 | } 118 | const options: SseChatCompatibleOptions = {}; 119 | options.streamBuilder = function (r, c) { 120 | return new Stream(r, c, Anthropic.parser); 121 | }; 122 | options.contentExtractor = function (data: any) { 123 | return data?.delta?.text; 124 | }; 125 | options.fullContentExtractor = function (data: any) { 126 | return data?.content?.at(0).text; 127 | }; 128 | options.errorExtractor = function (data: any) { 129 | return data?.error?.message; 130 | }; 131 | return convertStringToResponseMessages(requestChatCompletions(url, header, body, onStream, options)); 132 | }; 133 | } 134 | -------------------------------------------------------------------------------- /packages/lib/next/src/index.ts: -------------------------------------------------------------------------------- 1 | import type { ProviderV2 } from '@ai-sdk/provider'; 2 | import type { AgentUserConfig, ChatAgent, ChatAgentResponse, ChatStreamTextHandler, HistoryItem, LLMChatParams, ResponseMessage } from '@chatgpt-telegram-workers/core'; 3 | import type { AssistantModelMessage, LanguageModel, ModelMessage, ToolModelMessage } from 'ai'; 4 | import { createAnthropic } from '@ai-sdk/anthropic'; 5 | import { createAzure } from '@ai-sdk/azure'; 6 | import { createCohere } from '@ai-sdk/cohere'; 7 | import { createGoogleGenerativeAI } from '@ai-sdk/google'; 8 | import { createMistral } from '@ai-sdk/mistral'; 9 | import { createOpenAI } from '@ai-sdk/openai'; 10 | import { streamHandler } from '@chatgpt-telegram-workers/core'; 11 | import { generateText, streamText } from 'ai'; 12 | 13 | function convertResponseToMessages(messages: (AssistantModelMessage | ToolModelMessage)[]): ResponseMessage[] { 14 | return messages.map((message) => { 15 | if (message.role && message.content) { 16 | return { 17 | role: message.role, 18 | content: message.content, 19 | } as ResponseMessage; 20 | } 21 | return null; 22 | }).filter(message => message !== null) as ResponseMessage[]; 23 | } 24 | 25 | export async function requestChatCompletionsV2(params: { model: LanguageModel; system?: string; messages: HistoryItem[] }, onStream: ChatStreamTextHandler | null): Promise { 26 | const messages = params.messages as Array; 27 | const baseOptions = { 28 | model: params.model, 29 | messages, 30 | ...(params.system ? { system: params.system } : {}), 31 | }; 32 | 33 | if (onStream !== null) { 34 | const stream = streamText(baseOptions); 35 | await streamHandler(stream.textStream, t => t, onStream); 36 | return { 37 | text: await stream.text, 38 | responses: convertResponseToMessages((await stream.response).messages), 39 | }; 40 | } else { 41 | const result = await generateText(baseOptions); 42 | return { 43 | text: result.text, 44 | responses: convertResponseToMessages(result.response.messages), 45 | }; 46 | } 47 | } 48 | 49 | export type ProviderCreator = (context: AgentUserConfig) => ProviderV2; 50 | 51 | export class NextChatAgent implements ChatAgent { 52 | readonly name: string; 53 | readonly modelKey: string; 54 | readonly adapter: ChatAgent; 55 | readonly providerCreator: ProviderCreator; 56 | 57 | constructor(adapter: ChatAgent, providerCreator: ProviderCreator) { 58 | this.name = adapter.name; 59 | this.modelKey = adapter.modelKey; 60 | this.adapter = adapter; 61 | this.providerCreator = providerCreator; 62 | } 63 | 64 | static from(agent: ChatAgent): NextChatAgent | null { 65 | const provider = this.newProviderCreator(agent.name); 66 | if (!provider) { 67 | return null; 68 | } 69 | return new NextChatAgent(agent, provider); 70 | } 71 | 72 | readonly enable = (context: AgentUserConfig): boolean => { 73 | return this.adapter.enable(context); 74 | }; 75 | 76 | readonly model = (ctx: AgentUserConfig): string | null => { 77 | return this.adapter.model(ctx); 78 | }; 79 | 80 | static newProviderCreator = (provider: string): ProviderCreator | null => { 81 | switch (provider) { 82 | case 'anthropic': 83 | return (context: AgentUserConfig) => createAnthropic({ 84 | baseURL: context.ANTHROPIC_API_BASE, 85 | apiKey: context.ANTHROPIC_API_KEY || undefined, 86 | }); 87 | case 'azure': 88 | return (context: AgentUserConfig) => createAzure({ 89 | resourceName: context.AZURE_RESOURCE_NAME || undefined, 90 | apiKey: context.AZURE_API_KEY || undefined, 91 | }); 92 | case 'cohere': 93 | return (context: AgentUserConfig) => createCohere({ 94 | baseURL: context.COHERE_API_BASE, 95 | apiKey: context.COHERE_API_KEY || undefined, 96 | }); 97 | case 'gemini': 98 | return (context: AgentUserConfig) => createGoogleGenerativeAI({ 99 | baseURL: context.GOOGLE_API_BASE, 100 | apiKey: context.GOOGLE_API_KEY || undefined, 101 | }); 102 | case 'mistral': 103 | return (context: AgentUserConfig) => createMistral({ 104 | baseURL: context.MISTRAL_API_BASE, 105 | apiKey: context.MISTRAL_API_KEY || undefined, 106 | }); 107 | case 'openai': 108 | return (context: AgentUserConfig) => createOpenAI({ 109 | baseURL: context.OPENAI_API_BASE, 110 | apiKey: context.OPENAI_API_KEY.at(0) || undefined, 111 | }); 112 | default: 113 | return null; 114 | } 115 | }; 116 | 117 | readonly request = async (params: LLMChatParams, context: AgentUserConfig, onStream: ChatStreamTextHandler | null): Promise => { 118 | const model = this.model(context); 119 | if (!model) { 120 | throw new Error('Model not found'); 121 | } 122 | return requestChatCompletionsV2({ 123 | model: this.providerCreator(context).languageModel(model), 124 | messages: params.messages, 125 | system: params.prompt, 126 | }, onStream); 127 | }; 128 | 129 | readonly modelList = async (context: AgentUserConfig): Promise => { 130 | return this.adapter.modelList(context); 131 | }; 132 | } 133 | 134 | export function injectNextChatAgent(agents: ChatAgent[]) { 135 | for (let i = 0; i < agents.length; i++) { 136 | const next = NextChatAgent.from(agents[i]); 137 | if (next) { 138 | agents[i] = next; 139 | } 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/chat/index.ts: -------------------------------------------------------------------------------- 1 | import type { HistoryModifier, StreamResultHandler, UserContentPart, UserMessageItem } from '#/agent'; 2 | import type { WorkerContext } from '#/config'; 3 | import type * as Telegram from 'telegram-bot-api-types'; 4 | import { loadChatLLM, requestCompletionsFromLLM } from '#/agent'; 5 | import { ENV } from '#/config'; 6 | import { createTelegramBotAPI } from '../api'; 7 | import { MessageSender } from '../sender'; 8 | 9 | export async function chatWithMessage(message: Telegram.Message, params: UserMessageItem | null, context: WorkerContext, modifier: HistoryModifier | null): Promise { 10 | const sender = MessageSender.fromMessage(context.SHARE_CONTEXT.botToken, message); 11 | try { 12 | try { 13 | const msg = await sender.sendPlainText('...').then(r => r.json()) as Telegram.ResponseWithMessage; 14 | sender.update({ 15 | message_id: msg.result.message_id, 16 | }); 17 | } catch (e) { 18 | console.error(e); 19 | } 20 | const api = createTelegramBotAPI(context.SHARE_CONTEXT.botToken); 21 | setTimeout(() => api.sendChatAction({ 22 | chat_id: message.chat.id, 23 | action: 'typing', 24 | }).catch(console.error), 0); 25 | let onStream: StreamResultHandler | null = null; 26 | let nextEnableTime: number | null = null; 27 | if (ENV.STREAM_MODE) { 28 | onStream = async (text: string): Promise => { 29 | try { 30 | // 判断是否需要等待 31 | if (nextEnableTime && nextEnableTime > Date.now()) { 32 | return; 33 | } 34 | const resp = await sender.sendPlainText(text); 35 | // 判断429 36 | if (resp.status === 429) { 37 | // 获取重试时间 38 | const retryAfter = Number.parseInt(resp.headers.get('Retry-After') || ''); 39 | if (retryAfter) { 40 | nextEnableTime = Date.now() + retryAfter * 1000; 41 | return; 42 | } 43 | } 44 | nextEnableTime = null; 45 | if (resp.ok) { 46 | const respJson = await resp.json() as Telegram.ResponseWithMessage; 47 | sender.update({ 48 | message_id: respJson.result.message_id, 49 | }); 50 | } 51 | } catch (e) { 52 | console.error(e); 53 | } 54 | }; 55 | } 56 | 57 | const agent = loadChatLLM(context.USER_CONFIG); 58 | if (agent === null) { 59 | return sender.sendPlainText('LLM is not enable'); 60 | } 61 | const answer = await requestCompletionsFromLLM(params, context, agent, modifier, onStream); 62 | if (nextEnableTime !== null && nextEnableTime > Date.now()) { 63 | await new Promise(resolve => setTimeout(resolve, (nextEnableTime ?? 0) - Date.now())); 64 | } 65 | return sender.sendRichText(answer); 66 | } catch (e) { 67 | let errMsg = `Error: ${(e as Error).message}`; 68 | if (errMsg.length > 2048) { 69 | // 裁剪错误信息 最长2048 70 | errMsg = errMsg.substring(0, 2048); 71 | } 72 | return sender.sendPlainText(errMsg); 73 | } 74 | } 75 | 76 | export async function extractImageURL(fileId: string | null, context: WorkerContext): Promise { 77 | if (!fileId) { 78 | return null; 79 | } 80 | const api = createTelegramBotAPI(context.SHARE_CONTEXT.botToken); 81 | const file = await api.getFileWithReturns({ file_id: fileId }); 82 | const filePath = file.result.file_path; 83 | if (filePath) { 84 | const url = URL.parse(`${ENV.TELEGRAM_API_DOMAIN}/file/bot${context.SHARE_CONTEXT.botToken}/${filePath}`); 85 | if (url) { 86 | return url; 87 | } 88 | } 89 | return null; 90 | } 91 | 92 | export function extractImageFileID(message: Telegram.Message): string | null { 93 | if (message.photo && message.photo.length > 0) { 94 | const offset = ENV.TELEGRAM_PHOTO_SIZE_OFFSET; 95 | const length = message.photo.length; 96 | const sizeIndex = Math.max(0, Math.min(offset >= 0 ? offset : length + offset, length - 1)); 97 | return message.photo[sizeIndex]?.file_id; 98 | } else if (message.document && message.document.thumbnail) { 99 | return message.document.thumbnail.file_id; 100 | } 101 | return null; 102 | } 103 | 104 | export async function extractUserMessageItem(message: Telegram.Message, context: WorkerContext): Promise { 105 | let text = message.text || message.caption || ''; 106 | const urls = await extractImageURL(extractImageFileID(message), context).then(u => u ? [u] : []); 107 | if ( 108 | ENV.EXTRA_MESSAGE_CONTEXT 109 | && message.reply_to_message 110 | && message.reply_to_message.from 111 | && `${message.reply_to_message.from.id}` !== `${context.SHARE_CONTEXT.botId}` // ignore bot reply 112 | ) { 113 | const extraText = message.reply_to_message.text || message.reply_to_message.caption || ''; 114 | if (extraText) { 115 | text = `${text}\nThe following is the referenced context: ${extraText}`; 116 | } 117 | if (ENV.EXTRA_MESSAGE_MEDIA_COMPATIBLE.includes('image') && message.reply_to_message.photo) { 118 | const url = await extractImageURL(extractImageFileID(message.reply_to_message), context); 119 | if (url) { 120 | urls.push(url); 121 | } 122 | } 123 | } 124 | const params: UserMessageItem = { 125 | role: 'user', 126 | content: text, 127 | }; 128 | if (urls.length > 0) { 129 | const contents = new Array(); 130 | if (text) { 131 | contents.push({ type: 'text', text }); 132 | } 133 | for (const url of urls) { 134 | contents.push({ type: 'image', image: url }); 135 | } 136 | params.content = contents; 137 | } 138 | return params; 139 | } 140 | -------------------------------------------------------------------------------- /packages/lib/plugins/src/template.ts: -------------------------------------------------------------------------------- 1 | import { interpolate } from './interpolate'; 2 | 3 | /** 4 | * TemplateInputType: 输入数据的类型,将Telegram输入的数据转换为对应的数据类型 5 | * json: JSON格式 6 | * space-separated: 以空格分隔的字符串 7 | * comma-separated: 以逗号分隔的字符串 8 | * text: 文本,不分割(默认值) 9 | */ 10 | export type TemplateInputType = 'json' | 'space-separated' | 'comma-separated' | 'text'; 11 | 12 | /** 13 | * TemplateBodyType: 请求体的类型 14 | * json: JSON格式, 此时对于content字段的值应该为一个对象,其中的key为固定值,Value支持插值 15 | * form: 表单格式, 此时对于content字段的值应该为一个对象,其中的key为固定值,Value支持插值 16 | * text: 文本格式, 此时对于content字段的值应该为一个字符串,支持插值 17 | */ 18 | export type TemplateBodyType = 'json' | 'form' | 'text'; 19 | 20 | /** 21 | * TemplateResponseType: 响应体的类型 22 | * json: JSON格式, 此时会将响应体解析为JSON格式交给下一个模板渲染 23 | * text: 文本格式, 此时会将响应体解析为文本格式交给下一个模板渲染 24 | * blob: 二进制格式, 此时会将响应体直接返回 25 | */ 26 | export type TemplateResponseType = 'json' | 'text' | 'blob'; 27 | 28 | /** 29 | * TemplateOutputType: 输出数据的类型 30 | * text: 文本格式, 将渲染结果作为纯文本发送到telegram 31 | * image: 图片格式, 将渲染结果作为图片url发送到telegram 32 | * html: HTML格式, 将渲染结果作为HTML格式发送到telegram 33 | * markdown: Markdown格式, 将渲染结果作为Markdown格式发送到telegram 34 | */ 35 | export type TemplateOutputType = 'text' | 'image' | 'html' | 'markdown'; 36 | 37 | export interface RequestTemplate { 38 | url: string; // 必选, 支持插值 39 | method: string; // 必选, 固定值 40 | headers: { [key: string]: string }; // 可选, Key为固定值,Value支持插值 41 | input: { 42 | type: TemplateInputType; 43 | required: boolean; // 必选, 是否必须输入 44 | }; 45 | query: { [key: string]: string }; // 可选, Key为固定值,Value支持插值 46 | body: { 47 | type: TemplateBodyType; 48 | content: { [key: string]: string } | string; // content为对象时Key为固定值,Value支持插值。content为字符串时支持插值 49 | }; 50 | response: { 51 | content: { // 必选, 当请求成功时的处理 52 | input_type: TemplateResponseType; 53 | output_type: TemplateOutputType; 54 | output: string; 55 | }; 56 | error: { // 必选, 当请求失败时的处理 57 | input_type: TemplateResponseType; 58 | output_type: TemplateOutputType; 59 | output: string; 60 | }; 61 | }; 62 | } 63 | 64 | function interpolateObject(obj: any, data: any): any { 65 | if (obj === null || obj === undefined) { 66 | return null; 67 | } 68 | if (typeof obj === 'string') { 69 | return interpolate(obj, data); 70 | } 71 | if (Array.isArray(obj)) { 72 | return obj.map(item => interpolateObject(item, data)); 73 | } 74 | if (typeof obj === 'object') { 75 | const result: any = {}; 76 | for (const [key, value] of Object.entries(obj)) { 77 | result[key] = interpolateObject(value, data); 78 | } 79 | return result; 80 | } 81 | return obj; 82 | } 83 | 84 | export type ExecuteRequestResult = { content: string; type: TemplateOutputType } | { content: Blob; type: 'image' }; 85 | 86 | export async function executeRequest(template: RequestTemplate, data: any): Promise { 87 | const urlRaw = interpolate(template.url, data, encodeURIComponent); 88 | const url = new URL(urlRaw); 89 | 90 | if (template.query) { 91 | for (const [key, value] of Object.entries(template.query)) { 92 | url.searchParams.append(key, interpolate(value, data)); 93 | } 94 | } 95 | 96 | const method = template.method; 97 | const headers = Object.fromEntries( 98 | Object.entries(template.headers || {}).map(([key, value]) => { 99 | return [key, interpolate(value, data)]; 100 | }), 101 | ); 102 | for (const key of Object.keys(headers)) { 103 | if (headers[key] === null) { 104 | delete headers[key]; 105 | } 106 | } 107 | 108 | let body = null; 109 | if (template.body) { 110 | if (template.body.type === 'json') { 111 | body = JSON.stringify(interpolateObject(template.body.content, data)); 112 | } else if (template.body.type === 'form') { 113 | body = new URLSearchParams(); 114 | for (const [key, value] of Object.entries(template.body.content)) { 115 | body.append(key, interpolate(value, data)); 116 | } 117 | } else { 118 | body = interpolate(template.body.content as string, data); 119 | } 120 | } 121 | 122 | const response = await fetch(url, { 123 | method, 124 | headers, 125 | body, 126 | }); 127 | 128 | const renderOutput = async (type: TemplateResponseType, temple: string, response: Response): Promise => { 129 | switch (type) { 130 | case 'text': 131 | return interpolate(temple, await response.text()); 132 | case 'blob': 133 | throw new Error('Invalid output type'); 134 | case 'json': 135 | default: 136 | return interpolate(temple, await response.json()); 137 | } 138 | }; 139 | if (!response.ok) { 140 | const content = await renderOutput(template.response?.error?.input_type, template.response.error?.output, response); 141 | return { 142 | type: template.response.error.output_type, 143 | content, 144 | }; 145 | } 146 | if (template.response.content.input_type === 'blob') { 147 | if (template.response.content.output_type !== 'image') { 148 | throw new Error('Invalid output type'); 149 | } 150 | return { 151 | type: 'image', 152 | content: await response.blob(), 153 | }; 154 | } 155 | const content = await renderOutput(template.response.content?.input_type, template.response.content?.output, response); 156 | return { 157 | type: template.response.content.output_type, 158 | content, 159 | }; 160 | } 161 | 162 | export function formatInput(input: string, type: TemplateInputType): string | string[] | any { 163 | if (type === 'json') { 164 | return JSON.parse(input); 165 | } else if (type === 'space-separated') { 166 | return input.trim().split(' ').filter(Boolean); 167 | } else if (type === 'comma-separated') { 168 | return input.split(',').map(item => item.trim()).filter(Boolean); 169 | } else { 170 | return input; 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /packages/lib/core/src/config/context.ts: -------------------------------------------------------------------------------- 1 | import type { AgentUserConfig, AgentUserConfigKey } from '#/config/config'; 2 | import type * as Telegram from 'telegram-bot-api-types'; 3 | import { ENV, ENV_KEY_MAPPER } from './env'; 4 | import { ConfigMerger } from './merger'; 5 | 6 | export class ShareContext { 7 | botId: number; 8 | botToken: string; 9 | botName: string | null = null; 10 | 11 | // KV 保存的键 12 | chatHistoryKey: string; 13 | lastMessageKey: string; 14 | configStoreKey: string; 15 | groupAdminsKey?: string; 16 | 17 | constructor(token: string, update: UpdateContext) { 18 | const botId = Number.parseInt(token.split(':')[0]); 19 | 20 | const telegramIndex = ENV.TELEGRAM_AVAILABLE_TOKENS.indexOf(token); 21 | if (telegramIndex === -1) { 22 | throw new Error('Token not allowed'); 23 | } 24 | if (ENV.TELEGRAM_BOT_NAME.length > telegramIndex) { 25 | this.botName = ENV.TELEGRAM_BOT_NAME[telegramIndex]; 26 | } 27 | 28 | this.botToken = token; 29 | this.botId = botId; 30 | const id = update.chatID; 31 | if (id === undefined || id === null) { 32 | throw new Error('Chat id not found'); 33 | } 34 | // message_id每次都在变的。 35 | // 私聊消息中: 36 | // message.chat.id 是发言人id 37 | // 群组消息中: 38 | // message.chat.id 是群id 39 | // message.from.id 是发言人id 40 | // 没有开启群组共享模式时,要加上发言人id 41 | // chatHistoryKey = history:chat_id:bot_id:(from_id) 42 | // configStoreKey = user_config:chat_id:bot_id:(from_id) 43 | 44 | let historyKey = `history:${id}`; 45 | let configStoreKey = `user_config:${id}`; 46 | 47 | if (botId) { 48 | historyKey += `:${botId}`; 49 | configStoreKey += `:${botId}`; 50 | } 51 | // 标记群组消息 52 | switch (update.chatType) { 53 | case 'group': 54 | case 'supergroup': 55 | if (!ENV.GROUP_CHAT_BOT_SHARE_MODE && update.fromUserID) { 56 | historyKey += `:${update.fromUserID}`; 57 | configStoreKey += `:${update.fromUserID}`; 58 | } 59 | this.groupAdminsKey = `group_admin:${id}`; 60 | break; 61 | default: 62 | break; 63 | } 64 | 65 | // 判断是否为话题模式 66 | if (update.isForum && update.isTopicMessage) { 67 | if (update.messageThreadID) { 68 | historyKey += `:${update.messageThreadID}`; 69 | configStoreKey += `:${update.messageThreadID}`; 70 | } 71 | } 72 | 73 | this.chatHistoryKey = historyKey; 74 | this.lastMessageKey = `last_message_id:${historyKey}`; 75 | this.configStoreKey = configStoreKey; 76 | } 77 | } 78 | 79 | export class WorkerContext { 80 | // 用户配置 81 | USER_CONFIG: AgentUserConfig; 82 | SHARE_CONTEXT: ShareContext; 83 | 84 | constructor(USER_CONFIG: AgentUserConfig, SHARE_CONTEXT: ShareContext) { 85 | this.USER_CONFIG = USER_CONFIG; 86 | this.SHARE_CONTEXT = SHARE_CONTEXT; 87 | this.execChangeAndSave = this.execChangeAndSave.bind(this); 88 | } 89 | 90 | static async from(token: string, update: Telegram.Update): Promise { 91 | const context = new UpdateContext(update); 92 | const SHARE_CONTEXT = new ShareContext(token, context); 93 | const USER_CONFIG = Object.assign({}, ENV.USER_CONFIG); 94 | try { 95 | const userConfig: AgentUserConfig = JSON.parse(await ENV.DATABASE.get(SHARE_CONTEXT.configStoreKey)); 96 | ConfigMerger.merge(USER_CONFIG, ConfigMerger.trim(userConfig, ENV.LOCK_USER_CONFIG_KEYS) || {}); 97 | } catch (e) { 98 | console.warn(e); 99 | } 100 | return new WorkerContext(USER_CONFIG, SHARE_CONTEXT); 101 | } 102 | 103 | async execChangeAndSave(values: Record): Promise { 104 | for (const ent of Object.entries(values || {})) { 105 | let [key, value] = ent as [AgentUserConfigKey, any]; 106 | key = ENV_KEY_MAPPER[key] || key; 107 | if (ENV.LOCK_USER_CONFIG_KEYS.includes(key)) { 108 | throw new Error(`Key ${key} is locked`); 109 | } 110 | const configKeys = Object.keys(this.USER_CONFIG || {}) || []; 111 | if (!configKeys.includes(key)) { 112 | throw new Error(`Key ${key} is not allowed`); 113 | } 114 | this.USER_CONFIG.DEFINE_KEYS.push(key); 115 | ConfigMerger.merge(this.USER_CONFIG, { 116 | [key]: value, 117 | }); 118 | console.log('Update user config: ', key, this.USER_CONFIG[key]); 119 | } 120 | this.USER_CONFIG.DEFINE_KEYS = Array.from(new Set(this.USER_CONFIG.DEFINE_KEYS)); 121 | await ENV.DATABASE.put( 122 | this.SHARE_CONTEXT.configStoreKey, 123 | JSON.stringify(ConfigMerger.trim(this.USER_CONFIG, ENV.LOCK_USER_CONFIG_KEYS)), 124 | ); 125 | } 126 | } 127 | 128 | class UpdateContext { 129 | fromUserID?: number; 130 | chatID?: number; 131 | chatType?: string; 132 | 133 | isForum?: boolean; 134 | isTopicMessage?: boolean; 135 | messageThreadID?: number; 136 | 137 | constructor(update: Telegram.Update) { 138 | if (update.message) { 139 | this.fromUserID = update.message.from?.id; 140 | this.chatID = update.message.chat.id; 141 | this.chatType = update.message.chat.type; 142 | this.isForum = update.message.chat.is_forum; 143 | this.isTopicMessage = update.message.is_topic_message; 144 | this.messageThreadID = update.message.message_thread_id; 145 | } else if (update.callback_query) { 146 | this.fromUserID = update.callback_query.from.id; 147 | this.chatID = update.callback_query.message?.chat.id; 148 | this.chatType = update.callback_query.message?.chat.type; 149 | this.isForum = update.callback_query.message?.chat.is_forum; 150 | // this.isTopicMessage = update.callback_query.message?.is_topic_message; // unsupported 151 | // this.messageThreadID = update.callback_query.message?.message_thread_id; // unsupported 152 | } else { 153 | console.error('Unknown update type'); 154 | } 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /packages/lib/core/src/telegram/handler/handlers.ts: -------------------------------------------------------------------------------- 1 | import type { WorkerContext } from '#/config'; 2 | import type * as Telegram from 'telegram-bot-api-types'; 3 | import type { MessageHandler, UpdateHandler } from './types'; 4 | import { ENV } from '#/config'; 5 | import { isGroupChat } from '../auth'; 6 | import { handleCallbackQuery } from '../callback_query'; 7 | import { chatWithMessage, extractUserMessageItem } from '../chat'; 8 | import { handleCommandMessage } from '../command'; 9 | import { MessageSender } from '../sender'; 10 | 11 | export class EnvChecker implements UpdateHandler { 12 | handle = async (update: Telegram.Update, context: WorkerContext): Promise => { 13 | if (!ENV.DATABASE) { 14 | return MessageSender 15 | .fromUpdate(context.SHARE_CONTEXT.botToken, update) 16 | .sendPlainText('DATABASE Not Set'); 17 | } 18 | return null; 19 | }; 20 | } 21 | 22 | export class WhiteListFilter implements UpdateHandler { 23 | handle = async (update: Telegram.Update, context: WorkerContext): Promise => { 24 | if (ENV.I_AM_A_GENEROUS_PERSON) { 25 | return null; 26 | } 27 | const sender = MessageSender.fromUpdate(context.SHARE_CONTEXT.botToken, update); 28 | 29 | let chatType = ''; 30 | let chatID = 0; 31 | 32 | if (update.message) { 33 | chatType = update.message.chat.type; 34 | chatID = update.message.chat.id; 35 | } else if (update.callback_query?.message) { 36 | chatType = update.callback_query.message.chat.type; 37 | chatID = update.callback_query.message.chat.id; 38 | } 39 | 40 | if (!chatType || !chatID) { 41 | throw new Error('Invalid chat type or chat id'); 42 | } 43 | const text = `You are not in the white list, please contact the administrator to add you to the white list. Your chat_id: ${chatID}`; 44 | 45 | // 判断私聊消息 46 | if (chatType === 'private') { 47 | // 白名单判断 48 | if (!ENV.CHAT_WHITE_LIST.includes(`${chatID}`)) { 49 | return sender.sendPlainText(text); 50 | } 51 | return null; 52 | } 53 | 54 | // 判断群组消息 55 | if (isGroupChat(chatType)) { 56 | // 未打开群组机器人开关,直接忽略 57 | if (!ENV.GROUP_CHAT_BOT_ENABLE) { 58 | throw new Error('Not support'); 59 | } 60 | // 白名单判断 61 | if (!ENV.CHAT_GROUP_WHITE_LIST.includes(`${chatID}`)) { 62 | return sender.sendPlainText(text); 63 | } 64 | return null; 65 | } 66 | 67 | return sender.sendPlainText( 68 | `Not support chat type: ${chatType}`, 69 | ); 70 | }; 71 | } 72 | 73 | export class Update2MessageHandler implements UpdateHandler { 74 | messageHandlers: MessageHandler[]; 75 | constructor(messageHandlers: MessageHandler[]) { 76 | this.messageHandlers = messageHandlers; 77 | } 78 | 79 | loadMessage(body: Telegram.Update): Telegram.Message { 80 | if (body.edited_message) { 81 | throw new Error('Ignore edited message'); 82 | } 83 | if (body.message) { 84 | return body?.message; 85 | } else { 86 | throw new Error('Invalid message'); 87 | } 88 | } 89 | 90 | handle = async (update: Telegram.Update, context: WorkerContext): Promise => { 91 | const message = this.loadMessage(update); 92 | if (!message) { 93 | return null; 94 | } 95 | for (const handler of this.messageHandlers) { 96 | const result = await handler.handle(message, context); 97 | if (result) { 98 | return result; 99 | } 100 | } 101 | return null; 102 | }; 103 | } 104 | 105 | export class CallbackQueryHandler implements UpdateHandler { 106 | handle = async (update: Telegram.Update, context: WorkerContext): Promise => { 107 | if (update.callback_query) { 108 | return handleCallbackQuery(update.callback_query, context); 109 | } 110 | return null; 111 | }; 112 | } 113 | 114 | export class SaveLastMessage implements MessageHandler { 115 | handle = async (message: Telegram.Message, context: WorkerContext): Promise => { 116 | if (!ENV.DEBUG_MODE) { 117 | return null; 118 | } 119 | const lastMessageKey = `last_message:${context.SHARE_CONTEXT.chatHistoryKey}`; 120 | await ENV.DATABASE.put(lastMessageKey, JSON.stringify(message), { expirationTtl: 3600 }); 121 | return null; 122 | }; 123 | } 124 | 125 | export class OldMessageFilter implements MessageHandler { 126 | handle = async (message: Telegram.Message, context: WorkerContext): Promise => { 127 | if (!ENV.SAFE_MODE) { 128 | return null; 129 | } 130 | let idList = []; 131 | try { 132 | idList = JSON.parse(await ENV.DATABASE.get(context.SHARE_CONTEXT.lastMessageKey).catch(() => '[]')) || []; 133 | } catch (e) { 134 | console.error(e); 135 | } 136 | // 保存最近的100条消息,如果存在则忽略,如果不存在则保存 137 | if (idList.includes(message.message_id)) { 138 | throw new Error('Ignore old message'); 139 | } else { 140 | idList.push(message.message_id); 141 | if (idList.length > 100) { 142 | idList.shift(); 143 | } 144 | await ENV.DATABASE.put(context.SHARE_CONTEXT.lastMessageKey, JSON.stringify(idList)); 145 | } 146 | return null; 147 | }; 148 | } 149 | 150 | export class MessageFilter implements MessageHandler { 151 | // eslint-disable-next-line unused-imports/no-unused-vars 152 | handle = async (message: Telegram.Message, context: WorkerContext): Promise => { 153 | if (message.text) { 154 | return null;// 纯文本消息 155 | } 156 | if (message.caption) { 157 | return null;// 图文消息 158 | } 159 | if (message.photo) { 160 | return null;// 图片消息 161 | } 162 | throw new Error('Not supported message type'); 163 | }; 164 | } 165 | 166 | export class CommandHandler implements MessageHandler { 167 | handle = async (message: Telegram.Message, context: WorkerContext): Promise => { 168 | if (message.text || message.caption) { 169 | return await handleCommandMessage(message, context); 170 | } 171 | // 非文本消息不作处理 172 | return null; 173 | }; 174 | } 175 | 176 | export class ChatHandler implements MessageHandler { 177 | handle = async (message: Telegram.Message, context: WorkerContext): Promise => { 178 | const params = await extractUserMessageItem(message, context); 179 | return chatWithMessage(message, params, context, null); 180 | }; 181 | } 182 | --------------------------------------------------------------------------------