Technical Documentation: whyour/qinglong
ℹ️ Provenance: Multi-Tier Ingestion · CodeWiki Reference
Overview (`README.md`)
功能
- 支持多种脚本语言(python3、javaScript、shell、typescript)
- 支持在线管理脚本、环境变量、配置文件
- 支持在线查看任务日志
- 支持秒级任务设置
- 支持系统级通知
- 支持暗黑模式
- 支持手机端操作
- 支持通过 远程 CLI 与 Agent Skill 管理面板
版本
docker
latest 镜像是基于 alpine 构建,debian 镜像是基于 debian-slim 构建。如果需要使用 alpine 不支持的依赖,建议使用 debian 镜像
⚠️ 重要提示: 如果您需要以非 root 用户运行 Docker,请使用 debian 镜像。Alpine 的 crond 需要 root 权限。
docker pull whyour/qinglong:latest
docker pull whyour/qinglong:debian使用 debian 镜像以非 root 用户运行时,需指定 --user qinglong:
docker run -d \
-v /path/to/ql/data:/ql/data \
-p 5700:5700 \
--user qinglong \
--name qinglong \
whyour/qinglong:debiannpm
npm 版本支持 debian/ubuntu/alpine 系统,需要自行安装 node/npm/python3/pip3/pnpm
npm i @whyour/qinglong部署
内置 API
内置命令
远程 CLI 与 Agent Skill
使用独立 npm 包 @whyour/qinglong-cli,可以从本机终端或自动化流程管理远程青龙面板。支持任务与订阅的创建、修改、运行和日志查看,以及应用、环境变量、脚本、配置、依赖等 OpenAPI 资源;JSON 输出便于脚本和 AI Agent 使用。
需要 Node.js >=22.12,推荐 Node.js 24。安装后使用 ql 命令:
npm install @whyour/qinglong-cli
ql login --url https://ql.example.com
ql auth status --json
ql task list --json
ql subscription list --json
ql --help在面板「系统设置 → 应用设置」创建应用并授予所需权限,登录时按提示输入 Client ID/Secret;也支持通过 QL_URL 和 QL_ACCESS_TOKEN 使用已有令牌。各命令支持 --help,QL_LANG=en 可切换英文帮助。
npm CLI 用于远程管理。面板内置 ql 同样使用这个命令名,已有面板的机器请使用独立安装目录,避免覆盖内置命令。本机脚本执行和运维见 面板内部工具。
CLI 中文说明 · English guide · 完整命令参考
包内附带 远程管理 Skill,涵盖认证、命令选择和操作结果检查,可供 AI Agent 使用。本机执行与运维另见 内部 Skill。
开发
git clone https://github.com/whyour/qinglong.git
cd qinglong
cp .env.example .env
# 推荐使用 pnpm https://pnpm.io/zh/installation
npm install [email protected]
pnpm install
pnpm start打开你的浏览器,访问 http://127.0.0.1:5700
链接
名称来源
青龙,又名苍龙,在中国传统文化中是四象之一、天之四灵之一,根据五行学说,它是代表东方的灵兽,为青色的龙,五行属木,代表的季节是春季,八卦主震。苍龙与应龙一样,都是身具羽翼。《张果星宗》称“又有辅翼,方为真龙”。
《后汉书·律历志下》记载:日周于天,一寒一暑,四时备成,万物毕改,摄提迁次,青龙移辰,谓之岁。
在中国二十八宿中,青龙是东方七宿(角、亢、氐、房、心、尾、箕)的总称。 在早期星宿信仰中,祂是最尊贵的天神。 但被道教信仰吸纳入其神系后,神格大跌,道教将其称为“孟章”,在不同的道经中有“帝君”、“圣将”、“神将”和“捕鬼将”等称呼,与白虎监兵神君一起,是道教的护卫天神。
Overview (`cli/README.md`)
QingLong 2.x 远程管理 CLI
简体中文 | English
npm 与面板内部入口都叫 ql,但使用独立的 Commander 命令树:npm 入口只调用远程 API,内部入口只运行本机工具。使用前确认可执行文件的绝对路径和 --help;安装 npm 包不会迁移内置 Shell 命令。
@whyour/qinglong-cli 是独立 npm 包,覆盖当前 develop 的有效 OpenAPI,只注册一个 ql 命令。它不包含脚本执行器或本机运维实现;task exec、repo/raw、reload/update/reset* 等属于面板内部工具,不随 npm 包分发。开发发布也不属于 CLI 范围。
安装与使用
要求 Node >=22.12,推荐 Node 24。Commander 在构建时打包,无额外运行时 npm 依赖,不单独发布 CLI 镜像。
npm install @whyour/qinglong-cli
ql --help
# 临时使用,不覆盖面板已有的 ql
npm exec --package=@whyour/qinglong-cli -- ql --help也可以从源码构建:执行 npm ci --prefix cli、npm run build:cli,在 cli 目录执行 npm pack,然后安装本地 tgz。全局安装会占用 ql 名称;已有面板的机器建议使用独立 npm prefix 或直接执行 node /absolute/path/to/cli/dist/npm/ql.js。
ql login --url https://ql.example.com
ql auth status --json
ql task list --search 示例 --page 1 --size 50 --json
ql task get 12 --json
ql task logs 12 --tail 200 --json
ql task run 12 --json
ql task stop 12 --json
ql auth status --scope subscriptions --json
ql subscription list --json
ql subscription get 5 --json
ql subscription run 5 --json
ql subscription stop 5 --json
ql subscription logs 5 --tail 200 --json
ql subscription enable 5 --json
ql subscription disable 5 --json
ql auth logout --json各命令支持 --help,QL_LANG=en 切换英文帮助;命令和 JSON 字段不随语言变化。ql task 中只包含 API 操作,不会把无效命令解释为本机脚本,也不提供独立 task npm 入口。
全量 OpenAPI 管理
现在还支持任务/订阅创建、修改、删除,应用管理与密钥重置,以及环境变量、配置、脚本、日志、依赖、系统、仪表盘和用户管理。ql api routes --json 列出全部 143 条有效路由;3 条已下线文件读取接口不包含在内。新增命令在 完整双语参考 中逐项列出,路由覆盖由 CI 与后端代码核对。
ql task create --name demo --command 'task demo.js' --schedule '0 0 * * *' --json
ql subscription create --type public-repo --url https://example.com/repo.git --alias demo --schedule-type crontab --schedule '0 0 * * *' --json
ql app create --name agent --scopes crons,subscriptions --show-secrets --json
ql env create --data @envs.json --json
ql api request PUT /open/crons/run --data '[12,13]' --json请求体使用 --data JSON/@file/-,查询使用 --query,上传 --file,下载 --output。新命令支持 --timeout 秒数;旧命令行为保留,完整参数可用 api request。下载不覆盖现有文件,应用密钥默认隐藏,明确加 --show-secrets 才输出。新增资源通常保留原始返回字段,注意环境、配置和会话信息可能敏感。
远程 ql system ... 调用面板 API;本机 reload/reset 等仍不在 npm 包中。不要将远程 API 覆盖理解为本机运维重新混入包。
认证
受保护的请求均使用 Authorization: Bearer <token>,支持以下两种凭据来源:
| 方式 | 提供方式 | 保存与刷新 |
|---|---|---|
| 应用凭据登录 | ql login --url <面板地址>,输入 Client ID / Client Secret;自动化通过 QL_CLIENT_ID、QL_CLIENT_SECRET 注入 |
通过 /open/auth/token 换取 token,凭据和 token 保存到本机,过期自动刷新 |
| 直接访问令牌 | 环境中同时设置 QL_URL、QL_ACCESS_TOKEN,支持有效应用 token 或面板会话 token |
优先于保存的配置,不落盘、不自动刷新 |
应用凭据登录
在面板创建专用应用,按需授予 crons、subscriptions、envs 等权限。login 与 auth login 等价;交互输入 Client ID 和不回显的 Client Secret,密钥不支持命令行参数。
ql login --url https://ql.example.com
ql auth status --scope crons --json
# 自动化环境事先注入 QL_CLIENT_ID 和 QL_CLIENT_SECRET,再运行同一 login 命令凭据和 token 保存到 ~/.config/qinglong/cli.json,为当前用户所有的 0600 明文文件。QL_CLI_CONFIG 可选择其他配置文件。登录只验证应用凭据,不意味着具备所有资源权限。
直接使用访问令牌
在终端或 CI 的凭据配置中注入 QL_URL 与 QL_ACCESS_TOKEN 后,直接调用命令,无需再执行 login:
ql auth status --scope apps --json
ql app list --json两个变量必须同时提供给受保护命令;只提供一个会报参数错误。此方式优先于 QL_CLI_CONFIG 指定的配置。即使重新执行 login 写入了应用配置,后续请求仍使用环境令牌;切回保存的应用配置时,在自己的终端执行 unset QL_URL QL_ACCESS_TOKEN。
应用管理需要 apps 权限或有效的授权面板会话;当前面板 UI 没有列出所有后端 scope,CLI 不自动提权。匿名 user login 可在仅设置 QL_URL 时使用 --data @credentials.json 或 --data - 提交凭据;返回的会话不会自动保存。启用双因素认证时,服务端 420 对应 CLI 退出码 3,随后使用 user two-factor-login,请求体包含 username/password/code。不要将凭据写入命令参数或聊天。
权限检查、退出与失败
auth status 默认检查 crons 读取权限,可用 --scope subscriptions、--scope apps 等检查对应资源。它只验证代表性读取,不代表所有写操作都获授权。2.x 的 crons scope 同时覆盖读取和执行。
auth logout 仅删除本机应用配置,不撤销服务端 token,也不清除父进程中的 QL_ACCESS_TOKEN。令牌模式仍可能继续访问;如需撤销访问,使用面板提供的应用或会话管理。重置应用密钥会使旧应用 token 失效,需要使用新密钥重新 login。
URL 使用面板根地址,可包含代理路径前缀,不附加 /open。远程连接要求 HTTPS,回环地址允许 HTTP;不跟随重定向。2.x 应用 token 接口以查询参数传递凭据,代理应避免记录该查询字符串。401/403 不自动重放操作;直接令牌失效时需更换,CLI 不会转而使用保存的应用凭据。
输出契约
订阅支持 create/update/delete/list/get/run/stop/logs/enable/disable/status/log-files。subscription list/get 保留管理字段投影;run/stop/enable/disable 返回 subscriptionId/action/accepted。创建、修改、删除及通用 API 请求保留服务端响应,可能包含敏感字段;不能将所有变更都解释为 accepted。
命令默认输出缩进 JSON,--json 输出单行 JSON;成功写 stdout,错误写 stderr,互不混用。帮助在 --json 下也使用 {code:200,data:{help:"..."}}。
task list:{code:200,data:{data:[...tasks],total:123}},默认每页 50,最多 200;任务字段保留服务端内容。task get:{code:200,data:{id:12,...}}。task logs:{code:200,data:"日志尾部",logStatus:"completed",truncated:true}。默认 200 行,最多 10000;旧接口若不返回 logStatus,该字段省略。task run/stop:{code:200,data:{taskId:12,action:"run",accepted:true}}。仅表示请求被接受,不表示任务成功完成。- 错误:
{code:1,message:"..."};退出码 1 为 API/网络/配置错误,2 为参数错误,3 为未登录、HTTP/API 401/403 或需要双因素验证;成功退出码 0。
ID 必须为正整数,运行/停止一次操作一个任务。2.x 没有为此提供独立运行 ID 或幂等键,失败响应可能意味着执行结果未知,应先查询状态,禁止盲目重试。CLI 不自动重试 HTTP 请求。
日志是该任务最新日志,可能属于先前运行;completed 不代表成功。--tail 在客户端截取,不减少服务端读取量或网络传输量。日志与任务字段不自动脱敏,应按需读取并避免向聊天中暴露敏感信息。
Skill、构建与验证
npm 包附带 skills/qinglong-cli,覆盖全部远程命令。复制到所用 Agent 的 skills 目录,并确认使用 npm 远程入口。Skill 不存放凭据,也不替代服务端权限。
源码使用 TypeScript 严格检查和 Commander 解析;共用参数/输出逻辑,分别构建远程入口和面板内部入口。dist/npm/ql.js 是可独立运行的远程 bundle,构建依赖图会拒绝引入本机运维模块;npm 文件白名单仅包含该 bundle、source map、许可证、中英文说明和远程 Skill。完整 dist 用于面板内部构建,不随 npm 发布。
npm ci --prefix cli
npm run check:cli
npm run test:cli
node cli/scripts/verify-package.cjs测试覆盖请求格式、认证刷新、输出、错误、禁止自动重试、权限和独立安装。打包验证会离线安装 tgz,并确认唯一入口是 ql,本机命令不可调用。
CLI package 工作流在相关 PR、develop/master 推送和手动触发时执行 Node 22.12/24 类型检查、构建及测试。Node 24 上传通过离线安装验证的 qinglong-cli-<commit> artifact。两个矩阵任务成功后,master 推送会将这份已验证的 tgz 发布到 npm 的 latest 标签,通过 GitHub Actions OIDC 可信发布,无需 NPM_TOKEN Secret。手动运行需选择 master 并勾选 publish;PR、develop 和 fork 不发布。npm 的 Trusted Publisher 需绑定仓库 whyour/qinglong 和工作流 cli-package.yml,允许 npm publish;面板包 @whyour/qinglong 单独绑定 build-docker-image.yml。发布 job 使用 Node 24,并仅在该 job 授予 id-token: write。新包需先完成首次发布,再配置对应包的可信发布关系。
CLI 版本独立维护在 cli/package.json 和 cli/package-lock.json。发布改动前执行 npm version patch --prefix cli --no-git-tag-version(也可用 minor/major),提交这两个文件。已发布的版本会提示并跳过;registry 查询失败则停止发布。该流程仅发布 X.Y.Z 稳定版本,不重新构建产物或执行包生命周期脚本。
面板内部工具
本机执行、订阅同步和运维随面板源码/构建交付,使用完整内部 dist 与独立 qinglong-local Skill;源码说明见 cli/LOCAL.md 和 cli/LOCAL.en.md。npm 包不能用作 QL_CLI_ROOT。账号恢复、服务重载必须在实际面板宿主机或容器中执行;Docker 使用 docker exec 调用容器内选定入口。
API task run 返回请求接受,本机执行器等待脚本结束,二者耗时不能直接对比。Shell 迁移性能应比较相同配置和脚本下的内部 TS 执行器与原 Shell;远程 API 操作没有对应的旧 Shell 管理命令。
源码结构
src/
entrypoints/ # 可执行入口组装
remote/ # commands / api / auth
internal/ # commands / execution / subscription / maintenance / runtime / integration
compatibility/ # 旧入口和参数适配
shared/ # cli / i18n / 响应类型与错误shared 不能引用业务模块;remote 与 internal 只能引用各自区域及 shared,集成测试检查这些依赖边界。两套业务注册表分别注入共享 Commander 解析器。
测试按同样的区域分组。仓库根目录 npm run test:cli 或 cli/ 下 npm test 自动发现标准测试;test/linux 保持为显式执行的容器集成测试。scripts/entrypoints.cjs 保留现有 dist 可执行入口,以及 dist/local/entrypoints.js、cronEntrypoint.js;移动源码不要求修改 QL_CLI_ROOT 或 cron 命令。
Overview (`deploy/kubernetes/README.md`)
Kubernetes deployment
This deploys Qinglong as a single-replica StatefulSet with persistent data at /ql/data.
kubectl apply -k deploy/kubernetes/overlays/local
kubectl -n qinglong rollout status statefulset/qinglongOpen the panel locally:
kubectl -n qinglong port-forward svc/qinglong 5700:5700Then visit http://127.0.0.1:5700.
Image registry overlays
Use overlays/example as the committed template for registry customization:
whyour/qinglong:debian -> registry.example.com/whyour/qinglong:debianCreate overlays/local/kustomization.yaml for the actual cluster image. The local overlay is ignored by git so private registry names, digests, and credentials-related references stay local.
Storage
The manifest creates a 5 GiB ReadWriteOnce PVC from the cluster's default StorageClass.
If your cluster has no default storage class, add storageClassName under:
volumeClaimTemplates:
- metadata:
name: data
spec:
storageClassName: your-storage-classKeep replicas: 1. Qinglong stores state in the persistent data directory, including SQLite files, so multiple replicas should not share the same data volume.
Ingress example
If you expose Qinglong through an Ingress path other than /, set QlBaseUrl to the same path with leading and trailing slashes.
env:
- name: QlBaseUrl
value: "/qinglong/"Example Ingress:
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: qinglong
namespace: qinglong
spec:
rules:
- host: qinglong.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: qinglong
port:
number: 5700Maintenance commands
kubectl -n qinglong logs -f statefulset/qinglong
kubectl -n qinglong exec -it statefulset/qinglong -- ql check
kubectl -n qinglong exec -it statefulset/qinglong -- ql updateOverview (`cli/test/linux/README.md`)
Linux CLI regression runtime
Build the CLI on the host first (npm run build:cli), then build this test-only image:
docker build -t qinglong-cli-test:node22-debian cli/test/linux
docker run --rm --network none \
--mount type=bind,src="$(pwd)/cli",dst=/workspace/cli,readonly \
--mount type=bind,src="$(pwd)/shell",dst=/workspace/shell,readonly \
--mount type=bind,src="$(pwd)/back/api",dst=/workspace/back/api,readonly \
--workdir /workspace qinglong-cli-test:node22-debianThe image supplies Linux interpreters, Git and archive tools. It does not mount host node_modules, credentials, the Docker socket or production data. Tests use temporary installations and loopback API fixtures; the runtime has no external network. Package installation verification runs offline after the test suite. No installed ql/task command is replaced. The original Shell is mounted read-only for differential tests.
Image construction needs network access. If Docker injects a stale proxy, override it for this build only with empty HTTP_PROXY, HTTPS_PROXY, ALL_PROXY and their lowercase build arguments; do not change global proxy settings as part of the test.
The Node base image digest and TypeScript interpreter versions are fixed. Debian package revisions follow the Bookworm repositories at build time. Passing this suite does not prove real panel authentication/database persistence, nginx/PM2 deployment, or compatibility of all user scripts.
For Alpine/musl, use the second pinned base and the same read-only runtime mounts:
docker build -f cli/test/linux/Dockerfile.alpine -t qinglong-cli-test:node24-alpine cli/test/linux
docker run --rm --network none \
--mount type=bind,src="$(pwd)/cli",dst=/workspace/cli,readonly \
--mount type=bind,src="$(pwd)/shell",dst=/workspace/shell,readonly \
--mount type=bind,src="$(pwd)/back/api",dst=/workspace/back/api,readonly \
--workdir /workspace qinglong-cli-test:node24-alpineThis includes Bash/coreutils, as does the panel's Alpine Dockerfile; it is not a bare BusyBox compatibility claim. Python comes from the pinned base's Alpine repository, and package patch revisions remain build-time dependent.
Real service manager integration
After building the Debian test image:
docker build -f cli/test/linux/Dockerfile.services -t qinglong-cli-test:services cli/test/linux
docker run --rm --network none \
--mount type=bind,src="$(pwd)/cli",dst=/workspace/cli,readonly \
--workdir /workspace qinglong-cli-test:servicesThis separate test installs nginx and PM2 5.4.3 in the image. It invokes bootstrap reload twice with an isolated PM2_HOME and nginx config, checks proxy responses/config replacement/backend PID change, and verifies stopPanel removes the PM2 application. Cleanup quits nginx and kills the test PM2 daemon before the disposable container is removed. Ports 5801/5802 remain container-local. The backend is a minimal HTTP fixture, not a real QingLong application/database. No OS boot integration (pm2 startup) or first-time online dependency provisioning is claimed by this test.
Released 2.x panel integration
panel.cjs is for a fresh disposable official panel only. It refuses initialized instances, generates random temporary owner/application credentials without printing them, exercises API login/status/subscription reads and a scheduled task run, then invokes the new local task engine against the real token generator and verifies persisted log_path/execution time. It does not replace installed Shell entrypoints. The fixture script exits 7 in the local-runner step.
Run the official whyour/qinglong:2.20.1@sha256:4e96d821494cfbeddd29f5a46dfb006a5a64f5639e0b8665816fcf55f39a85ea image with --network none, no published ports, and this CLI directory mounted read-only at /candidate/cli. Clear inherited proxy variables for the test container. After its startup log reports readiness, execute:
docker exec -e QL_PANEL_INTEGRATION=1 <temporary-container-id> node /candidate/cli/test/linux/panel.cjsAlways remove that temporary container and its anonymous volume with docker rm -fv <temporary-container-id> after the run, including failures. Never point this test at an existing initialized panel. The version is explicit because the current workspace's backend build also contains independent 3.0 changes; this gate tests the public 2.x compatibility contract without pulling 3.0 work into the migration.
Interrupted replacement with real services
Using the services image above, run:
docker run --rm --network none \
--mount type=bind,src="$(pwd)/cli",dst=/workspace/cli,readonly \
--workdir /workspace qinglong-cli-test:services \
node --test cli/test/linux/upgrade.test.cjsThis test covers direct Node and PM2 separately. It starts the old HTTP fixture, invokes replaceAndReload in a child process, waits until the new HTTP fixture responds, sends SIGTERM to the upgrade child, then checks exit 143, old file/HTTP restoration, disappearance of the new PID, and backup cleanup. The child fixture inserts a deterministic wait after real startup so the signal cannot race past the rollback boundary. Test services and PM2_HOME are temporary; port 5811 is container-local. It exercises the real service functions and filesystem replacement but not QingLong database migrations, archive downloads, or interruption during a package install.
The optional QL_PANEL_ENTRYPOINTS=1 environment flag on the same fresh-panel command enables a test-only reversible switch of the official release's ~/bin/task and ~/bin/ql symlinks. Original Shell targets remain on disk, and the links are restored in finally. It additionally schedules ql extra and checks persisted log metadata. The corrected gate passed on official 2.20.1 after legacy status-field selection and scheduler log-path reuse were added; retain the run output as evaluation evidence. It covers task and ql extra scheduling, not every legacy command.
With entry switching enabled, panel.cjs also runs panel-subscription.cjs: a loopback file subscription downloads through the new worker, creates one task with its subscription ID, then executes that task through the panel and checks its output. The official 2.20.1 gate passed on 2026-09-25. Notification channels are intentionally unconfigured; their rejection must produce a diagnostic without failing an otherwise completed synchronization. Repository subscriptions are outside this particular gate.
The switched-entrypoint fixture also includes panel-repository.cjs. It creates a local file:// Git repository with a main and selected branch, include/exclude patterns, a shared dependency, and two revisions. Assertions check the selected branch, exclusion, dependency execution, stable ID for an unchanged task, addition/removal after the second pull, and removal of the obsolete script. The source is local because the official image lacks git-daemon. This gate does not establish network Git transport, proxies or private repository authentication.
The switched-entrypoint gate also runs panel-account.cjs at the end. It changes only the freshly generated test owner's username/password through ql, verifies new login credentials, activates login retry/second-factor requirements via the test owner's API, and verifies resetlet/resettfa restore access. It never prints credentials and must only run inside the disposable panel guarded by panel.cjs. The account and its data volume are removed with the container.
The expanded switched-entrypoint gate now includes panel-operator.cjs for real log retention and full-panel service reload. Without the candidate loader, the original 2.20.1 startup loader overwrites temporary Node wrappers with Shell links. Use the candidate-loader gate below to verify persistent entry selection.
Persistent entrypoint selection gate
The loader integration regression can be run on the development host with node --test cli/test/linux/entrypoints.test.cjs; it needs the repository TypeScript dev dependency and reads the actual back/loaders/deps.ts. It verifies both commands, exact args/exit codes, paths with spaces, repeated startup selection, invalid/incomplete installations and return to original Shell.
For the real-panel gate, transpile only back/loaders/deps.ts to a temporary CommonJS file using the repository TypeScript transpileModule (CommonJS, esModuleInterop, ES2022). Mount that file read-only as /candidate/deps.js alongside the CLI mount and start the disposable official image with QL_CLI_ROOT=/candidate/cli. Add QL_PANEL_LOADER=1 to the panel.cjs docker exec environment. The guarded fixture backs up the installed compiled loader, copies this single candidate loader, exercises reload, and restores the compiled loader and command links in finally. No full workspace backend/3.0 build is used. The updated gate passed with operatorMaintenance=true on 2026-09-25; without the candidate loader the expected old-entry overwrite remains reproducible.
Online check/repair gate
Use a fresh disposable official 2.20.1 container with the CLI read-only mount, no published ports/user data, and normal container networking (unlike the offline gates). Clear inherited proxy environment variables. Execute node /candidate/cli/dist/admin.js check --root /ql --json with npm_config_registry=https://registry.npmjs.org, npm_config_fetch_retries=0 and npm_config_fetch_timeout=30000, redirecting stdout to /tmp/check-result.json. This performs real package downloads and container-local installs.
For the repair scenario, append a fixture comment to /ql/data/config/config.sh, copy it to /tmp/expected-config.sh, delete /ql/data/config/task_before.sh, and overwrite /ql/data/scripts/sendNotify.js with a fixture string. Run the same check again, redirecting stdout to /tmp/check-repair-result.json. Then execute QL_PANEL_INTEGRATION=1 node /candidate/cli/test/linux/verify-check.cjs inside that container. Always remove the container and anonymous volumes afterward. The official image and registry dependencies can change; retain the exact image digest and resulting tool versions with the evidence. Never run this damage fixture on an existing panel.
First-start container gate
Start a fresh official image with --entrypoint sleep, arguments infinity, explicit QL_DIR=/ql and QL_DATA_DIR=/ql/data, and the read-only CLI mount. Allow package network access and clear inherited proxy variables as in the online check gate. This bypasses the legacy container entrypoint. Run node /candidate/cli/dist/admin.js start --root /ql --data-dir /ql/data --no-startup --json > /tmp/start-result.json inside the container, then QL_PANEL_INTEGRATION=1 node /candidate/cli/test/linux/verify-start.cjs. Use the same bounded npm registry environment as online check. Remove the container and anonymous volumes afterward.
--no-startup explicitly skips OS boot registration for containers lacking init, while PM2 save still runs. This does not validate host reboot behavior. The official image already provides Node/Python/panel dependencies; the gate verifies migration of startup orchestration, not provisioning an empty OS.
Online Bot installation/process gate
Use a fresh official 2.20.1 container with --entrypoint sleep ... infinity, normal container networking, cleared proxy variables and the read-only CLI mount. Run QL_PANEL_INTEGRATION=1 node /candidate/cli/test/linux/bot-install.cjs inside it. The fixture creates a local Git repository, performs real apk/pip installation, starts two Python test bots in separate data directories, replaces one and verifies configuration/process isolation, then stops/cleans them. It sends no Telegram traffic and uses no bot credentials. Remove the disposable container and anonymous volumes afterward. The dependency fixture pins colorama 0.4.6; OS packages follow the image's configured repositories.
The network repository regression uses a loopback smart-HTTP Git service, random test credentials and a loopback HTTP proxy. It requires git-http-backend; Alpine packages it in git-daemon, included only in this test image. No external repository or credential is used, and the runtime still works with --network none.
The same regression also runs a TLS smart-HTTP service and an actual HTTP CONNECT tunnel. OpenSSL generates a temporary self-signed certificate with an IP SAN; Git receives it through GIT_SSL_CAINFO only for the trusted case. Removing trust must fail, as must HTTP 401/503, while preserving installed scripts and checkout HEAD. The Alpine test image includes the openssl executable for this fixture; no runtime CLI dependency or certificate-verification bypass is introduced.
Isolated SSH subscription gate
Build cli/test/linux/Dockerfile.ssh as qinglong-cli-test:ssh after the Alpine test image. Run only in a fresh disposable container:
docker run --rm --init --network none -e QL_SSH_INTEGRATION=1 \
--mount type=bind,src="$(pwd)/cli",dst=/workspace/cli,readonly \
qinglong-cli-test:ssh node --test cli/test/linux/ssh-repository.test.cjsThis root-only fixture creates a container-local account, host/client keys and repository. It starts sshd on loopback port 22022 with password authentication and forwarding disabled, then verifies both SSH URL and scp-style Git addresses. StrictHostKeyChecking and IdentitiesOnly remain enabled. Wrong identities and mismatched known_hosts must fail without changing scripts or checkout HEAD. The fixture does not mount host SSH configuration or keys. Its temporary authorization files live below /var/lib because sshd StrictModes rejects the world-writable /tmp ancestor. Never run this opt-in fixture directly on a host: account removal is provided by disposable-container deletion.
Published upgrade archive gate
Run QL_ARCHIVE_INTEGRATION=1 node --test cli/test/linux/published-archive.test.cjs with network access and current dist. Set QL_ARCHIVE_MIRROR=gitee to exercise that explicit mirror instead of GitHub. The gate calls stageUpgrade against real master source/static ZIP URLs and validates paths, expected app entry, readiness marker and selected-stage pointer. It records archive hashes and version for reproducibility because master can change.
Dependency installation is intercepted: downloaded source is never executed, and no installed panel is reloaded. The existing package-install and retained-data upgrade gates provide separate evidence for those operations. This gate uses temporary directories, bounds download work with cancellation at 150 seconds and removes staging afterward. External archive availability is a prerequisite; connection failures must not be reported as successful transport validation.
Data reload with a real mount root and a live Node backend has a separate disposable gate:
docker run --rm --network none --tmpfs /mounted-data -e QL_MOUNT_TEST=1 \
--mount type=bind,src="$(pwd)/cli",dst=/workspace/cli,readonly \
qinglong-cli-test:node24-alpine node --test cli/test/linux/mounted-data.test.cjsThe fixture requires an empty separate filesystem at /mounted-data, never a production data mount. It verifies the mount inode/device remain unchanged, obsolete data is removed, staged data is installed and the restarted backend serves the new data.
Container startup-hook ownership
docker run --rm --init --network none --user 65534:65534 \
--mount type=bind,src="$(pwd)/cli",dst=/workspace/cli,readonly \
qinglong-cli-test:node24-alpine node --test \
cli/test/containerRuntime.test.cjs cli/test/linux/container-bot.test.cjsThe bot fixture runs the actual detached admin installer and Python launcher against a local module with no Telegram code. OS/pip provisioning commands are fixture stubs; the separate online gate above validates dependency installation. It waits for the installer to exit while Python remains alive, stops the container runtime, and verifies that only its bot exits while another installation's bot stays alive. All fixture processes are cleaned, including on assertion failures. --init reaps adopted orphans.
Real container crond gate
Start a fresh opt-in 2.20.1 image using the TS container entry with --init, --stop-timeout 30 and QL_SCHEDULER=system. Rebuild the evaluation image with the current CLI and mount only cli/test read-only at /opt/qinglong-cli/test. Do not overlay cli or dist: the source tree lacks image-generated bin links and local tsc output lacks the executable modes assigned during image packaging. No network or published port is required. Run QL_PANEL_INTEGRATION=1 node /opt/qinglong-cli/test/linux/container-cron.cjs with docker exec. It creates one minute-scheduled task through the local API, checks the installed system crontab and waits up to ninety seconds for a real minute tick. It never calls the task run API. The task records its process ancestry; the gate requires real crond, the selected Node task entry (verified wrapper or symlink target) and the TS container entry, and reads output through the panel log API. Task and script are removed in a finalizer. Stop the container, inspect its exit code and remove it with its anonymous volume afterward.
Packaged language preload and service reload gate
Mount only cli/test at /opt/qinglong-cli/test in a fresh packaged 2.20.1 evaluation container. Start it through dist/container.js with QL_PANEL_INTEGRATION=1, --init and a 30-second stop timeout. Run panel.cjs first on the uninitialized panel for application authentication and API task/log checks. Then run node /opt/qinglong-cli/test/linux/panel-preload.cjs inside that container; set QL_PANEL_RELOAD_INTEGRATION=1 to additionally invoke the packaged local service reload after creating the first task and verify it survives. The gate schedules JS/MJS/Python through the real API, checks Shell before-hook exports and actual QLAPI, and imports a temporary global ESM exported subpath. It restores the hook and removes tasks/scripts/package in cleanup.
Use the image's ordinary seeded data volume. An empty tmpfs or host bind mount over /ql/data masks image-preinstalled requests under dep_cache/python3; that is not a complete interpreter environment. When explicitly evaluating tmpfs, seed the fixed image's preinstalled dependencies into the live mount and verify python3 -c 'import requests' before this gate. The recorded run used an offline copy from the same image, not pip installation or stub notification modules. Docker cp did not materialize writes in the active tmpfs in this environment; extraction by tar inside the running container did. Keep this prerequisite distinct from the separate empty-OS provisioning requirement.
Real host reboot gate
host-boot.cjs runs only as root with QL_HOST_INTEGRATION=1 inside a disposable Alpine/OpenRC or Debian/systemd VM. Prepare a 2.x panel distribution plus the CLI, with Bash/Node/npm/Python/pip prerequisite runtimes. Run the actual ql-local-cli start --root /ql and require a successful registration result. Copy the test directory alongside the CLI dist directory, then run node test/linux/host-boot.cjs before, reboot the guest, and run the same script with after once SSH returns. Never run this on a production host.
The before phase verifies live nginx/crond, creates a minute-scheduled task and saves the kernel boot ID in a private fixture state file. The after phase checks OpenRC or systemd service state, requires a different boot ID, checks the stored task command and waits up to ninety seconds for a task log containing the new boot ID. It does not call the task run API. Success removes the task, script and state. On failure, retain the isolated guest for diagnosis or destroy it; do not cite PM2-only recovery as proof of nginx/cron recovery. The task fixture and script paths are under /ql, and the guest state file is /var/lib/ql-host-fixture.json.
Remote OpenAPI CRUD gate
openapi.cjs requires a separate fresh official panel and QL_PANEL_INTEGRATION=1; it refuses initialized panels. Mount cli read-only at /candidate/cli, run the official image without published ports or production data, then execute docker exec -e QL_PANEL_INTEGRATION=1 <container> node /candidate/cli/test/linux/openapi.cjs. It creates temporary owner/application credentials without printing them and verifies task/subscription/app/env CRUD, app secret rotation, script/config writes and reads, and log/dependency reads through the npm bundle. It does not execute system updates, data import, real dependency installation or every dashboard/user mutation. Always remove the disposable container and anonymous volumes with docker rm -fv <container>.
PROJECT ARCHITECTURE (`docs/PROJECT_ARCHITECTURE.md`)
Project Architecture Guide
This document is written for AI coding agents and maintainers who need to understand and modify this project safely. It focuses on where behavior lives, how the application starts, and which files are usually involved for common changes.
Project Summary
Qinglong is a timed task management platform. It provides a web admin panel for managing cron jobs, scripts, environment variables, subscriptions, dependencies, logs, configuration files, and system settings.
The repository is organized as a full-stack TypeScript application:
src/: frontend admin panel, built with Umi Max, React, Ant Design, and Ant Design Pro Layout.back/: backend application, built with Express, TypeScript, typedi, Sequelize, SQLite, gRPC, and worker processes.shell/: runtime shell scripts used to execute tasks and preload task environments.data/: local runtime data, including scripts, logs, configs, SQLite database, uploaded files, and cloned repositories.static/: built frontend and backend artifacts.docker/: Docker images, compose file, and entrypoint.sample/: sample scripts and default config templates.
High-Level Runtime Flow
Browser
-> src/pages/*
-> src/utils/http.tsx
-> /api/*
-> back/api/*
-> back/services/*
-> back/data/* Sequelize models
-> data/db/database.sqlite
Cron/task execution
-> back/services/cron.ts
-> shell/task.sh or shell/otask.sh
-> data/scripts/*
-> data/log/*
Frontend assets in production
-> static/dist/*
-> served by back/loaders/express.tsMain Startup Path
Development starts from package.json:
pnpm startThis runs:
start:back:nodemon ./back/app.tsstart:front:max dev
Backend startup begins in back/app.ts.
Important details:
- The backend uses Node
cluster. - The primary process initializes the database first.
- A gRPC worker starts before the HTTP worker.
- The HTTP worker starts Express and serves API routes plus frontend static files.
- If the gRPC worker restarts, the HTTP worker is asked to re-register cron jobs.
Production-style backend output is generated by:
pnpm run build:backThe compiled backend is placed under static/build.
Frontend output is generated by:
pnpm run build:frontThe compiled frontend is placed under static/dist.
Backend Architecture
Entry Point
back/app.ts
Responsibilities:
- Creates the Express application.
- Starts primary/worker process logic.
- Initializes database in the primary process.
- Starts gRPC and HTTP workers.
- Handles graceful shutdown.
- Re-registers cron jobs after gRPC worker recovery.
Loaders
back/loaders/app.tsback/loaders/express.tsback/loaders/db.tsback/loaders/depInjector.tsback/loaders/initData.tsback/loaders/initFile.tsback/loaders/initTask.tsback/loaders/server.tsback/loaders/sock.ts
Loader responsibilities:
- Register dependency injection bindings.
- Sync Sequelize models.
- Initialize files and default data.
- Initialize scheduled tasks.
- Configure Express middleware.
- Register routes.
- Attach socket/server behavior.
back/loaders/express.ts is the main HTTP middleware and routing setup. It handles:
- CORS.
- Helmet.
- body parser.
- static frontend serving.
- JWT validation.
- token validation against shared auth state.
/open/*rewrite to/api/*.- route mounting through
back/api/index.ts. - frontend fallback to
static/dist/index.html. - API error handling.
API Routes
back/api/index.ts
This file registers all API modules:
user.ts: login, initialization, authentication-related user endpoints.env.ts: environment variable endpoints.config.ts: config file endpoints.log.ts: log endpoints.cron.ts: cron/task endpoints.script.ts: script file endpoints.open.ts: open API/app token endpoints.dependence.ts: dependency management endpoints.system.ts: system information/settings endpoints.subscription.ts: subscription endpoints.update.ts: update/check endpoints.health.ts: health check endpoints.
Route files should stay thin. They should validate input, get a service from typedi's Container, call the service, and return { code, data, message } style responses.
Services
back/services/*
Services contain most business logic. Common examples:
cron.ts: create/update/delete/run cron jobs, generate crontab data, manage logs, call scheduler client.env.ts: manage environment variables.config.ts: read/write config files.script.ts: manage script files.subscription.ts: manage script subscriptions and repository pulls.dependence.ts: install/manage runtime dependencies.system.ts: system info and settings.notify.ts: notification behavior.sock.ts: socket/log stream behavior.grpc.ts: gRPC server lifecycle.http.ts: HTTP server lifecycle.
When changing backend behavior, first find the API route, then follow it into the matching service. In most cases, the service is the right place for behavioral changes.
Data Models
back/data/index.tsback/data/*.ts
The backend uses Sequelize with SQLite. Database storage is configured in back/data/index.ts:
data/db/database.sqliteCommon model files:
cron.ts: cron job model.cronView.ts: saved cron table views.env.ts: environment variable model.dependence.ts: dependency model.open.ts: open API app/token model.subscription.ts: subscription model.system.ts: system settings model.notify.ts: notification-related data.
Model sync and simple column migrations are currently handled in back/loaders/db.ts.
Configuration
back/config/index.ts
This is the central runtime config file. It reads .env, establishes QL_DIR, and defines important paths:
dataPath: runtime data root.configPath: config files.scriptPath: user scripts.repoPath: subscription repositories.logPath: task logs.dbPath: SQLite database location.uploadPath: uploaded files.shellPath: shell runtime scripts.preloadPath: JS/Python/Shell preload files.
Before hardcoding paths, check back/config/index.ts.
Scheduling And gRPC
back/schedule/*back/protos/*back/services/grpc.ts
The project has two scheduling paths:
- Standard crontab-style tasks are persisted and written through backend cron logic.
- Node/gRPC scheduler logic handles cases such as second-level cron expressions or additional schedules.
back/services/cron.ts decides whether a task needs the Node scheduler using schedule shape and extra_schedules.
Shared Backend Utilities
back/shared/*back/config/util.tsback/config/share.tsback/config/http.ts
Use these before adding new global helpers. Existing shared code includes:
- auth helpers.
- shared store.
- log stream manager.
- task runner helpers.
- concurrency limits.
- file locking utilities.
- HTTP/proxy helpers.
Frontend Architecture
Umi Config
.umirc.ts
Important behavior:
- Dev server proxies API requests to
http://127.0.0.1:5700/. - Frontend build output is
static/dist. - Runtime env script is loaded from
./api/env.js. QlBaseUrlaffects frontend public path and routing base.
App Initialization
src/app.ts
Responsibilities:
- Load Chinese and English locale JSON.
- Determine locale from URL/cookie/localStorage.
- Set Umi locale.
- Apply
QlBaseUrlas public path and router basename.
Layout And Routes
src/layouts/defaultProps.tsxsrc/layouts/index.tsx
defaultProps.tsx defines the main route/menu list. If adding a new page visible in the sidebar, update this file.
Current major pages:
src/pages/crontab: timed task management.src/pages/subscription: subscription management.src/pages/env: environment variables.src/pages/config: config files.src/pages/script: script management.src/pages/dependence: dependency management.src/pages/log: log management.src/pages/diff: diff tool.src/pages/setting: system settings.src/pages/login: login.src/pages/initialization: first-run initialization.src/pages/error: error page.
Frontend Utilities
src/utils/http.tsx: API request helper.src/utils/websocket.ts: socket connection behavior.src/utils/config.ts: frontend config helpers.src/utils/const.ts: constants.src/utils/date.ts: date formatting helpers.src/utils/init.ts: initialization helpers.src/utils/codemirror/*: CodeMirror integration.src/utils/monaco/*: Monaco integration.
When changing a page's API behavior, inspect both the page file and src/utils/http.tsx.
Components And Styling
src/components/*: reusable UI components.src/pages/**/index.less: page-level styles.src/pages/script/index.module.lessandsrc/pages/log/index.module.less: CSS module styles.src/assets/fonts/*: bundled fonts.src/locales/*.json: i18n text.
Follow the existing Ant Design and Ant Design Pro patterns when modifying UI.
Shell Runtime
shell/task.sh: task execution path.shell/otask.sh: alternate/manual task execution path.shell/api.sh: shell-side API helpers.shell/env.sh: environment setup.shell/check.sh: runtime check helpers.shell/update.sh: update helpers.shell/rmlog.sh: log cleanup.shell/share.sh: shared shell helpers.shell/preload/*: preload files injected into JS/Python/Shell task environments.
The backend often coordinates task execution, but the actual user script process environment is shaped by files in shell/.
Runtime Data Directory
data/
This directory is runtime state, not just source code. Be careful when modifying or deleting files here.
Important subdirectories:
data/db: SQLite database.data/config: generated and user-edited config files.data/scripts: user scripts.data/repo: cloned subscription repositories.data/log: task logs.data/upload: uploaded files.data/syslog: system logs.data/ssh.d: SSH-related runtime data.data/dep_cache: dependency cache, when present.
Many bugs that appear as "backend logic" may involve state stored under data/.
Docker And Release Files
docker/Dockerfiledocker/310.Dockerfiledocker/docker-compose.ymldocker/docker-entrypoint.shecosystem.config.jsversion.yaml
Use these when changing deployment, container startup, PM2 behavior, or release metadata.
Common Modification Map
Add Or Modify A Backend API
Typical files:
- Add or update route in
back/api/<module>.ts. - Add or update service logic in
back/services/<module>.ts. - Add or update model in
back/data/<module>.tsif persistence changes. - Add validation with
celebrate/Joinear the route. - Update frontend caller in
src/pages/**orsrc/utils/**.
Add A New Frontend Page
Typical files:
- Create
src/pages//index.tsx. - Add styles in
src/pages//index.lessif needed. - Register route/menu in
src/layouts/defaultProps.tsx. - Add locale strings in
src/locales/zh-CN.jsonandsrc/locales/en-US.json. - Add API calls through the existing request helper.
Change Cron/Task Behavior
Start with:
back/api/cron.tsback/services/cron.tsback/schedule/*shell/task.shshell/otask.shshell/preload/*
Also inspect:
back/data/cron.tsback/validation/schedule.tsdata/config/crontab.listdata/log/*
Change Environment Variable Behavior
Start with:
back/api/env.tsback/services/env.tsback/data/env.tssrc/pages/env/index.tsx
Also inspect:
shell/preload/env.shshell/preload/env.jsshell/preload/env.py
Change Script Management
Start with:
back/api/script.tsback/services/script.tssrc/pages/script/index.tsxdata/scripts/*
Change Login/Auth/Security
Start with:
back/api/user.tsback/services/user.tsback/shared/auth.tsback/shared/store.tsback/loaders/express.tsback/token.tssrc/pages/login/index.tsxsrc/pages/initialization/index.tsx
Be careful with:
- JWT behavior.
- open API token behavior.
- first-run initialization.
- platform-specific session limits.
Change Subscription Behavior
Start with:
back/api/subscription.tsback/services/subscription.tsback/data/subscription.tssrc/pages/subscription/index.tsxdata/repo/*
Change Dependency Management
Start with:
back/api/dependence.tsback/services/dependence.tsback/data/dependence.tssrc/pages/dependence/index.tsxdata/depsdata/dep_cache
Change Logs Or Live Log Streaming
Start with:
back/api/log.tsback/services/log.tsback/services/sock.tsback/shared/logStreamManager.tssrc/pages/log/index.tsxsrc/components/terminal.tsxdata/log/*
Coding Conventions
Backend:
- Prefer adding business logic to services, not route files.
- Use
typediservices consistently. - Use existing config paths from
back/config/index.ts. - Return API responses in the existing
{ code, data, message }shape. - Use existing utilities before adding new helpers.
- Preserve current SQLite/Sequelize style unless doing a larger data-layer refactor.
Frontend:
- Follow existing Umi/React/Ant Design patterns.
- Keep route/menu changes in
src/layouts/defaultProps.tsx. - Use existing request/WebSocket helpers.
- Add or update locale strings for visible UI text.
- Keep page-specific styles near the page.
Shell/runtime:
- Treat
shell/as part of production behavior. - Test task execution changes with realistic scripts when possible.
- Be careful with path quoting and environment variable propagation.
Data:
- Treat
data/as mutable runtime state. - Do not delete runtime state unless explicitly requested.
- Schema changes should account for existing SQLite databases.
Suggested First Steps For AI Agents
When asked to modify behavior:
- Identify whether the change is frontend, backend, shell runtime, data model, or deployment.
- Search by feature name in
src/pages,back/api, andback/services. - Read the route file and matching service before editing.
- If persistence is involved, read the matching
back/datamodel andback/loaders/db.ts. - If task execution is involved, inspect
shell/andback/services/cron.ts. - Make the smallest scoped change that matches existing patterns.
- Run the most relevant check:
pnpm run build:backfor backend TypeScript changes.pnpm run build:frontfor frontend build changes.- targeted manual task/API checks for shell and scheduler changes.
Quick Directory Reference
.
├── back/ Backend TypeScript application
│ ├── api/ Express route modules
│ ├── config/ Runtime config, paths, constants, helpers
│ ├── data/ Sequelize models and SQLite connection
│ ├── loaders/ Startup initialization and Express setup
│ ├── middlewares/ Express middlewares
│ ├── protos/ gRPC proto files and generated TS
│ ├── schedule/ Scheduler/gRPC client helpers
│ ├── services/ Business logic services
│ ├── shared/ Shared backend utilities
│ └── validation/ Joi validation schemas
├── src/ Frontend Umi/React application
│ ├── assets/ Fonts and static frontend assets
│ ├── components/ Shared UI components
│ ├── hooks/ Frontend hooks
│ ├── layouts/ Main layout and menu route config
│ ├── locales/ i18n JSON
│ ├── pages/ Feature pages
│ └── utils/ HTTP, WebSocket, editor, date, and config utilities
├── shell/ Task runtime shell scripts and preload files
├── data/ Runtime state: db, logs, scripts, repos, configs
├── docker/ Docker build and compose files
├── sample/ Sample scripts and default config templates
├── static/ Built frontend/backend artifacts
└── docs/ Project documentationScript History (`docs/script-history.md`)
脚本编辑历史
脚本管理工具栏的“历史版本”打开右侧抽屉。选择版本后,对比历史内容与服务器上当前已保存文件;桌面使用双栏对比,手机使用行内对比。恢复需要确认,有未保存修改时必须先保存或取消。恢复前保留当前版本,因此恢复操作也可以撤销。普通保存不再弹出不可恢复确认。
记录范围
- 编辑器
PUT /api/scripts和调试窗口POST /api/scripts覆盖已有脚本时,在内容变化时留档,首次修改前的内容也保留;相同内容不写历史。 - 历史只包含保存前真实存在的内容;不把失败写入当成一个内容版本。
- 上传、新增、重命名、删除、订阅更新和外部编辑不主动留档。若下次面板保存时检测到外部变更,会保留那一刻的内容,标记为“面板外修改后”。这不代表外部修改之前的内容受到保护。
- 按脚本规范化后的相对路径隔离历史;不同目录同名文件不会混淆,指向同一脚本的安全符号链接共用历史。重命名后不会自动迁移历史。调试器另存为覆盖已有脚本时,目标旧内容进入历史,删除源文件前将其旧内容保留到现有 bak 目录;备份失败则不覆盖目标、不删除源文件。源和目标实际指向同一脚本时(如安全符号链接或不同路径写法),按原地保存处理,保留两个路径,不删除源文件。历史不能用于回收已删除的文件。
- 历史记录支持至多 1 MiB 的 UTF-8 文本文件(含空文件)。当前文件或新内容超出范围时,界面要求明确确认“不记录历史直接保存”;取消则保持文件和草稿不变。确认后以
skipHistory: true和服务端返回的当前哈希重试,锁内检查哈希以防确认期间覆盖他人修改。成功后明确提示本次未留档。大文件可以通过此流程缩小,后续恢复普通留档。未确认的 API 请求返回 413,并包含historyUnavailable和currentHash;历史读取损坏等其他错误不允许用此选项绕过。
存储与资源边界
位置:<QL_DATA_DIR>/script-history/<SHA-256(规范相对路径)>.br。目录位于脚本目录之外,避免进入脚本树、订阅和任务扫描。目录权限 0700,历史文件权限 0600。目录随整个数据目录备份。
每个脚本仅一个 Brotli 压缩 JSON 包,存储完整的旧内容快照以及时间、来源、内容哈希。新文件内容不再重复存储,只保存头版本元数据,供下一次留档标注来源。面板服务通过 TypeDI 注册为单例,路由使用 Container.get(ScriptHistoryService) 获取;构造实例只初始化配置,首次发生保存时才创建历史目录。包内重复文本共享压缩窗口,不依赖补丁链,也不需要额外数据库、文件监听、后台任务或常驻缓存。使用 Node 内置 zlib 异步压缩,Brotli quality=4、window=4 MiB,不新增依赖。
| 限制 | 默认值 |
|---|---|
| 每脚本历史版本 | 最多 20 个 |
| 单个脚本内容 | 1 MiB |
| 单包解压后的 JSON | 8 MiB |
| 单个压缩历史包 | 2 MiB |
| 所有压缩历史包合计 | 32 MiB |
| 有历史的脚本数 | 最多 1000 个 |
| 历史写入暂存文件 | 共享一个,最多 2 MiB |
| 脚本写入暂存文件 | 每次保存一个,与本次保存内容等大,结束后清理 |
达到单文件限制时删除最旧版本,至少保留本次覆盖前的内容;无法保留则拒绝覆盖。达到全局限制时,在脚本成功提交后优先清理最久未更新的脚本历史包(该文件的整份历史),不删除脚本本身。历史暂存、提交或脚本提交失败时不清理任何其他历史;脚本提交失败会回滚本文件历史。如果提交成功但清理失败,返回 cleanupPending,界面提示检查存储权限和空间,后续保存重新尝试清理。配额是正常清理成功后的目标,事务期间和清理失败时会暂时超出。32 MiB 是文件内容字节数,不包含文件系统块分配、目录和锁的开销。清理只在发生保存时执行,通常最多扫描 1000 个包的元数据(清理失败期间可能更多),不解压其他脚本的历史。读取仅解压当前脚本,限制解压输出长度。
文件保存复用现有锁路径,同时以独立跨进程目录锁串行化历史写入和配额清理;finally 释放锁。先准备并同步脚本暂存文件,保留文件权限和所有者;随后写并同步历史暂存文件、原子提交历史,再原子替换脚本。任一步准备失败不会截断原脚本;最后替换脚本失败时还原此前历史;成功后才清理配额。超限确认保存不创建新历史包,使用同样的脚本暂存和替换方式,并流式计算旧文件哈希。外部进程不遵守该锁时,不承诺与其并发写入的事务一致性。
恢复请求携带预览时的当前内容哈希,锁内检查后才写入;文件变化则返回 409。历史读写与脚本读取采用相同的路径和符号链接边界检查。面板配置目录中的敏感文件仍受黑名单保护;脚本目录中的 auth.json、token.json 等普通脚本名称可以正常保存和留档。
压缩样本
运行 node scripts/benchmark-script-history.cjs 可复现比较。模拟同一脚本连续 20 次小修改,比较独立 gzip、合并 gzip 与合并 Brotli。首次本机测量:
| 样本 | 独立 gzip | 合并 gzip | 合并 Brotli(quality 4) |
|---|---|---|---|
| sample/notify.js | 242,430 B | 221,832 B | 13,930 B |
| shell/task.sh | 30,159 B | 2,317 B | 1,737 B |
这是文本重复度较高的模拟样本,不是所有工作负载的压缩保证。Brotli 合包特别适合大小超过 gzip 32 KiB 窗口、但每次只改少量内容的脚本。增量补丁可能在部分样本更小,但增加补丁生成、链式恢复、裁剪和损坏恢复的复杂度;本实现选择压缩合包与硬上限,在磁盘、CPU 和可靠性之间取平衡。
API
GET /api/scripts/history?filename=...&path=...:版本元数据、当前哈希、保留数量。GET /api/scripts/history/detail?filename=...&path=...&id=...:历史内容、当前内容及当前哈希。PUT /api/scripts/history/restore:{filename, path, id, expectedHash}。
沿用现有 scripts 路由的认证与权限。历史 ID 使用 UUID;不存在或已清理返回 404,文件已变更返回 409,文本或容量限制返回 413。
Development Node Cron Migration (`docs/development/node-cron-migration.md`)
Node cron 调度器迁移
Node 调度路径使用固定版本 [email protected],普通任务(含额外规则)、订阅和系统 cron 共用 back/shared/cronScheduler.ts。启动/一次性任务、间隔任务、脚本并发限制和进程执行链路保持原有实现。系统模式只将语义一致的普通五字段规则交给 crond。
移除 node-schedule、其类型包以及兼容别名 cron-parser-v4。保留 cron-parser@5 做旧表达式的校验与标准化;计时器和日历推进由 node-cron 管理。
兼容行为
- 保留旧版 1–6 字段补全、五种宏、月份/星期名称、数字起点步长、L 和 #。另外明确新增
@annually(等同@yearly)、@midnight(等同@daily)、@minutely(每分钟第 0 秒)。宏按进程时区解释。?仅允许日期和星期字段;裸/N、H 和其他未列出的宏仍拒绝。 - 日期与星期同时指定时保留 OR 语义,不采用 node-cron 默认的 AND。两个原生任务分别处理日期和星期,并按计划时间过滤交集,避免重复执行;保留旧解析器的月份全日期范围及全局 nth-week 限制。
- 原生任务先暂停创建并验证,再替换旧任务。构造失败时销毁已准备的实例并保留旧快照。
- 进程内迟到的触发通过
execution:missed加入既有执行队列,记录补执行日志。并发和单任务积压上限仍由青龙队列控制;这不提供进程退出期间的持久化补跑或分布式 exactly-once 保证。 - 标准化结果使用上限 512 条的只读缓存,仅缓存语法,不缓存运行实例或下次执行时间。避免大批相同表达式重复解析导致恢复 RPC 超时。
- 回调异常记录日志,后续调度继续;取消时调用原生
destroy()释放计时器和注册表。 - 持久化无效任务在恢复时跳过并记录日志,无效订阅不会产生未处理的启动拒绝。API、导入和执行使用同一套表达式校验。
复审修复
- 移除没有生产调用的
nextInvocation()接口,避免对 OR/# 等过滤规则报告不会实际执行的原生日期。回归测试直接观察实际执行和原生实例状态。 - 批量注册 RPC 时限为 5 秒加每条规则 5 毫秒,计入额外规则并封顶 120 秒;保留超时后的恢复机制,不立即重放结果不确定的写入。
- Alpine/系统模式下,数字起点步长(如
0/5)、星期包含7、日期步长、日期和星期同时受限的表达式转入 Node 路径。BusyBox 的这些解析或通配语义与旧 Node 解析器不同,不能仅因是纯数字五字段就交给 crond。 - 这些规则在
crontab.list中仍保留为注释,避免同时被系统和 Node 执行。其他可移植规则仍使用系统 crond。
夏令时差异
采用 node-cron 的当地时间语义:春季跳时不存在的时间跳过,秋季重复小时不再执行第二次。旧 cron-parser 对部分春季时间会顺延,并可能重复秋季高频任务。使用有夏令时的时区时需要考虑该行为变化;Asia/Shanghai 不受影响。回归测试固定 America/New_York 的 2026 年转换日期。
验证
npm test 和 npm run build:back。
test/fixtures/legacy-cron.json是迁移前使用 node-schedule 2.1.1 / cron-parser 4.9.0 保存的快照:269 条语法判定,18 组日历序列,每组 10 次执行。- 真正的 node-cron 实例配合虚拟时钟验证日期/星期 OR、交集去重、闰年、L/#、8 秒迟到补执行、取消释放、回调拒绝和夏令时。
- 注册构造失败回滚、无效订阅隔离、空表达式立即运行,以及现有调度恢复/503/子进程测试覆盖接入流程。
- 隔离的正式 Debian 镜像使用实际编译产物验证 10,000 条有效任务和 2 条错误任务的恢复、300 个同时到期的 Python 脚本、并发上限 16 及 HTTP 健康检查。具体结果见本次测试报告;这是生产镜像隔离验证,不是线上长期运行证明。
本次验收结果(2026-09-27)
完整回归 260 通过、3 项原有跳过,后端编译成功。Node 20 生产运行时的 21 项兼容测试通过。隔离镜像中 300/300 脚本完成,无重复或遗漏,最大并发 16,270 个成功及 30 个预设失败均落库;完成耗时 52.36 秒。恢复后健康检查 1,870/1,870 成功,任务列表 38/38 成功。重启恢复阶段另有 50 次健康检查失败。
初版重复解析曾导致恢复超过 5 秒 RPC 时限,加入有上限的语法缓存后重跑通过。本次 10,000 个任务共享两个表达式,不能视为一万种复杂表达式的容量保证;实际脚本耗时也不代表纯调度性能。
新增宏在 UTC 和 Asia/Shanghai 下验证连续两次实际触发及不提前触发;原有语法快照仅对这三个新增宏允许预期变化。
复审后验收
完整回归 267 通过、3 项原有跳过;Node 20 的 10 项修复回归及 Alpine 镜像的 24 项兼容/宏测试通过。
额外使用 9,700 个不同表达式和 300 个同时到期任务验证恢复(另有 2 个无效任务),不再局限于共享两个表达式。300/300 脚本完成,无重复或遗漏,最大并发 16,270 个退出码 0、30 个预设退出码 3 均落库。恢复后健康检查 1,650/1,650 成功、任务列表 33/33 成功;启动恢复阶段另有 260 次健康检查失败,整批脚本耗时 60.27 秒。较大快照恢复仍需等待,不承诺启动阶段始终可用或任意规模都能在时限内恢复。
Development Notification Audit (`docs/development/notification-audit.md`)
通知日志
通知日志记录系统统一通知入口(包括登录通知、配置测试和系统通知 API),以及任务使用内置 notify.js、notify.py 或 QLAPI.notify 发出的通知。脚本通知按实际配置的渠道各记录一条结果,未配置渠道和跳过的标题不记账。
日志包含时间、标题、正文、渠道、结果和来源;脚本来源另包含文件名与任务 ID。成功表示通知渠道返回成功或接受请求,不表示最终收件人已经收到。多目标渠道只在所有目标成功时记为成功。
所有入口共用数据库中的最近 200 条日志。标题最多 512 字符、正文最多 8192 字符;上报字段不包含通知密码、令牌或渠道配置。日志页受系统设置的管理员权限保护。
脚本通过现有的本机 mTLS gRPC 服务上报,独立客户端最多等待 1.5 秒。旧面板、服务不可用或数据库写入失败不会改变通知发送结果。Python 使用 Node 上报子进程,并限制在 3 秒内。
自行保存的旧版通知库需要更新为面板内置版本才能上报。脚本自行调用 HTTP、SMTP 或第三方 SDK 时不经过这些入口,无法自动纳入通知日志。
Development Python Dependency Sources (`docs/development/python-dependency-sources.md`)
Python 依赖来源
依赖管理中可手动添加 Python 包名、git+https:// 等 pip 支持的源 URL,或脚本/订阅仓库目录内的 requirements 文件、pyproject.toml 路径。文件路径支持目录空格,必须位于持久化 scripts 或 repo 根目录内;越界与指向根目录外的符号链接会被拒绝。
requirements 使用 pip install -r,pyproject 使用完整项目目录安装,保留相对依赖文件和项目源码。拉取订阅不会自动安装仓库声明的依赖。
删除 requirements 条目会调用 pip uninstall -r。源 URL 或 pyproject 条目无法可靠逆向推断其安装的所有分发包,因此删除该条目只移除面板记录;需另外按包名卸载已安装的包,日志会明确提示。
Development Task Startup (`docs/development/task-startup.md`)
Task startup and notification loading
task performs shell configuration, status reporting and dependency-path discovery
before invoking the selected runtime. These costs are separate from cron callback
latency and time spent waiting for an execution slot.
Dependency-path cache
Node scheduling usually starts in /ql, while Alpine crond starts in /root.
The pnpm discovery key includes the working directory, npm/pnpm configuration,
relevant environment and executable metadata. Each key now has its own cached
record, so alternating scheduler environments do not evict each other.
Records expire after 60 seconds. Refreshes share a persistent per-user lock;
records older than an hour are removed on refresh. Warm lookups do not scan the
cache directory. The lock is never removed while another process might use it.
The opt-out QL_NODE_PATH_CACHE=0, failed-discovery fallback, and package resolution
order remain available. This caches the dependency directory, not its contents.
Language startup paths
| Invocation | Environment and before hooks | Automatic notification loading |
|---|---|---|
task script.py / .pyc |
Python preload imports generated environment, runs the shell/command before hooks, imports task_before.py, then applies account selection |
QLAPI.notify(...) imports __ql_notify__ on first call |
task script.js / .mjs / .ts |
Node preload imports generated environment, runs shell/command before hooks, requires task_before.js, then applies account selection |
QLAPI.notify(...) requires __ql_notify__.js on first call |
task script.sh |
Shell sources the generated environment and runs shell before hooks | No automatic Python/Node notification module |
Explicit interpreter or other command, e.g. task python3 script.py |
Retains generated environment and shell before-hook exports; it does not automatically install the Python/Node preload | No automatic notification module unless configured by the caller |
TypeScript uses ts-node-transpile-only; MJS uses the same Node preload plus the
ESM loader. There are no separate built-in Ruby, Go, Java or PHP preload modules.
All these task paths still pass through shared dependency-path setup: non-JS
scripts and their hooks may themselves call Node or use the QLAPI bridge.
The before hooks are deliberately still synchronous: they can export environment
variables or perform work required by the user's script. Python's preload still
starts a child shell/Python to capture that environment; Node's preload starts a
child shell/Node. Node also still loads its gRPC client eagerly. These are remaining
startup costs, not work performed by the cron library, and are not removed by this
change.
Shell scripts run in a child shell that inherits the wrapper's environment and
before-hook functions. This lets the wrapper run its after hooks and report the
exit status even when a script calls exit, replaces itself with exec, or fails
under set -e. The script's own EXIT trap still runs in the child. Changes made
inside the script (variables, working directory, functions and shell options)
do not propagate back to the wrapper or its after hooks.
Lazy notifications
Scripts keep calling QLAPI.notify with the same arguments. When no notification
is sent, the notification provider and its channel dependencies are never imported
by the preload. First use loads the provider; subsequent uses reuse the standard
Python/Node module cache. JS forwards the receiver and return value, including
promises, and Python forwards positional and keyword arguments. Import/send errors
surface at the notification call rather than preventing unrelated scripts from
obtaining QLAPI during startup. A failed import can be retried after its cause is
resolved.
Provider initialization, including provider configuration reads, now happens at
first use. A script's explicit import __ql_notify__, require(...), or independent
notification helper still loads that module immediately.
Task timeouts
task -m DURATION ... also applies to sourced Shell scripts. The timeout worker
inherits before-hook variables, arrays and functions without serializing them.
At the deadline, the worker's process group receives TERM, followed by KILL after
0.5 seconds, so ordinary child processes cannot continue after the timeout.
Explicit interpreters and other commands use the same mechanism. Processes that
intentionally create a separate session/process group are outside this boundary.
The wrapper records status 124 and runs its after hook; concurrent account mode
retains its existing aggregate status behavior. Zero disables the timeout.
Cron executions waiting for a concurrency slot retain their schedule revision.
Disabling, deleting or replacing that revision prevents its queued executions
from starting when a slot becomes available. Already running executions retain
the existing stop behavior. Invalidated callbacks do not count against a new
revision's limit of five pending/running executions.
Releases 2.22.0 Validation (`docs/releases/2.22.0-validation.md`)
2.22.0 发布准备验收
日期:2026-09-26。候选代码基于 origin/develop@801a71d7402f5d4b582318ed88ad00633357a531,主包和面板版本统一为 2.22.0。本文件记录验收证据,不代表已经发布。
自动化与构建
- 新增 5 项日志弹窗回归:大日志结束后读完分页、尾部截取不重复取历史、运行中空日志继续轮询、关闭取消定时器/丢弃在途响应、旧响应和不前进偏移不死循环。
- 用真实日志读取器写入 600 KiB 后结束任务:原 develop 在新增回归中失败,修复后 5 项全通过。
- macOS arm64、Node 20、按冻结锁文件独立安装依赖:228 项测试,225 通过、3 项平台相关跳过、0 失败。后端 TypeScript 与前端生产构建通过;前端有已有的大包体提示。
npm pack --dry-run成功,包含前后端产物,主包版本为 2.22.0;npm registry 查询该版本返回 E404(尚未占用,发布时仍需重新核实)。- 上游基线 Linux CI 为 223 项回归全部通过;CLI Node 22.12/24 流水线通过,Node 24 的 291 项测试全部通过。该结果属于上游提交,不冒充本分支新的 CI 结果。
- GitNexus 对
getCronLog修改前影响分析:LOW,直接调用者CronLogModal,1 条流程。最终差异分析涉及日志弹窗流程;行号移动也将autoScroll列出,但该函数实现未修改。
容器升级与回退
测试仅使用新建账号、新建任务和隔离容器数据,不接触真实用户数据。
- 已发布旧版镜像
whyour/qinglong:2.21.0digest:sha256:e72f77d855a86cfabf774de76b030731c38540b251a0c888038810fbcf256c48。镜像内源码为9b37f064d815baf49b1869445bbd60e4209bacc9,与当前 master 提交不同;这是已发布镜像到候选版的实际升级验收。 - 候选运行环境为
whyour/qinglong:developdigest:sha256:35c5b9f2d0666ab7fcde954f175087c097e93fa852306360aa07b8aa0a5d0481,revision 标签对应801a71d7402f5d4b582318ed88ad00633357a531。装入本分支本地构建的前后端及版本文件。因此验证了候选代码的容器运行,不等同于最终发行镜像的完整构建验收。 - 旧版创建账号及真实 Shell 脚本任务并验证独立输出,停机复制完整
/ql/data作为一致的升级前备份。 - 候选版读取该备份后原密码可登录、原任务仍存在并能真实执行,数据库已增加迁移表,测试账号密码已转为 scrypt(仅检查布尔值,未记录凭据)。
- 长运行脚本启动后确认实际 PID;停止后确认该进程不存在;立即重跑并再次停止成功。状态字段恢复为空闲;不把历史 PID 字段当作进程存活判断。
- 两步验证启用、密码阶段返回挑战、OTP 登录、同一验证码重放拒绝、关闭两步验证及重新登录均通过。
- 每 5 秒调度的测试脚本产生持续输出;本容器 HTTP 夹具订阅下载 V1 文件成功。
- 订阅夹具更新为 V2 后再次同步,落盘内容已变为 V2。
- 容器重启前调度标记 34 行,重启后 41 行,HTTP/gRPC 健康且可重新登录,确认定时注册恢复。
- 将升级前完整备份恢复到全新的 2.21.0 容器,原密码登录、原任务保留及真实脚本输出均通过。
生产依赖审计:仍需处理
在无共享 node_modules 的临时目录,使用候选 package.json 和 pnpm-lock.yaml 执行 pnpm audit --prod --json:414 个依赖,报告 3 Critical、38 High、49 Moderate、9 Low。数量是扫描公告命中,不等于独立漏洞数量或均能在面板中利用。审计时间与数据库变化会影响结果。
| Critical 命中 | 生产依赖路径 | 初步适用性与处理 |
|---|---|---|
| protobufjs 7.5.4 | @grpc/proto-loader → protobufjs |
公告要求攻击者能控制加载的 schema/descriptor;当前主要使用仓库内生成的协议,尚未证实外部可控 schema 入口。仍应升级到修复版本并验证 gRPC。 |
| websocket-driver 0.7.4 | sockjs → websocket-driver / faye-websocket |
处理网络协议帧,不能用面板业务鉴权替代协议层修复;应升级到至少 0.7.5 并回归 WebSocket。 |
| tar 6.2.1 | @whyour/sqlite3 → tar、@mapbox/node-pre-gyp → tar |
主要涉及安装/构建解压链,不能直接等同于面板上传接口漏洞。修复跨 tar 主版本,需验证 SQLite 预编译下载和源码编译路径;另有 tar 7.5.20 的后续公告。 |
参考:protobufjs 公告、websocket-driver 公告、tar 公告。
High 命中还涉及 gRPC、multer、js-yaml、JWT 的 jws、undici 等运行时依赖。当前准备改动没有批量升级依赖;不能宣称安全扫描通过。正式发布仍需完成依赖升级与可达性评估、回归,或由维护者明确记录接受的风险。
尚未放行的发布步骤
- 本分支新的 Linux CI 尚待执行。
- Python 3.10 镜像和全部架构的最终候选构建尚未验证;上游 develop 工作流跳过这些发布专用任务。
- 主包及 CLI 的 npm Trusted Publisher 设置属于 npm 账户配置,未通过代码或只读公开 registry 验证;不得把
id-token: write视作已完成授权。 - 最终发版时确认
publishTime与实际发布时间一致,并重新核对版本是否可发布。 - 完成上述事项前不合并到 master、不打正式 tag、不执行
shell/pub.sh。
Releases 2.22.0 (`docs/releases/2.22.0.md`)
青龙 2.22.0
2026-09-27 覆盖更新
- 修复历史任务中合法的
?定时表达式被拒绝,导致启动恢复失败、面板健康检查返回 503 的问题(#3078)。主规则和附加规则均保留兼容性,无效规则仍在替换现有任务前拒绝。 - 修复
ql check等 Shell 诊断输出以--开头时的printf: --: invalid option,补全调度注册失败日志中的错误详情。 - 启动恢复跳过无效主规则或附加规则的任务,记录任务 ID、名称和错误规则;数据库原记录保留可编辑,其他任务与面板正常启动。服务连接故障仍返回 503。
- 校验使用与实际调度器一致的解析器,避免接受
H等无法执行的语法;五字段扩展规则、宏和历史简写交给 Node 调度器,避免系统 crontab 拒绝整个任务快照。 - 已对 250 多个表达式样本与真实调度库做一致性校验,验证无效任务隔离和两种调度模式;本地回归 240 项,237 通过、3 项平台相关跳过,后端构建通过。
新增功能
- 独立远程命令行包
@whyour/qinglong-cli:覆盖任务、订阅、环境变量、配置、脚本、日志、依赖、应用、系统等 OpenAPI,支持认证、JSON 输出和中英文帮助。 - 可选面板内部 TypeScript 工具:任务执行、订阅同步、启动检查、升级重载和账号恢复。默认继续使用原 Shell 入口。
- 仪表盘支持点击今日成功、今日失败查看任务明细。
- 客户端 IP 和代理链诊断、可信代理配置、登录 IP 黑名单。
- 历史运行实例和统计数据的保留参数、清理预览、手动清理、依赖缓存清理和数据库压缩。保存保留参数不等于开启自动清理。
- WPUSH 通知,支持面板、JavaScript、Python 和 gRPC。
ql log [--lines <n>] [--nostream]查看服务日志。
修复与优化
- 修复任务生命周期、调度锁、调度器就绪和恢复、排队与停止竞争、订阅清理,以及停止失败的反馈。
- 日志改为分块读取;任务结束后继续读完剩余分页,确保最后的输出和退出信息可见。修复中文任务日志名。
- 加固密码存储、JWT 有效性校验、两步验证、认证状态变更和文件路径访问。
- 修复过期登录令牌引起的页面刷新循环,完善任务字段校验提示。
- 减少常驻依赖和重复 WebSocket 鉴权,限制指标缓存和日志显示量,优化大目录枚举。
- 优化 Node 路径缓存与 Alpine 并发缓存锁,修正依赖缓存清理后的安装状态。
- 启动时保留
.env配置;保留容器启动错误诊断;数据库迁移失败时中止启动。 - 镜像增加 provenance、SBOM、源码/静态产物/依赖锁文件一致性校验,改进多架构构建。
升级兼容性
数据备份与回退
升级前停止面板和任务,备份完整持久化数据目录,并记录当前镜像的不可变 digest、启动参数、挂载和环境配置。不要只复制正在写入的 SQLite 主文件;必须保留完整离线数据备份。
新版兼容旧密码;首次成功登录或修改密码后,密码会迁移为 scrypt 哈希。旧版不识别这种格式,因此不能只回退镜像并继续使用升级后的数据。
需要回退时:停止新版,另外保存升级后的数据,恢复升级前的完整备份,再使用原镜像 digest 和原启动配置启动旧版。恢复备份会丢弃备份后产生的任务、配置和日志变更。如必须保留升级后数据,应单独验证旧版兼容性,并通过可信终端使用旧版账号恢复功能重新设置密码;不要手工把哈希当作密码使用。
数据库迁移新增 SchemaMigrations 和任务排队标识;迁移异常会阻止启动。应检查启动日志并修复数据或恢复备份,不要通过删除数据库来绕过错误。
API 与日志
旧文件读取路由 /configs/:file、/scripts/:file、/logs/:file 返回业务码 410,请改用对应的 /detail 接口(适用于 /api 和 /open 前缀)。
日志接口默认返回尾部片段。客户端需要使用 offset、nextOffset、total 读取后续数据;任务已结束不代表所有日志已经读取完毕。面板仅保留有限长度的日志显示,完整文件仍在日志目录中。
修改账号或两步验证配置会使旧会话失效;客户端应重新登录。IP 黑名单针对登录;反向代理场景应先在客户端 IP 页面确认识别结果再配置黑名单。
CLI
远程 npm CLI 要求 Node >=22.12,推荐 Node 24。它与面板内置工具均使用 ql 名称;已有面板的机器应使用独立 npm prefix 或 npm exec --package=@whyour/qinglong-cli -- ql ...,避免覆盖内置命令。
远程 npm 包不包含本机执行器、升级、重载和账号恢复实现,也不会自动切换面板入口。使用内部工具需单独构建完整 cli/dist,在满足 Node 版本要求的环境中显式设置 QL_CLI_ROOT;清除该变量并重启恢复 Shell 入口。标准面板镜像并未默认启用内部 TypeScript 工具。
发布验收
正式发布前应完成:旧版离线数据升级、登录与两步验证、定时/手动任务、停止后立即重跑、订阅更新、容器重启调度恢复和备份回退。发布者还需检查 Python 3.10 镜像矩阵,以及主包和 CLI 的 npm Trusted Publisher 配置。
依赖审计与执行证据见同目录的 2.22.0-validation.md。通过单元测试或构建不代表完成所有平台和真实升级场景的验收。
Releases 2.23.0 (`docs/releases/2.23.0.md`)
青龙 2.23.0
新增功能
- 脚本历史版本:查看、对比和恢复历史内容;每个脚本保留最多 20 个版本,支持不超过 1 MiB 的 UTF-8 文本脚本。恢复前校验当前内容哈希,发生并发修改时返回冲突,避免覆盖新内容。
- 独立远程 CLI 0.1.2 增加脚本历史查看、对比和恢复命令,并完善资源冲突及认证错误反馈。最低要求仍为 Node.js 22.12。
修复与优化
- 普通任务、额外规则和订阅的 Node 调度路径迁移到 node-cron。保留旧规则标准化、日期/星期 OR、L/# 等兼容行为,补充
@annually、@midnight、@minutely。 - 禁用、删除或修改任务后,旧规则对应的排队执行不会再启动。完善任务进程超时、停止回收、Shell before/after 环境传递和退出状态。
- 修复系统令牌未主动续期和认证缓存并发更新:定时生成与启动后重新计时会主动续期,避免长期运行后失效。Shell API 每次请求检查令牌,遇到 401 时刷新并重试一次;其他错误不自动重试。
- 降低任务启动开销;JS/Python 的 QLAPI 通知依赖在首次通知调用时加载,减少不发送通知的任务启动成本。
- 加固环境变量名和字面值、订阅参数、路径访问及 SSH 配置输入;保留合法的中文及分支别名兼容。
- 修复页面升级、
ql update和静态资源的版本同步,完善升级暂存校验及应用、完整备份与恢复流程。 - 修复 Debian 上的依赖安装、通知和调度回归,补齐前端缺失的中英文翻译,改进 Gitee/GitLab 静态资源镜像同步检查。
- 最小升级 compression、multer、@grpc/grpc-js、proxy-addr、websocket-driver、sequelize,修补现有 HTTP、上传、代理信任、WebSocket、gRPC 和 JSON 查询路径相关风险。完整依赖治理、Node/pnpm 迁移及工具打包调整延期。
升级说明
升级前停止面板与任务,备份完整持久化数据目录,包括数据库、脚本、配置、日志、依赖缓存及已有的 script-history 目录,并记录原镜像摘要和启动配置。使用面板备份接口时,确认选择了脚本、配置和日志;仅备份数据库不能恢复全部数据。
升级后试运行代表性的 JavaScript、Python、Shell 任务,检查订阅、环境变量、依赖安装以及通知。存在夏令时的时区采用 node-cron 行为:跳过春季不存在的当地时间,秋季重复小时不重复执行;Asia/Shanghai 不受影响。调度器不提供进程退出期间的持久化补跑保证。
任务超时会终止普通子进程所在的进程组并记录退出状态 124;主动创建独立会话/进程组的程序不在该边界内。并发账号执行模式保留既有汇总退出状态。
本次沿用现有 Dockerfile 和 pnpm 8.3.1。默认 Debian 镜像使用 Debian 12 与 Node.js 20,默认 Alpine 镜像的 Node.js 由 APK 提供;默认 Python 为 3.11。Node 20 与 Node 18 构建运行时已结束上游维护,相关迁移延期,功能验证通过不代表全部依赖风险清零。
2.23.0 是 Python 3.10 兼容镜像的最后一个版本。2.23.0-python3.10 和 2.23.0-debian-python3.10 发布后停止维护,不再提供后续更新;请验证脚本和依赖后迁移至默认 Python 3.11 镜像。
主包 @whyour/qinglong 与独立远程包 @whyour/qinglong-cli 的 ql 命令用途不同。在已有面板的主机上使用独立 npm prefix 或 npm exec --package=@whyour/qinglong-cli -- ql ...,避免覆盖面板内置入口。
发布时间:2026-10-11 12:27:02(Asia/Shanghai)。本版本已完成四类镜像验证及独立 dev 环境超过两小时的持续功能验收。
--- METRICS ---
- Files Extracted: 13
- Estimated Token Budget: ~17463 tokens
- Canonical Reference: https://codewiki.google/github.com/whyour/qinglong