feat(cli): cover OpenAPI with a remote npm CLI and internal panel tools (#3074)

* feat(cli): add unified Commander CLI for QingLong 2.x

* fix(cli): publish via npm and address security review feedback

* ci(cli): package npm artifacts and remove evaluation collateral

* test(cli): use a fixed shell fixture for log retention

* refactor(cli): separate remote npm client from panel tools

* feat(cli): cover active panel OpenAPI resources

* docs(cli): unify authentication and skill guidance

* refactor(cli): isolate internal commands and generate Commander help

* refactor(cli): organize remote and internal modules by responsibility

* ci(cli): publish verified npm archives from master

* fix(cli): publish under the whyour npm scope

* ci: use npm trusted publishing for both packages

* docs: introduce the published CLI on the project homepage

* fix(cli): preserve server log truncation and correct login hints

* fix(cli): accept dashboard record request bodies

* fix(cli): preserve stdin for local task execution

* fix(cli): resolve task executables after changing directory

* fix(cli): preserve shell function tasks and sanitize test failures

* fix(cli): preserve shell hook state and resolve workdir after hooks

* fix(cli): preserve cleanup across shared shell task timeouts

* fix(cli): isolate shell control descriptors and reap timed-out descendants
This commit is contained in:
whyour
2026-09-25 23:24:41 +08:00
committed by GitHub
parent f051135fc4
commit 801a71d740
185 changed files with 22412 additions and 6 deletions
@@ -0,0 +1,213 @@
# OpenAPI 全量参考 / Complete OpenAPI reference
CLI 对应当前 develop 的 143 条有效路由。通过 `ql api routes --json` 查看方法、路径、对应命令、位置参数、请求体与上传字段;CLI 测试与 back/api 路由逐项核对。GET configs/:file、scripts/:file、logs/:file 已返回 410,改用 detail 命令,不再暴露旧入口。旧版面板可能没有新路由,以服务端响应为准。
The catalogue covers 143 active routes in the current develop backend. `ql api routes --json` lists methods, paths, commands, positional parameters, bodies and upload fields. A route-coverage test detects drift. Three retired GET :file routes return 410; use detail-based commands. Older panel versions may not implement newer routes.
## 认证与权限 / Authentication and permissions
常规使用 `ql login` 的应用凭据。scope 是 /open 后首个路径段:crons、subscriptions、envs、configs、scripts、logs、dependencies、system、dashboard 等。应用管理需要 apps;user、health、update 等路由同样接受服务端权限校验。面板 UI 当前没有列出全部这些 scope,不能把路由存在理解成现有应用已获授权。
认证步骤、环境变量优先级、应用登录、面板会话与双因素验证、退出行为,统一见 [认证参考](panel.md#authentication)。应用管理可用 apps 权限的应用或授权面板会话;QL_URL/QL_ACCESS_TOKEN 不保存、不刷新,不自动退回应用配置。
See the [authentication reference](panel.md#authentication) for application login, direct-token precedence, owner sessions/2FA and logout. App management accepts apps-scoped credentials or an authorized owner session. Direct tokens are not saved/refreshed and never fall back to application configuration.
`auth status --scope <scope>` tests a representative read; update has no read endpoint and is not offered by status. This does not prove permission for every write. `app list/create/update/reset-secret` 默认隐藏 client_secret/tokens;只有明确需要凭据时才加 --show-secrets,并避免把输出记录到共享日志。其他资源/通用请求可能含环境值、配置、脚本、订阅凭据和会话,按敏感数据处理。
App results omit client_secret/tokens unless --show-secrets is explicit. Other resources and raw responses may contain sensitive values. Handle them accordingly.
## 输入和输出 / Inputs and outputs
- `--data '<JSON>'` / `--data @file.json` / `--data -`:完整请求体,保留所有后端字段;query 同样支持这三种输入。
- `--query '{"searchValue":"demo","page":1}'`:查询对象,值为标量或标量数组,CLI 编码为 URL 参数;不要拼接 URL 查询字符串。
- `create/update` 常用字段可用选项;同一字段不能同时在 --data 和选项中提供。update 的位置 ID 注入请求体,拒绝与 body.id 冲突;它提交完整更新对象,不会先读取后盲目合并。
- 使用 `<id...>` 的命令可以传多个 ID。原 task run/stop 与 subscription run/stop/enable/disable 保持单 ID 契约;批量使用 api request 和数组请求体。
- 上传使用 `--file`,其他 multipart 字段用 --data 对象,字段名由路由决定(file/env/data/avatar);下载与 system command-run 使用 `--output`,不覆盖已有文件,文件权限 0600,失败清理本次新建文件。
- 默认 30 秒超时,可用新命令的 `--timeout <秒>` 调整,最多 3600。耗时任务优先创建任务再 run,持续执行的 command-run 需合理超时;连接超时不表示远程操作已停止。
- JSON 响应保留服务端 data/附加字段。原任务/订阅读取保留原有分页、裁剪和日志 tail 契约;api request 提供完整原始能力。下载只输出保存路径/字节数,不把二进制混入 stdout。
- POST/PUT/DELETE 不自动重试,网络/5xx 后先检查服务端状态。GET crons/import 也可能改变服务端状态,不能仅凭 HTTP 动词认定只读。
Inputs accept inline JSON, @file or stdin (-). Queries are objects with scalar/array values. Named updates require complete server fields plus the positional ID; there is no implicit read/merge/write. Bulk operations use ID positionals where shown, or api request arrays. Uploads use --file; downloads/command streams require a new --output file. Responses remain JSON; downloaded bytes go only to the file. New operations support --timeout (seconds, default 30, maximum 3600). Timeouts do not prove remote cancellation. No request is automatically retried.
## 常用示例 / Examples
```sh
ql task create --name demo --command 'task demo.js' --schedule '0 0 * * *' --json
ql task update 12 --data @task.json --json
ql task enable 12 13 --json
ql task delete 12 13 --json
ql subscription create --type public-repo --url https://example.com/repo.git --alias demo --schedule-type crontab --schedule '0 0 * * *' --json
ql subscription update 5 --data @subscription.json --json
ql subscription delete 5 --query '{"force":true}' --json
ql app create --name agent --scopes crons,subscriptions --show-secrets --json
ql app update 3 --name agent --scopes crons,subscriptions,envs --json
ql app reset-secret 3 --show-secrets --json
ql env create --data @envs.json --json
ql env update 8 --data '{"name":"EXAMPLE","value":"value"}' --json
ql script create --file ./demo.js --data '{"filename":"demo.js","path":""}' --json
ql script get --query '{"file":"demo.js","path":""}' --json
ql config save --data '{"name":"example.sh","content":"# example"}' --json
ql config get --query '{"path":"example.sh"}' --json
ql log download --data '{"filename":"example.log","path":"example"}' --output ./example.log --json
ql dependency create --data '[{"name":"example-package","type":0}]' --json
ql system data-export --data '{}' --output ./panel.tgz --json
ql system data-import --file ./panel.tgz --json
ql api request PUT /open/crons/run --data '[12,13]' --json
ql api request GET /open/crons --query '{"page":1,"size":100,"searchValue":"demo"}' --json
```
任务 create/update 需要 command/schedule;name、labels、sub_id、extra_schedules、task_before/after、log_name、allow_multiple_instances、work_dir 等字段可通过 --data 提交。订阅 create 需要 type/url/alias/schedule_type,update 需要 id/type/url/alias;interval_schedule、pull_option、dependences、extensions、sub_before/after、proxy、autoAddCron/autoDelCron 等用 --data。private-repo 凭据仅通过受保护文件或 stdin 提供。环境变量与依赖 create 接受对象数组,应用 scopes 为字符串数组。其余准确字段/枚举由当前后端 Joi schema 校验:见 [back/api](https://github.com/whyour/qinglong/tree/develop/back/api) 和 [定时规则 schema](https://github.com/whyour/qinglong/blob/develop/back/validation/schedule.ts)。
Task creation/update needs command/schedule; additional fields use --data. Subscription creation needs type/url/alias/schedule_type; updates need id/type/url/alias. Interval schedules, private repository credentials, filters, hooks and booleans use --data. Env/dependency creation accepts arrays; application scopes is a string array. The server validates field types/enums; the linked backend schemas are authoritative.
下面包含应用/用户/系统远程管理;这些通过 HTTP 在目标面板执行,与已从 npm 移除的本机 reload/reset/start 不同。诊断不自动授权创建、删除、升级、重启、密钥重置或数据导入。遵循当前用户已给出的授权,不重复确认已授权操作。
System/user operations below execute remotely through the panel API. They do not restore local system tools to npm. Follow the user's authorized scope, including for resets, data import and upgrades; diagnosis alone does not authorize mutations.
## 路由表 / Route table
| 命令 / Command | 方法 / Method | /open 路径 / Path | 输入 / Input |
| --- | --- | --- | --- |
| `ql task view-list ` | GET | `crons/views` | --query |
| `ql task view-create ` | POST | `crons/views` | --data, --query |
| `ql task view-update <id>` | PUT | `crons/views` | --data, --query |
| `ql task view-delete <id...>` | DELETE | `crons/views` | --query |
| `ql task view-move ` | PUT | `crons/views/move` | --data, --query |
| `ql task view-disable <id...>` | PUT | `crons/views/disable` | --query |
| `ql task view-enable <id...>` | PUT | `crons/views/enable` | --query |
| `ql task list ` | GET | `crons` | --query via api request |
| `ql task detail ` | GET | `crons/detail` | --query |
| `ql task create ` | POST | `crons` | --data, --query |
| `ql task run <id...>` | PUT | `crons/run` | --query via api request |
| `ql task stop <id...>` | PUT | `crons/stop` | --query via api request |
| `ql task labels-delete ` | DELETE | `crons/labels` | --data, --query |
| `ql task labels-create ` | POST | `crons/labels` | --data, --query |
| `ql task disable <id...>` | PUT | `crons/disable` | --query |
| `ql task enable <id...>` | PUT | `crons/enable` | --query |
| `ql task logs <id>` | GET | `crons/:id/log` | --query via api request |
| `ql task update <id>` | PUT | `crons` | --data, --query |
| `ql task delete <id...>` | DELETE | `crons` | --query |
| `ql task pin <id...>` | PUT | `crons/pin` | --query |
| `ql task unpin <id...>` | PUT | `crons/unpin` | --query |
| `ql task import ` | GET | `crons/import` | --query |
| `ql task get <id>` | GET | `crons/:id` | --query via api request |
| `ql task status ` | PUT | `crons/status` | --data, --query |
| `ql task instances <id>` | GET | `crons/:id/instances` | --query |
| `ql task instance-stop <id> <instanceId>` | POST | `crons/:id/instances/:instanceId/stop` | --query |
| `ql task log-files <id>` | GET | `crons/:id/logs` | --query |
| `ql subscription list ` | GET | `subscriptions` | --query via api request |
| `ql subscription create ` | POST | `subscriptions` | --data, --query |
| `ql subscription run <id...>` | PUT | `subscriptions/run` | --query via api request |
| `ql subscription stop <id...>` | PUT | `subscriptions/stop` | --query via api request |
| `ql subscription disable <id...>` | PUT | `subscriptions/disable` | --query via api request |
| `ql subscription enable <id...>` | PUT | `subscriptions/enable` | --query via api request |
| `ql subscription logs <id>` | GET | `subscriptions/:id/log` | --query via api request |
| `ql subscription update <id>` | PUT | `subscriptions` | --data, --query |
| `ql subscription delete <id...>` | DELETE | `subscriptions` | --query |
| `ql subscription get <id>` | GET | `subscriptions/:id` | --query via api request |
| `ql subscription status ` | PUT | `subscriptions/status` | --data, --query |
| `ql subscription log-files <id>` | GET | `subscriptions/:id/logs` | --query |
| `ql app list ` | GET | `apps` | --query |
| `ql app create ` | POST | `apps` | --data, --query |
| `ql app update <id>` | PUT | `apps` | --data, --query |
| `ql app delete <id...>` | DELETE | `apps` | --query |
| `ql app reset-secret <id>` | PUT | `apps/:id/reset-secret` | --query |
| `ql auth login ` | GET | `auth/token` | --query via api request |
| `ql env list ` | GET | `envs` | --query |
| `ql env create ` | POST | `envs` | --data, --query |
| `ql env update <id>` | PUT | `envs` | --data, --query |
| `ql env delete <id...>` | DELETE | `envs` | --query |
| `ql env move <id>` | PUT | `envs/:id/move` | --data, --query |
| `ql env disable <id...>` | PUT | `envs/disable` | --query |
| `ql env enable <id...>` | PUT | `envs/enable` | --query |
| `ql env rename ` | PUT | `envs/name` | --data, --query |
| `ql env get <id>` | GET | `envs/:id` | --query |
| `ql env pin <id...>` | PUT | `envs/pin` | --query |
| `ql env unpin <id...>` | PUT | `envs/unpin` | --query |
| `ql env labels-create ` | POST | `envs/labels` | --data, --query |
| `ql env labels-delete ` | DELETE | `envs/labels` | --data, --query |
| `ql env upload ` | POST | `envs/upload` | --data, --file (env), --query |
| `ql config samples ` | GET | `configs/samples` | --query |
| `ql config list ` | GET | `configs/files` | --query |
| `ql config get ` | GET | `configs/detail` | --query |
| `ql config save ` | POST | `configs/save` | --data, --query |
| `ql script list ` | GET | `scripts` | --query |
| `ql script get ` | GET | `scripts/detail` | --query |
| `ql script create ` | POST | `scripts` | --data, --file (file), --query |
| `ql script update ` | PUT | `scripts` | --data, --file (file), --query |
| `ql script delete ` | DELETE | `scripts` | --data, --query |
| `ql script download ` | POST | `scripts/download` | --data, --output, --query |
| `ql script run ` | PUT | `scripts/run` | --data, --query |
| `ql script stop ` | PUT | `scripts/stop` | --data, --query |
| `ql script rename ` | PUT | `scripts/rename` | --data, --query |
| `ql log list ` | GET | `logs` | --query |
| `ql log get ` | GET | `logs/detail` | --query |
| `ql log delete ` | DELETE | `logs` | --data, --query |
| `ql log download ` | POST | `logs/download` | --data, --output, --query |
| `ql dependency list ` | GET | `dependencies` | --query |
| `ql dependency create ` | POST | `dependencies` | --data, --query |
| `ql dependency update <id>` | PUT | `dependencies` | --data, --query |
| `ql dependency delete <id...>` | DELETE | `dependencies` | --query |
| `ql dependency force-delete <id...>` | DELETE | `dependencies/force` | --query |
| `ql dependency get <id>` | GET | `dependencies/:id` | --query |
| `ql dependency reinstall <id...>` | PUT | `dependencies/reinstall` | --query |
| `ql dependency cancel <id...>` | PUT | `dependencies/cancel` | --query |
| `ql system info ` | GET | `system` | --query |
| `ql system config-get ` | GET | `system/config` | --query |
| `ql system config-log-remove-frequency ` | PUT | `system/config/log-remove-frequency` | --data, --query |
| `ql system config-cron-concurrency ` | PUT | `system/config/cron-concurrency` | --data, --query |
| `ql system config-dependence-proxy ` | PUT | `system/config/dependence-proxy` | --data, --query |
| `ql system config-node-mirror ` | PUT | `system/config/node-mirror` | --data, --query |
| `ql system config-python-mirror ` | PUT | `system/config/python-mirror` | --data, --query |
| `ql system config-linux-mirror ` | PUT | `system/config/linux-mirror` | --data, --query |
| `ql system update-check ` | PUT | `system/update-check` | --query |
| `ql system update ` | PUT | `system/update` | --query |
| `ql system reload ` | PUT | `system/reload` | --data, --query |
| `ql system notify ` | PUT | `system/notify` | --data, --query |
| `ql system command-run ` | PUT | `system/command-run` | --data, --output, --query |
| `ql system command-stop ` | PUT | `system/command-stop` | --data, --query |
| `ql system data-export ` | PUT | `system/data/export` | --data, --output, --query |
| `ql system data-import ` | PUT | `system/data/import` | --data, --file (data), --query |
| `ql system logs ` | GET | `system/log` | --query |
| `ql system logs-delete ` | DELETE | `system/log` | --query |
| `ql system auth-reset ` | PUT | `system/auth/reset` | --data, --query |
| `ql system config-timezone ` | PUT | `system/config/timezone` | --data, --query |
| `ql system config-lang ` | PUT | `system/config/lang` | --data, --query |
| `ql system config-panel-title ` | PUT | `system/config/panel-title` | --data, --query |
| `ql system config-global-ssh-key ` | PUT | `system/config/global-ssh-key` | --data, --query |
| `ql system config-dependence-clean ` | PUT | `system/config/dependence-clean` | --data, --query |
| `ql dashboard record ` | POST | `dashboard/record` | --data, --query |
| `ql dashboard overview ` | GET | `dashboard/overview` | --query |
| `ql dashboard trend ` | GET | `dashboard/trend` | --query |
| `ql dashboard top-time ` | GET | `dashboard/top-time` | --query |
| `ql dashboard top-count ` | GET | `dashboard/top-count` | --query |
| `ql dashboard runtime ` | GET | `dashboard/runtime` | --query |
| `ql dashboard labels ` | GET | `dashboard/labels` | --query |
| `ql dashboard system ` | GET | `dashboard/system` | --query |
| `ql system client-ip-get ` | GET | `system/client-ip/config` | --query |
| `ql system client-ip-set ` | PUT | `system/client-ip/config` | --data, --query |
| `ql system client-ip-diagnose ` | GET | `system/client-ip/diagnose` | --query |
| `ql system retention-set ` | PUT | `system/storage-retention/config` | --data, --query |
| `ql system retention-preview ` | POST | `system/storage-retention/preview` | --data, --query |
| `ql system retention-cleanup ` | POST | `system/storage-retention/cleanup` | --data, --query |
| `ql user login ` | POST | `user/login` | --data, --query |
| `ql user logout ` | POST | `user/logout` | --query |
| `ql user update ` | PUT | `user` | --data, --query |
| `ql user get ` | GET | `user` | --query |
| `ql user two-factor-init ` | GET | `user/two-factor/init` | --query |
| `ql user two-factor-active ` | PUT | `user/two-factor/active` | --data, --query |
| `ql user two-factor-deactivate ` | PUT | `user/two-factor/deactivate` | --query |
| `ql user two-factor-login ` | PUT | `user/two-factor/login` | --data, --query |
| `ql user login-log ` | GET | `user/login-log` | --query |
| `ql user ip-blacklist ` | GET | `user/ip-blacklist` | --query |
| `ql user ip-blacklist-set ` | PUT | `user/ip-blacklist` | --data, --query |
| `ql user ip-blacklist-delete ` | DELETE | `user/ip-blacklist` | --data, --query |
| `ql user notification-get ` | GET | `user/notification` | --query |
| `ql user notification-set ` | PUT | `user/notification` | --data, --query |
| `ql user init ` | PUT | `user/init` | --data, --query |
| `ql user notification-init ` | PUT | `user/notification/init` | --data, --query |
| `ql user avatar ` | PUT | `user/avatar` | --data, --file (avatar), --query |
| `ql system apply-reload ` | PUT | `update/reload` | --query |
| `ql system apply-system ` | PUT | `update/system` | --query |
| `ql system apply-data ` | PUT | `update/data` | --query |
| `ql health get ` | GET | `health` | --query |