mirror of
https://github.com/whyour/qinglong.git
synced 2026-11-06 17:34:34 +08:00
fix(cli): expose asynchronous dependency refresh operations
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
# OpenAPI 全量参考 / Complete OpenAPI reference
|
||||
|
||||
CLI 对应本版本后端的 146 条有效路由。通过 `ql api routes --json` 查看方法、路径、对应命令、位置参数、请求体与上传字段;CLI 测试与 back/api 路由逐项核对。GET configs/:file、scripts/:file、logs/:file 已返回 410,改用 detail 命令,不再暴露旧入口。旧版面板可能没有新路由,以服务端响应为准。
|
||||
CLI 对应本版本后端的 151 条有效路由。通过 `ql api routes --json` 查看方法、路径、对应命令、位置参数、请求体与上传字段;CLI 测试与 back/api 路由逐项核对。GET configs/:file、scripts/:file、logs/:file 已返回 410,改用 detail 命令,不再暴露旧入口。旧版面板可能没有新路由,以服务端响应为准。
|
||||
|
||||
The catalogue covers 146 active routes in the current 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.
|
||||
The catalogue covers 151 active routes in the current 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
|
||||
|
||||
@@ -53,12 +53,19 @@ 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 dependency refresh --data '{"type":"python3"}' --json
|
||||
ql dependency refresh-active --json
|
||||
ql dependency refresh-status <JOB_UUID> --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
|
||||
```
|
||||
|
||||
依赖 refresh 的 type 为 nodejs 或 python3,立即返回后台任务 id 与 state;用 refresh-status 查询该 UUID,直到 completed 或 failed。refresh-active 返回运行中的任务或 null,普通 list 不触发检查。
|
||||
|
||||
Dependency refresh accepts type nodejs or python3 and immediately returns a background job id and state. Poll refresh-status with that UUID until completed or failed. refresh-active returns the running job or null; listing dependencies does not trigger checks.
|
||||
|
||||
任务 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.
|
||||
@@ -151,6 +158,9 @@ System/user operations below execute remotely through the panel API. They do not
|
||||
| `ql log delete ` | DELETE | `logs` | --data, --query |
|
||||
| `ql log download ` | POST | `logs/download` | --data, --output, --query |
|
||||
| `ql dependency list ` | GET | `dependencies` | --query |
|
||||
| `ql dependency refresh ` | POST | `dependencies/refresh` | --data, --query |
|
||||
| `ql dependency refresh-active ` | GET | `dependencies/refresh/active` | --query |
|
||||
| `ql dependency refresh-status <id>` | GET | `dependencies/refresh/:id` | --query |
|
||||
| `ql dependency create ` | POST | `dependencies` | --data, --query |
|
||||
| `ql dependency update <id>` | PUT | `dependencies` | --data, --query |
|
||||
| `ql dependency delete <id...>` | DELETE | `dependencies` | --query |
|
||||
|
||||
@@ -5,6 +5,7 @@ export interface OpenOperation {
|
||||
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
|
||||
path: string;
|
||||
params?: string[];
|
||||
uuidParams?: string[];
|
||||
body?: 'ids' | 'json' | 'object-id';
|
||||
upload?: string;
|
||||
download?: boolean;
|
||||
@@ -334,6 +335,24 @@ export const openOperations: readonly OpenOperation[] = [
|
||||
download: true,
|
||||
},
|
||||
{ name: 'dependency list', method: 'GET', path: 'dependencies' },
|
||||
{
|
||||
name: 'dependency refresh',
|
||||
method: 'POST',
|
||||
path: 'dependencies/refresh',
|
||||
body: 'json',
|
||||
},
|
||||
{
|
||||
name: 'dependency refresh-active',
|
||||
method: 'GET',
|
||||
path: 'dependencies/refresh/active',
|
||||
},
|
||||
{
|
||||
name: 'dependency refresh-status',
|
||||
method: 'GET',
|
||||
path: 'dependencies/refresh/:id',
|
||||
params: ['id'],
|
||||
uuidParams: ['id'],
|
||||
},
|
||||
{
|
||||
name: 'dependency create',
|
||||
method: 'POST',
|
||||
|
||||
@@ -49,12 +49,19 @@ async function input(value: string | boolean | undefined): Promise<unknown> {
|
||||
}
|
||||
}
|
||||
|
||||
const UUID_PATTERN =
|
||||
'[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}';
|
||||
|
||||
function route(method: string, path: string): OpenOperation {
|
||||
const match = openOperations.find(
|
||||
(op) =>
|
||||
op.method === method &&
|
||||
new RegExp(
|
||||
'^' + op.path.replace(/:[A-Za-z]+/g, '[1-9][0-9]*') + '$',
|
||||
'^' +
|
||||
op.path.replace(/:([A-Za-z]+)/g, (_match, name) =>
|
||||
op.uuidParams?.includes(name) ? UUID_PATTERN : '[1-9][0-9]*',
|
||||
) +
|
||||
'$',
|
||||
).test(path),
|
||||
);
|
||||
if (!match)
|
||||
@@ -63,7 +70,8 @@ function route(method: string, path: string): OpenOperation {
|
||||
'Method/path is not a supported OpenAPI route.',
|
||||
);
|
||||
for (const [index, part] of match.path.split('/').entries())
|
||||
if (part.startsWith(':')) integer(path.split('/')[index]!);
|
||||
if (part.startsWith(':') && !match.uuidParams?.includes(part.slice(1)))
|
||||
integer(path.split('/')[index]!);
|
||||
return match;
|
||||
}
|
||||
|
||||
@@ -107,7 +115,11 @@ export async function openCommand(
|
||||
if (!generic) {
|
||||
endpoint = op.path;
|
||||
for (const [index, name] of (op.params ?? []).entries()) {
|
||||
const value = integer(command.positionals[index]!);
|
||||
const positional = command.positionals[index]!;
|
||||
const isUuid = op.uuidParams?.includes(name);
|
||||
if (isUuid && !new RegExp(`^${UUID_PATTERN}$`).test(positional))
|
||||
usage('任务 ID 必须为 UUID。', 'Job ID must be a UUID.');
|
||||
const value = isUuid ? positional : integer(positional);
|
||||
endpoint = endpoint.replace(`:${name}`, String(value));
|
||||
if (op.body === 'object-id') {
|
||||
if (body !== undefined && !isRecord(body))
|
||||
|
||||
@@ -83,6 +83,27 @@ test('script history commands send exact query parameters and restore payloads',
|
||||
assert.equal(f.requests.length, count, 'missing restore payload must not issue a request');
|
||||
});
|
||||
|
||||
test('dependency refresh commands send the dependency type and address background jobs', async t => {
|
||||
const f = await fixture(t);
|
||||
success(await f.run(['dependency', 'refresh', '--data', '{"type":"python3"}']));
|
||||
assert.equal(f.requests[0].method, 'POST');
|
||||
assert.equal(f.requests[0].url, '/panel/open/dependencies/refresh');
|
||||
assert.deepEqual(JSON.parse(f.requests[0].body), { type: 'python3' });
|
||||
const id = '32ec8b22-2c48-4bcf-85d4-b394198417ba';
|
||||
success(await f.run(['dependency', 'refresh-status', id]));
|
||||
assert.equal(f.requests[1].method, 'GET');
|
||||
assert.equal(f.requests[1].url, `/panel/open/dependencies/refresh/${id}`);
|
||||
success(await f.run(['dependency', 'refresh-active']));
|
||||
assert.equal(f.requests[2].method, 'GET');
|
||||
assert.equal(f.requests[2].url, '/panel/open/dependencies/refresh/active');
|
||||
success(await f.run(['api', 'request', 'GET', `/open/dependencies/refresh/${id}`]));
|
||||
assert.equal(f.requests[3].url, `/panel/open/dependencies/refresh/${id}`);
|
||||
assert.equal((await f.run(['dependency', 'refresh-status', '7'])).code, 2);
|
||||
assert.equal((await f.run(['api', 'request', 'GET', '/open/dependencies/refresh/7'])).code, 2);
|
||||
assert.equal((await f.run(['dependency', 'refresh'])).code, 2);
|
||||
assert.equal(f.requests.length, 4, 'invalid arguments must not issue a request');
|
||||
});
|
||||
|
||||
test('OpenAPI catalogue covers every active registered backend route; retired 410 routes are explicit', async () => {
|
||||
const root = path.resolve(__dirname, '../../../back/api');
|
||||
const expected = [];
|
||||
@@ -132,7 +153,8 @@ test('all added named operations reach their registered method/path, including u
|
||||
const file = path.join(f.root, 'fixture.json'); await fs.writeFile(file, '{}');
|
||||
for (const op of openOperations.filter(op => !op.existing)) {
|
||||
const args = op.name.split(' ');
|
||||
for (const _ of op.params ?? []) args.push('7');
|
||||
const parameter = name => op.uuidParams?.includes(name) ? '32ec8b22-2c48-4bcf-85d4-b394198417ba' : '7';
|
||||
for (const name of op.params ?? []) args.push(parameter(name));
|
||||
if (op.body === 'ids') args.push('7', '8');
|
||||
else if (op.body) {
|
||||
const data = op.name.startsWith('task ') ? { command: 'echo fixture', schedule: '0 0 * * *' }
|
||||
@@ -144,7 +166,7 @@ test('all added named operations reach their registered method/path, including u
|
||||
success(await f.run(args));
|
||||
const last = f.requests.at(-1);
|
||||
assert.equal(last.method, op.method, op.name);
|
||||
assert.equal(last.url, '/panel/open/' + op.path.replace(/:[A-Za-z]+/g, '7'), op.name);
|
||||
assert.equal(last.url, '/panel/open/' + op.path.replace(/:([A-Za-z]+)/g, (_match, name) => parameter(name)), op.name);
|
||||
assert.equal(last.headers.authorization, op.anonymous ? undefined : 'Bearer fixture-token', op.name);
|
||||
if (op.upload) assert.ok(last.body.includes(`name="${op.upload}"; filename="fixture.json"`), op.name);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user