core
通用、统一的后端接口项目。各种想法和接口统一收纳到这里,按业务域拆分为独立模块。基于 Koa + TypeScript,当前已包含图片压缩、Telegram 用户运行时、Telegram 监听与媒体下载、数据库队列、WebSocket 推送等能力。
已实现能力
image-compress:接收 Lsky 图片信息,后台压缩为多档 webp,提供公开静态访问和批量删除。auth:统一 Bearer token 鉴权,并提供 Telegram 2FA 密码提交接口。telegram:用 mtcute 启动 Telegram 用户客户端,支持会话同步、历史消息读取、媒体批次管理、通用消息发送、论坛话题操作、监听群组管理、监听消息里的直传媒体和 Telegram 消息链接。queue:基于 MySQL 的通用任务队列,目前用于 Telegram 消息解析和媒体下载,并提供状态查询、失败重试、运行中下载停止。storage:在环境变量配置的固定根目录内分层浏览目录,并通过限时签名 URL 流式下载普通文件。ws:挂载在/ws的 WebSocket 服务,复用AUTH_TOKEN鉴权,提供模块化事件广播和队列状态推送。
接口
统一响应格式 { code, data, message },成功 code: 0。常规 HTTP 鉴权方式:请求头 Authorization: Bearer <AUTH_TOKEN>,或 query 参数 ?token=<AUTH_TOKEN>。
Telegram 相关接口的完整参数、返回类型和接口串联关系见:
| 方法 | 路径 | 说明 | 鉴权 | 模块 |
|---|---|---|---|---|
| POST | /api/image/compress |
批量接收图片信息,下载原图并加入压缩队列,后台异步生成多档 webp | Bearer | image-compress |
| POST | /api/image/delete |
批量删除图片的全部压缩产物 | Bearer | image-compress |
| GET | /img/<date>/<md5>_<variant>.webp |
静态访问压缩后的图片(公开,可直接用于 <img>) |
无 | image-compress |
| POST | /api/auth/telegram/2fa |
Telegram 登录需要二次验证时提交密码 | Bearer | auth |
| POST | /api/telegram/dialogs/sync |
从 Telegram 拉取全量会话并同步到本地 DB | Bearer | telegram |
| GET | /api/telegram/dialogs |
获取启动时加载并在同步后刷新的全量 Telegram 会话缓存 | Bearer | telegram |
| POST | /api/telegram/history |
读取指定会话历史消息,返回完整消息快照和分页游标 | Bearer | telegram |
| GET | /api/telegram/history/preview/:chatId/:messageId |
读取历史消息图片/视频预览图(永久签名 URL) | 永久 HMAC 签名 | telegram |
| GET | /api/telegram/history/video/:chatId/:messageId |
按 Range 流式代理历史消息视频(永久签名 URL) | 永久 HMAC 签名 | telegram |
| POST | /api/telegram/forum/topics/list |
分页搜索 Telegram 论坛超级群话题 | Bearer | telegram |
| POST | /api/telegram/forum/topics |
创建 Telegram 论坛话题 | Bearer | telegram |
| POST | /api/telegram/send |
向 Telegram 会话发送文本和/或已下载媒体文件;文件可按 mediaItemId、analysisId 或来源消息引用,媒体进入 telegram.send 队列异步上传 |
Bearer | telegram |
| POST | /api/telegram/monitor |
添加需要监听的 Telegram 群组/频道 | Bearer | telegram |
| GET | /api/telegram/monitor?page=1&pageSize=20 |
分页获取监听列表 | Bearer | telegram |
| DELETE | /api/telegram/monitor/:id |
软删除监听项 | Bearer | telegram |
| GET | /api/queue/status?queueName=telegram.download |
获取队列 worker 和任务统计概览 | Bearer | queue |
| GET | /api/queue/jobs?status=FAILED&page=1&pageSize=20 |
分页获取队列任务列表,可按队列名和状态过滤 | Bearer | queue |
| POST | /api/queue/jobs |
停止或重试 Telegram 下载任务 | Bearer | queue |
| GET | /api/storage/entries?path=aaa&page=1&pageSize=100 |
分页读取固定根目录下当前层目录和文件 | Bearer | storage |
| GET | /api/storage/files/<path> |
按 Range 流式下载文件,URL 由列表接口限时签发 | 限时 HMAC 签名 | storage |
| WS | /ws |
WebSocket 连接,支持 Authorization: Bearer <AUTH_TOKEN> 或 ?token= |
Bearer/query token | ws |
图片压缩
POST /api/image/compress
请求体为数组(至少一项),逐张处理:
[
{
"md5": "86c94f9f7523185d852741aec0d46fbe",
"url": "https://lsky.example.com/uploads/20260628/xxx.jpg",
"filename": "avatar.jpg",
"pathname": "20260628/xxx.jpg"
}
]
每项:md5 必填(32 位)、url 必填(合法 URL)、filename 必填、pathname 可选。
响应 data 为数组,与请求一一对应,每项含 status:queued(已下载入队)/ already_queued(已在队列)/ completed(已压缩完成)。
产物写入:
storage/image-compress/<YYYYMMDD>/<md5>/<md5>_<variant>.webp
variant 当前为 small / medium / large。服务会校验 URL,禁止 localhost、环回地址和常见内网网段,防止 SSRF。
POST /api/image/delete
批量删除图片的全部压缩产物(每张对应整个 <date>/<md5>/ 目录)。请求体为数组(至少一项):
[
{
"md5": "86c94f9f7523185d852741aec0d46fbe",
"url": "https://lsky.example.com/uploads/20260628/xxx.jpg",
"pathname": "20260628/xxx.jpg"
}
]
每项:md5 必填(定位目录)、url 必填(从中推断日期目录)、pathname 可选。响应 data 为数组,与请求一一对应。幂等:目录不存在时该项返回 deleted: false,不报错。
GET /img/<date>/<md5>_<variant>.webp
静态访问压缩后的图片,公开无需鉴权,可直接用于 <img>。date 取自上传链接(如 20260628),variant 为 small / medium / large。例:
https://core.qflink.xyz/img/20260628/df59e5a859683xe480605aa5dcc32c0e_small.webp
固定根目录文件浏览
FILE_BROWSER_ROOT 指定服务端唯一允许浏览的根目录。列表接口不传 path 时只返回根目录的第一层普通目录;进入子目录后,前端将响应中的相对 path 原样传回,即可继续读取下一层目录和普通文件。隐藏条目、符号链接和其他特殊文件不会暴露。
curl "http://localhost:3000/api/storage/entries?path=aaa&page=1&pageSize=100" \
-H "Authorization: Bearer $AUTH_TOKEN"
文件条目会返回现有下载状态值 status: "DONE",表示它当前是可读普通文件;目录不返回 status。文件还会携带约 30–60 分钟有效的 downloadUrl。该 URL 支持完整下载和单段 HTTP Range,不需要额外携带 Bearer token。
Docker 中应将文件源以只读方式挂载到统一容器目录:
docker run \
-e FILE_BROWSER_ROOT=/app/files \
-v /host/aaa/aaa:/app/files/aaa:ro \
-v /host/ccc/bbb:/app/files/bbb:ro \
core
宿主机路径和一级目录名由各部署环境决定,不写入业务代码。
Telegram 用户运行时
服务启动后会异步初始化 Telegram 用户运行时:
TELEGRAM_USER_RUNTIME_ENABLED=false时跳过初始化。- 未配置
DATABASE_URL、TELEGRAM_API_ID或TELEGRAM_API_HASH时跳过初始化并记录日志。 - 按
TELEGRAM_SESSION_KEY从 MySQL 的tg_user_session恢复会话;未配置时依次使用APP_ENV、NODE_ENV,最后回退到local。 - 没有可用会话时发起二维码登录,日志里会输出可扫码的
previewUrl。 - 如果账号需要 2FA,优先读取
TELEGRAM_2FA_PASSWORD;没有该环境变量时,调用POST /api/auth/telegram/2fa提交密码。
TELEGRAM_SESSION_KEY 用于同一个数据库内隔离不同运行环境的 Telegram 账号,例如本地 .env 配 local,开发服务器配 development,两边会分别写入 tg_user_session.singletonKey = local/development,互不踢下线。TELEGRAM_DEVICE_MODEL、TELEGRAM_SYSTEM_VERSION、TELEGRAM_APP_VERSION 等会传给 mtcute 的 initConnectionOptions,用于 Telegram Active Sessions 中展示设备/客户端信息。
2FA 提交示例:
curl -X POST "http://localhost:3000/api/auth/telegram/2fa" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"password":"your-2fa-password"}'
登录成功后,会话会保存到 MySQL,后续启动自动恢复。
Telegram 会话与监听
同步会话:
curl -X POST "http://localhost:3000/api/telegram/dialogs/sync" \
-H "Authorization: Bearer $AUTH_TOKEN"
查询本地会话缓存:
curl "http://localhost:3000/api/telegram/dialogs" \
-H "Authorization: Bearer $AUTH_TOKEN"
读取会话历史消息:
curl -X POST "http://localhost:3000/api/telegram/history" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"chatId":-1001234567890,"pageSize":3}'
请求体中 chatId 支持 Telegram 标记 peer ID、@username、me 或 self;pageSize 默认 3,最大 15,按前端逻辑消息计数,一个媒体组只占一项。响应 data.nextCursor 非 null 时,下一页将它作为 cursor 原样传回;前端不解析 cursor,也不通过返回条数推断是否还有下一页。图片和视频消息会在 data.previews 中返回只携带 sig 的永久 HMAC URL;视频资源还会在 streamUrl 返回永久流地址。浏览器访问预览 URL 时,后端懒下载预览图并缓存到:
storage/telegram-preview/<chatId>/<messageId>.jpg
预览图缓存超过 24 小时会由后台定时清理。
永久 URL 只保证地址在 MEDIA_SIGNING_SECRET 不变时保持稳定;缓存被清理后仍会从 Telegram 重拉预览,视频也始终实时代理 Telegram。原消息删除、账号失去访问权限或 Telegram 运行时不可用时,永久 URL 仍可能无法读取。媒体管理页的 /api/telegram/media/items/:id/* URL 继续使用 expires + sig 限时签名。
Telegram 消息发送与论坛话题
chatId 支持数字标记 peer ID、@username、me 或 self。发送接口不传 topicId 时发到普通会话/群组;传 topicId 时发到论坛话题。话题管理接口要求目标 chatId 是 Telegram 论坛超级群,否则返回 400。
分页搜索话题:
curl -X POST "http://localhost:3000/api/telegram/forum/topics/list" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"chatId":-1001234567890,"query":"素材","limit":50}'
请求体字段:query 可选,limit 默认 100、最大 100,响应里的 data.next 可作为下一页 offset 原样传回。
创建话题:
curl -X POST "http://localhost:3000/api/telegram/forum/topics" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"chatId":-1001234567890,"title":"今日素材"}'
发送文本和/或已下载媒体文件。文本会在接口请求内同步发送;媒体文件会先展开 analysisId、sourceChatId + sourceMessageId 或 mediaItemId 引用,并校验本地文件,再拆成 telegram.send 队列任务异步上传:
curl -X POST "http://localhost:3000/api/telegram/send" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"chatId": -1001234567890,
"topicId": 123,
"text": "已整理完成",
"caption": "原始媒体",
"files": [
{
"mediaItemId": "cm456...",
"kind": "photo"
}
]
}'
也可以直接按媒体批次发送:
{
"chatId": "-1004351218176",
"topicId": 20,
"caption": "#绿光",
"files": [{ "analysisId": "cm123..." }]
}
不知道 analysisId 但知道触发消息时,也可以直接按来源消息发送:
{
"chatId": "-1004351218176",
"topicId": 20,
"caption": "#绿光",
"files": [
{
"sourceChatId": "-5202016150",
"sourceMessageId": 77983
}
]
}
也可以把文件浏览接口返回的相对路径作为 storagePath 发送。路径是文件时发送该文件,是目录时递归展开其中全部普通文件:
{
"chatId": "me",
"files": [
{ "storagePath": "aaa/photos" },
{ "storagePath": "bbb/video.mp4", "kind": "video" },
{ "storagePath": "bbb/read me.pdf", "kind": "document" }
]
}
topicId 可选,不传时发送到 chatId 对应的普通会话/群组,传入时发送到对应论坛话题。text 和 files 至少传一个。files[] 支持四种引用:mediaItemId 精准发送单个媒体项,analysisId 自动发送该批次内全部可上传文件,sourceChatId + sourceMessageId 自动查找来源批次,storagePath 发送固定浏览根目录下的文件或目录;不同来源可以混合。显式 files[] 最多 100 项,目录展开后的文件不截断;重叠的 storage 路径按第一次选择去重。kind 可选,支持 photo / video / document / audio / voice / sticker / animation,不传时按文件扩展名推断。目录引用的 caption 只应用到第一个展开文件;storage 的 fileName 仅在整个请求最终只有一个文件时生效;顶层 caption 写入第一个未设置 caption 的文件。单文件走单条媒体发送;多张图片/视频会按 Telegram 媒体组发送;混合类型按顺序逐条发送。
接口立即响应中的 data.uploadBatchId 用于追踪上传批次;data.jobs[] 保留 mediaItemIds 并新增 storagePaths。批次详情的条目同样通过 mediaItemId 或 storagePath 区分来源。storage 文件的上传进度和最终结果继续查询现有 send batch 接口,不会创建新的文件状态或 tg_media_item 记录。响应 data.mode 可能为 text / single / media_group / sequential / mixed。
添加监听群组/频道:
curl -X POST "http://localhost:3000/api/telegram/monitor" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"chatId":-1001234567890,"label":"素材群"}'
chatId 使用 Telegram 标记 peer ID(群组通常为负数,频道通常是 -100 前缀负数)。监听列表缓存在内存中,新增/删除后会同步更新缓存。
监听中的群组收到新消息后,系统会提取:
- 当前消息自身携带的媒体。
- 文本和 entities 中的 Telegram 消息链接,例如
https://t.me/c/<chat>/<message>或https://t.me/<username>/<message>。
命中后会创建 TgMediaAnalysis,入队 telegram.resolveMessageMedia。解析任务会用 mtcute 拉取链接指向的消息;如果是相册/媒体组,会通过 groupedId + getMessageGroup 展开整组,再为每个可下载媒体创建 telegram.downloadMedia。下载产物写入:
storage/telegram-media/<sourceChatId>/<sourceMessageId>/<suggestedFileName>
下载任务具备去重、重试、超时恢复和 Telegram flood wait 延迟重排能力。状态落库到 queue_job、tg_media_analysis、tg_media_item。
Queue 状态与任务控制
队列状态接口依赖数据库配置;本地开发需配置 DB_USER / DB_PASSWORD / DB_NAME,生产环境需配置 DATABASE_URL。
获取队列概览:
curl "http://localhost:3000/api/queue/status?queueName=telegram.download" \
-H "Authorization: Bearer $AUTH_TOKEN"
queueName 可选;不传时返回所有逻辑队列。Telegram 相关逻辑队列当前为 telegram.resolve、telegram.download、telegram.send。响应包含:
worker:worker 是否已启动、是否正在停止。totals:全局任务统计,包含active/runnable/delayed/running/done/failed/retrying。queues:每个逻辑队列的并发数、handler、当前运行任务 ID 和状态计数。
分页查询任务:
curl "http://localhost:3000/api/queue/jobs?queueName=telegram.download&status=FAILED&page=1&pageSize=20" \
-H "Authorization: Bearer $AUTH_TOKEN"
查询参数:queueName 可选;status 可选,支持 PENDING / RUNNING / DONE / FAILED;page 默认 1;pageSize 默认 20、最大 100。响应的 items[].payloadSummary 只暴露安全摘要,不返回完整 payload;Telegram 任务会额外带 business,包含媒体批次、媒体项和业务状态。
停止或重试下载任务:
curl -X POST "http://localhost:3000/api/queue/jobs" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{"id":"queue_job_id","action":"retry"}'
action 支持:
stop:仅支持telegram.downloadMedia任务。PENDING会直接标记为FAILED;RUNNING会尝试中断当前进程内的下载;DONE不能停止;已FAILED的任务返回changed: false。retry:支持FAILED或DONE的telegram.downloadMedia任务重新下载。接口会删除旧目标文件、重置队列任务为PENDING,并同步重置关联的tg_media_item/tg_media_analysis状态。
WebSocket
WebSocket 服务挂载在 /ws,握手时复用 AUTH_TOKEN:
ws://localhost:3000/ws?token=<AUTH_TOKEN>
或使用请求头:
Authorization: Bearer <AUTH_TOKEN>
出站消息格式:
{
"module": "system",
"event": "connected",
"data": {
"clientId": "..."
},
"ts": 1790000000000
}
客户端可发送 {"event":"ping"},服务端会返回 system/pong。
队列存在活跃任务时,服务端会每 5 秒广播一次队列状态:
{
"module": "queue",
"event": "status",
"data": {
"generatedAt": "2026-07-08T00:00:00.000Z",
"hasActiveJobs": true,
"worker": {
"started": true,
"stopping": false
},
"totals": {},
"queues": [],
"runningJobs": []
},
"ts": 1790000000000
}
当任务从活跃变为空闲时会补推一次空闲状态,方便前端及时收尾。
开发
pnpm install
cp .env.example .env # 填好 AUTH_TOKEN、MEDIA_SIGNING_SECRET、DATABASE_URL 等
pnpm dev # 热重载开发
pnpm build # prisma generate + 编译到 dist/ + tsc-alias 改写 #/ 别名
pnpm start # 运行编译产物
常用环境变量:
| 变量 | 说明 |
|---|---|
PORT |
HTTP/WS 服务端口,默认 3000 |
AUTH_TOKEN |
HTTP 接口和 WS 握手使用的 Bearer token,必填 |
MEDIA_SIGNING_SECRET |
媒体签名 URL 使用的 HMAC 密钥,必填 |
FILE_BROWSER_ROOT |
固定根目录文件浏览的服务端绝对路径;可选,未配置时跳过文件浏览初始化 |
DATABASE_URL |
MySQL 连接串;Prisma CLI / 迁移、生产运行时和 Telegram 运行时预检依赖它 |
DB_HOST |
本地开发运行时数据库主机,默认 127.0.0.1 |
DB_PORT |
本地开发运行时数据库端口,默认 3306 |
DB_USER |
本地开发运行时数据库用户名 |
DB_PASSWORD |
本地开发运行时数据库密码 |
DB_NAME |
本地开发运行时数据库名 |
TELEGRAM_USER_RUNTIME_ENABLED |
设为 false 可跳过 Telegram 用户运行时 |
TELEGRAM_SESSION_KEY |
Telegram 会话账号 key;同库多环境时建议设为 local、development 等 |
TELEGRAM_API_ID |
Telegram API ID |
TELEGRAM_API_HASH |
Telegram API Hash |
TELEGRAM_2FA_PASSWORD |
可选,无头环境下自动提交 Telegram 2FA 密码 |
TELEGRAM_DEVICE_MODEL |
可选,Telegram Active Sessions 展示的设备名,默认 Koa Core <sessionKey> |
TELEGRAM_SYSTEM_VERSION |
可选,Telegram Active Sessions 展示的系统版本,默认当前 Node 版本 |
TELEGRAM_APP_VERSION |
可选,Telegram Active Sessions 展示的客户端版本,默认 1.0.0 |
TELEGRAM_LANG_CODE |
可选,Telegram initConnection 的语言代码,默认 en |
TELEGRAM_SYSTEM_LANG_CODE |
可选,Telegram initConnection 的系统语言代码,默认跟随 TELEGRAM_LANG_CODE |
TELEGRAM_LANG_PACK |
可选,Telegram initConnection 的语言包,默认空字符串 |
启动前必须配置 AUTH_TOKEN 和 MEDIA_SIGNING_SECRET。FILE_BROWSER_ROOT 可选:未配置时服务可正常启动并跳过文件浏览;一旦配置则必须存在、可读、可遍历,否则启动失败。启用 Telegram 相关能力时还需要配置 DATABASE_URL、TELEGRAM_API_ID、TELEGRAM_API_HASH。本地开发运行时的 Prisma adapter 优先使用 DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME(并开启 allowPublicKeyRetrieval),缺失时回退 DATABASE_URL;生产优先 DATABASE_URL。
类型检查:
npx tsc --noEmit
Prisma(MySQL)
当前项目已使用 Prisma 对接 MySQL(prisma/schema.prisma 中 provider = "mysql"),Prisma Client 生成到 src/generated/prisma。
npx prisma generate
npx prisma migrate dev --name <migration_name>
npx prisma migrate deploy
DATABASE_URL 从 .env 读取,配置见 .env.example。
数据库结构变更由本地 Prisma 命令显式执行,不放进 Docker 构建、pnpm build、pnpm start 或 Gitea 部署流程。新增/修改 schema 后先生成迁移并应用到当前连接的数据库,例如:
npx prisma migrate dev --name add_xxx_table
已有迁移需要应用到目标数据库时执行:
npx prisma migrate deploy
当前主要数据表:
tg_user_session:Telegram 用户登录会话,按singletonKey区分不同运行环境账号。tg_dialog:Telegram 会话缓存。tg_monitored_chat:监听群组/频道配置。queue_job:数据库任务队列。tg_media_analysis:一次监听消息触发的媒体解析批次。tg_media_item:批次内单个媒体项及下载状态。
部署
Gitea workflow 会构建 Docker 镜像并部署到 vps_jp runner。容器端口为 3000,宿主机映射 127.0.0.1:33090。
运行时目录按功能挂载到宿主机 /app/core:
/app/core/logs -> /app/logs/app/core/storage -> /app/storage
新模块若产生需持久化的文件,统一写到 storage/<模块>/ 下,随 storage 卷持久化。当前包括 image-compress/、telegram-media/、telegram-preview/。
文件浏览根目录是只读输入,不属于上述运行时产物卷。部署时需另外设置 FILE_BROWSER_ROOT,并将需要浏览的宿主机目录以 :ro 分别挂载到该根目录的一级子目录。
架构与约定详见 AGENTS.md。