jiawei ebdd993b89
core / deploy (push) Successful in 24s
bugfix: sql err
2026-08-05 18:16:02 +08:00
2026-07-31 15:57:49 +08:00
2026-07-17 23:28:22 +08:00
2026-07-18 14:23:50 +08:00
2026-07-20 20:22:27 +08:00
2026-08-05 18:16:02 +08:00
2026-07-16 16:53:51 +08:00
2026-06-28 17:24:46 +08:00
2026-08-05 18:16:02 +08:00
2026-07-20 16:14:25 +08:00
2026-06-28 16:20:42 +08:00
2026-07-16 20:09:54 +08:00
2026-07-20 16:14:25 +08:00
2026-06-28 17:24:46 +08:00
2026-07-08 16:39:37 +08:00
2026-07-08 16:39:37 +08:00
2026-07-04 11:49:39 +08:00
2026-08-05 18:16:02 +08:00
2026-07-04 11:49:39 +08:00
2026-07-04 11:49:39 +08:00
2026-07-04 11:49:39 +08:00

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 会话发送文本和/或已下载媒体文件;文件可按 mediaItemIdanalysisId 或来源消息引用,媒体进入 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 为数组,与请求一一对应,每项含 statusqueued(已下载入队)/ 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),variantsmall / 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 用户运行时:

  1. TELEGRAM_USER_RUNTIME_ENABLED=false 时跳过初始化。
  2. 未配置 DATABASE_URLTELEGRAM_API_IDTELEGRAM_API_HASH 时跳过初始化并记录日志。
  3. TELEGRAM_SESSION_KEY 从 MySQL 的 tg_user_session 恢复会话;未配置时依次使用 APP_ENVNODE_ENV,最后回退到 local
  4. 没有可用会话时发起二维码登录,日志里会输出可扫码的 previewUrl
  5. 如果账号需要 2FA,优先读取 TELEGRAM_2FA_PASSWORD;没有该环境变量时,调用 POST /api/auth/telegram/2fa 提交密码。

TELEGRAM_SESSION_KEY 用于同一个数据库内隔离不同运行环境的 Telegram 账号,例如本地 .envlocal,开发服务器配 development,两边会分别写入 tg_user_session.singletonKey = local/development,互不踢下线。TELEGRAM_DEVICE_MODELTELEGRAM_SYSTEM_VERSIONTELEGRAM_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、@usernamemeselfpageSize 默认 3,最大 15,按前端逻辑消息计数,一个媒体组只占一项。响应 data.nextCursornull 时,下一页将它作为 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、@usernamemeself。发送接口不传 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":"今日素材"}'

发送文本和/或已下载媒体文件。文本会在接口请求内同步发送;媒体文件会先展开 analysisIdsourceChatId + sourceMessageIdmediaItemId 引用,并校验本地文件,再拆成 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 对应的普通会话/群组,传入时发送到对应论坛话题。textfiles 至少传一个。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。批次详情的条目同样通过 mediaItemIdstoragePath 区分来源。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_jobtg_media_analysistg_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.resolvetelegram.downloadtelegram.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 / FAILEDpage 默认 1pageSize 默认 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 会直接标记为 FAILEDRUNNING 会尝试中断当前进程内的下载;DONE 不能停止;已 FAILED 的任务返回 changed: false
  • retry:支持 FAILEDDONEtelegram.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;同库多环境时建议设为 localdevelopment
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_TOKENMEDIA_SIGNING_SECRETFILE_BROWSER_ROOT 可选:未配置时服务可正常启动并跳过文件浏览;一旦配置则必须存在、可读、可遍历,否则启动失败。启用 Telegram 相关能力时还需要配置 DATABASE_URLTELEGRAM_API_IDTELEGRAM_API_HASH。本地开发运行时的 Prisma adapter 优先使用 DB_HOST / DB_PORT / DB_USER / DB_PASSWORD / DB_NAME(并开启 allowPublicKeyRetrieval),缺失时回退 DATABASE_URL;生产优先 DATABASE_URL

类型检查:

npx tsc --noEmit

PrismaMySQL

当前项目已使用 Prisma 对接 MySQLprisma/schema.prismaprovider = "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 buildpnpm start 或 Gitea 部署流程。新增/修改 schema 后先生成迁移并应用到当前连接的数据库,例如:

npx prisma migrate dev --name add_xxx_table

已有迁移需要应用到目标数据库时执行:

npx prisma migrate deploy

当前主要数据表:

  • tg_user_sessionTelegram 用户登录会话,按 singletonKey 区分不同运行环境账号。
  • tg_dialogTelegram 会话缓存。
  • 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

S
Description
一个 vibecoding 统一后端接口,负责将平时的一些想法落地,扩展一些工具的功能
Readme 2.7 MiB
Languages
TypeScript 99.8%
JavaScript 0.1%