mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-09-24 16:05:44 +08:00
Merge pull request #3077 from twodogegg/add-sub2api-admin-skill
docs: add Sub2API admin skill
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
---
|
||||
name: sub2api-admin
|
||||
description: Manage Sub2API admin APIs for accounts, groups, proxies, error passthrough rules, TLS fingerprint profiles, imports, exports, batch updates, and raw administrator API calls. Use when the user mentions Sub2API, admin API keys, account management, bulk account import/export, keeping or deleting accounts, refreshing accounts, clearing errors, CRS sync, or managing Sub2API backend settings through the admin API.
|
||||
---
|
||||
|
||||
# Sub2API Admin
|
||||
|
||||
Use the bundled CLI instead of ad hoc `curl`.
|
||||
|
||||
```bash
|
||||
export SUB2API_BASE_URL='https://your-sub2api-host'
|
||||
export SUB2API_ADMIN_API_KEY='<admin api key>'
|
||||
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list
|
||||
```
|
||||
|
||||
For all commands and payload examples, read [references/admin-cli.md](references/admin-cli.md).
|
||||
|
||||
## Workflow
|
||||
|
||||
1. Reuse `SUB2API_BASE_URL` and `SUB2API_ADMIN_API_KEY` from the environment.
|
||||
2. Run read-only commands first: `accounts list`, `accounts get <id>`, `groups all`, or `proxies all`.
|
||||
3. Before destructive or bulk writes, print the target account names and IDs.
|
||||
4. Execute the write command only after the target set is clear.
|
||||
5. Run a follow-up read command to verify the result.
|
||||
|
||||
## Common Commands
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list --page-size 20
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts get 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts usage 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts set-schedulable 40 true
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts bulk-update --ids 40,39 --json '{"concurrency":10}'
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules list
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles list
|
||||
```
|
||||
|
||||
## Safety Notes
|
||||
|
||||
- Authentication uses only `x-api-key`.
|
||||
- If the API returns `INVALID_ADMIN_KEY`, ask the user to regenerate the admin API key.
|
||||
- `accounts export` includes credentials and tokens. Prefer `--file` and avoid printing exports in chat.
|
||||
- For uncertain or newly added backend APIs, use `api <METHOD> <admin-path>` after a read-only check.
|
||||
@@ -0,0 +1,7 @@
|
||||
interface:
|
||||
display_name: "Sub2API Admin"
|
||||
short_description: "Manage Sub2API admin accounts and settings"
|
||||
default_prompt: "Use $sub2api-admin to inspect and manage Sub2API backend accounts safely."
|
||||
|
||||
policy:
|
||||
allow_implicit_invocation: true
|
||||
@@ -0,0 +1,201 @@
|
||||
# Sub2API Admin Reference
|
||||
|
||||
## Environment
|
||||
|
||||
```bash
|
||||
export SUB2API_BASE_URL='https://your-sub2api-host'
|
||||
export SUB2API_ADMIN_API_KEY='<admin api key>'
|
||||
```
|
||||
|
||||
后台鉴权只使用 `x-api-key`。如果返回 `INVALID_ADMIN_KEY`,重新生成管理员 API Key。
|
||||
|
||||
## CLI
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js <command>
|
||||
```
|
||||
|
||||
## Accounts
|
||||
|
||||
### 只读
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list --page-size 20
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts list --search outlook --platform openai --type oauth --status active
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts get 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts usage 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts stats 40 --days 30
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts today-stats 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-today-stats --ids 40,39
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts models 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts temp-unschedulable 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts antigravity-default-model-mapping
|
||||
```
|
||||
|
||||
`accounts export` 会包含账号凭据和 token,建议写入文件,不要直接刷屏:
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts export --ids 40,39 --file accounts-export.json
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts export --platform openai --type oauth --include-proxies false --file accounts-export.json
|
||||
```
|
||||
|
||||
### 单账号写入
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts create --file account.json
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts update 40 --json '{"concurrency":20}'
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts set-status 40 active
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts set-schedulable 40 true
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts clear-error 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts clear-rate-limit 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts recover-state 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts reset-quota 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts refresh 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts test 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts sync-models 40
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts apply-oauth 40 --file credentials.json
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts reset-temp-unschedulable 40
|
||||
```
|
||||
|
||||
### 删除与清理
|
||||
|
||||
删除前先列出目标账号名和 ID。
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts delete 25
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts keep-only --name 'target@example.com'
|
||||
```
|
||||
|
||||
### 批量写入
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-create --file accounts.json
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-update-credentials --file payload.json
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts bulk-update --ids 40,39 --json '{"concurrency":10,"priority":2}'
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-refresh --ids 40,39
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts batch-clear-error --ids 40,39
|
||||
```
|
||||
|
||||
`bulk-update` 可覆盖页面“批量更新”的字段,payload 由后台表单字段决定,例如 `base_url`、`model_mapping`、`group_ids`、`proxy_id`、`concurrency`、`priority`、`rate_multiplier`、`status`、`compact_mode` 等。更新前先用 `accounts get <id>` 确认字段名。
|
||||
|
||||
### 导入
|
||||
|
||||
通用后台导入:
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts import-data --file accounts-export.json
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts import-codex-session --file payload.json
|
||||
```
|
||||
|
||||
CRS 同步:
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts crs-preview --file payload.json
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts crs-sync --file payload.json
|
||||
```
|
||||
|
||||
旧版 JSON 导入仍可用,会把模板账号的配置复制给导入账号:
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js accounts import-json \
|
||||
--file /path/accounts.json \
|
||||
--template-name 'template@example.com' \
|
||||
--dry-run
|
||||
```
|
||||
|
||||
复制字段:
|
||||
|
||||
- `concurrency`
|
||||
- `priority`
|
||||
- `group_ids`
|
||||
- `credentials.model_mapping`
|
||||
|
||||
## Groups And Proxies
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js groups all
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js proxies all
|
||||
```
|
||||
|
||||
## Error Rules And TLS Profiles
|
||||
|
||||
对应账号页顶部“错误透传规则”和“TLS 指纹模板”。
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules list
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules get 1
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules create --file rule.json
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules update 1 --json '{"enabled":true}'
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules toggle 1 false
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js error-rules delete 1
|
||||
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles list
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles get 1
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles create --file profile.json
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles update 1 --file profile.json
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js tls-profiles delete 1
|
||||
```
|
||||
|
||||
## Raw Admin API
|
||||
|
||||
未封装或新版本后台接口可用 `api` 直通。路径可写 `/admin/...` 或 `/api/v1/admin/...`。
|
||||
|
||||
```bash
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js api GET /admin/groups/all
|
||||
node ~/.codex/skills/sub2api-admin/scripts/sub2api-admin.js api POST /admin/accounts/bulk-update \
|
||||
--json '{"account_ids":[40],"concurrency":10}'
|
||||
```
|
||||
|
||||
## Confirmed Admin Endpoints
|
||||
|
||||
- `GET /api/v1/admin/accounts`
|
||||
- `GET /api/v1/admin/accounts/:id`
|
||||
- `POST /api/v1/admin/accounts`
|
||||
- `PUT /api/v1/admin/accounts/:id`
|
||||
- `DELETE /api/v1/admin/accounts/:id`
|
||||
- `POST /api/v1/admin/accounts/check-mixed-channel`
|
||||
- `GET /api/v1/admin/accounts/:id/usage`
|
||||
- `GET /api/v1/admin/accounts/:id/stats`
|
||||
- `GET /api/v1/admin/accounts/:id/today-stats`
|
||||
- `POST /api/v1/admin/accounts/today-stats/batch`
|
||||
- `POST /api/v1/admin/accounts/:id/schedulable`
|
||||
- `POST /api/v1/admin/accounts/:id/test`
|
||||
- `POST /api/v1/admin/accounts/:id/refresh`
|
||||
- `POST /api/v1/admin/accounts/:id/apply-oauth-credentials`
|
||||
- `POST /api/v1/admin/accounts/:id/clear-error`
|
||||
- `POST /api/v1/admin/accounts/:id/clear-rate-limit`
|
||||
- `POST /api/v1/admin/accounts/:id/recover-state`
|
||||
- `POST /api/v1/admin/accounts/:id/reset-quota`
|
||||
- `GET /api/v1/admin/accounts/:id/temp-unschedulable`
|
||||
- `DELETE /api/v1/admin/accounts/:id/temp-unschedulable`
|
||||
- `GET /api/v1/admin/accounts/:id/models`
|
||||
- `POST /api/v1/admin/accounts/:id/models/sync-upstream`
|
||||
- `POST /api/v1/admin/accounts/batch`
|
||||
- `POST /api/v1/admin/accounts/batch-update-credentials`
|
||||
- `POST /api/v1/admin/accounts/bulk-update`
|
||||
- `POST /api/v1/admin/accounts/batch-refresh`
|
||||
- `POST /api/v1/admin/accounts/batch-clear-error`
|
||||
- `GET /api/v1/admin/accounts/data`
|
||||
- `POST /api/v1/admin/accounts/data`
|
||||
- `POST /api/v1/admin/accounts/import/codex-session`
|
||||
- `POST /api/v1/admin/accounts/sync/crs/preview`
|
||||
- `POST /api/v1/admin/accounts/sync/crs`
|
||||
- `GET /api/v1/admin/accounts/antigravity/default-model-mapping`
|
||||
- `GET /api/v1/admin/groups/all`
|
||||
- `GET /api/v1/admin/proxies/all`
|
||||
- `GET /api/v1/admin/error-passthrough-rules`
|
||||
- `GET /api/v1/admin/error-passthrough-rules/:id`
|
||||
- `POST /api/v1/admin/error-passthrough-rules`
|
||||
- `PUT /api/v1/admin/error-passthrough-rules/:id`
|
||||
- `DELETE /api/v1/admin/error-passthrough-rules/:id`
|
||||
- `GET /api/v1/admin/tls-fingerprint-profiles`
|
||||
- `GET /api/v1/admin/tls-fingerprint-profiles/:id`
|
||||
- `POST /api/v1/admin/tls-fingerprint-profiles`
|
||||
- `PUT /api/v1/admin/tls-fingerprint-profiles/:id`
|
||||
- `DELETE /api/v1/admin/tls-fingerprint-profiles/:id`
|
||||
|
||||
## Notes
|
||||
|
||||
- 线上写入前先只读核对目标集合。
|
||||
- 导出结果包含敏感凭据,优先使用 `--file`。
|
||||
- `PUT /admin/accounts/:id` 和 `bulk-update` 接受宽松请求体,字段名不确定时先用 `accounts get` 或后台页面确认。
|
||||
+613
@@ -0,0 +1,613 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
|
||||
const BASE_URL = (process.env.SUB2API_BASE_URL || "").replace(/\/$/, "");
|
||||
const ADMIN_API_KEY = process.env.SUB2API_ADMIN_API_KEY || "";
|
||||
|
||||
function usage() {
|
||||
console.log(`Usage:
|
||||
sub2api-admin.js accounts list [--page-size 200] [--page N] [--search TEXT] [--platform openai] [--type oauth] [--status active] [--group NAME] [--privacy-mode MODE] [--sort-by name] [--sort-order asc]
|
||||
sub2api-admin.js accounts export [--ids 1,2] [--file accounts.json] [--include-proxies false] [list filters...]
|
||||
sub2api-admin.js accounts import-data --file accounts.json [--skip-default-group-bind]
|
||||
sub2api-admin.js accounts create --json '{...}' | --file account.json
|
||||
sub2api-admin.js accounts update <id> --json '{...}' | --file patch.json
|
||||
sub2api-admin.js accounts get <id>
|
||||
sub2api-admin.js accounts delete <id>
|
||||
sub2api-admin.js accounts keep-only --name <account-name>
|
||||
sub2api-admin.js accounts usage <id> [--source SOURCE] [--force]
|
||||
sub2api-admin.js accounts stats <id> [--days 30]
|
||||
sub2api-admin.js accounts today-stats <id>
|
||||
sub2api-admin.js accounts batch-today-stats --ids 1,2
|
||||
sub2api-admin.js accounts set-status <id> <active|paused|...>
|
||||
sub2api-admin.js accounts set-schedulable <id> <true|false>
|
||||
sub2api-admin.js accounts clear-error <id>
|
||||
sub2api-admin.js accounts clear-rate-limit <id>
|
||||
sub2api-admin.js accounts recover-state <id>
|
||||
sub2api-admin.js accounts reset-quota <id>
|
||||
sub2api-admin.js accounts refresh <id>
|
||||
sub2api-admin.js accounts test <id>
|
||||
sub2api-admin.js accounts models <id>
|
||||
sub2api-admin.js accounts sync-models <id>
|
||||
sub2api-admin.js accounts apply-oauth <id> --json '{...}' | --file credentials.json
|
||||
sub2api-admin.js accounts batch-create --file accounts.json
|
||||
sub2api-admin.js accounts batch-update-credentials --json '{...}' | --file payload.json
|
||||
sub2api-admin.js accounts bulk-update --ids 1,2 --json '{...}' | --file patch.json
|
||||
sub2api-admin.js accounts batch-refresh --ids 1,2
|
||||
sub2api-admin.js accounts batch-clear-error --ids 1,2
|
||||
sub2api-admin.js accounts temp-unschedulable <id>
|
||||
sub2api-admin.js accounts reset-temp-unschedulable <id>
|
||||
sub2api-admin.js accounts crs-preview --json '{...}' | --file payload.json
|
||||
sub2api-admin.js accounts crs-sync --json '{...}' | --file payload.json
|
||||
sub2api-admin.js accounts import-codex-session --json '{...}' | --file payload.json
|
||||
sub2api-admin.js accounts antigravity-default-model-mapping
|
||||
sub2api-admin.js accounts import-json --file <path> --template-name <name> [--skip-name <name>] [--dry-run]
|
||||
sub2api-admin.js groups all
|
||||
sub2api-admin.js proxies all
|
||||
sub2api-admin.js error-rules list|get|create|update|delete|toggle ...
|
||||
sub2api-admin.js tls-profiles list|get|create|update|delete ...
|
||||
sub2api-admin.js api <GET|POST|PUT|DELETE> <admin-path> [--json '{...}' | --file payload.json]
|
||||
`);
|
||||
}
|
||||
|
||||
function parseArgs(argv) {
|
||||
const positional = [];
|
||||
const flags = {};
|
||||
for (let i = 0; i < argv.length; i += 1) {
|
||||
const token = argv[i];
|
||||
if (token.startsWith("--")) {
|
||||
const key = token.slice(2);
|
||||
const next = argv[i + 1];
|
||||
if (!next || next.startsWith("--")) {
|
||||
flags[key] = true;
|
||||
} else {
|
||||
if (flags[key] === undefined) {
|
||||
flags[key] = next;
|
||||
} else if (Array.isArray(flags[key])) {
|
||||
flags[key].push(next);
|
||||
} else {
|
||||
flags[key] = [flags[key], next];
|
||||
}
|
||||
i += 1;
|
||||
}
|
||||
} else {
|
||||
positional.push(token);
|
||||
}
|
||||
}
|
||||
return { positional, flags };
|
||||
}
|
||||
|
||||
function authHeaders() {
|
||||
if (!BASE_URL) throw new Error("Missing SUB2API_BASE_URL");
|
||||
if (ADMIN_API_KEY) return { "x-api-key": ADMIN_API_KEY };
|
||||
throw new Error("Missing SUB2API_ADMIN_API_KEY");
|
||||
}
|
||||
|
||||
async function apiRequest(method, pathname, body) {
|
||||
const headers = {
|
||||
...authHeaders(),
|
||||
Accept: "application/json",
|
||||
};
|
||||
const options = { method, headers };
|
||||
if (body !== undefined) {
|
||||
headers["Content-Type"] = "application/json";
|
||||
options.body = JSON.stringify(body);
|
||||
}
|
||||
const res = await fetch(`${BASE_URL}${pathname}`, options);
|
||||
const text = await res.text();
|
||||
let data;
|
||||
try {
|
||||
data = JSON.parse(text);
|
||||
} catch {
|
||||
data = { raw: text };
|
||||
}
|
||||
if (!res.ok || (data && data.code !== undefined && data.code !== 0 && data.code !== "0")) {
|
||||
const detail = data.message || data.code || res.statusText;
|
||||
throw new Error(`${method} ${pathname} failed: ${detail}`);
|
||||
}
|
||||
return data.data;
|
||||
}
|
||||
|
||||
function encodeQuery(params) {
|
||||
const pairs = [];
|
||||
for (const [key, value] of Object.entries(params)) {
|
||||
if (value === undefined || value === null || value === false || value === "") continue;
|
||||
pairs.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
|
||||
}
|
||||
return pairs.length ? `?${pairs.join("&")}` : "";
|
||||
}
|
||||
|
||||
function normalizeAdminPath(pathname) {
|
||||
if (!pathname.startsWith("/")) pathname = `/${pathname}`;
|
||||
if (pathname.startsWith("/api/v1/admin/")) return pathname;
|
||||
if (pathname.startsWith("/admin/")) return `/api/v1${pathname}`;
|
||||
return pathname;
|
||||
}
|
||||
|
||||
async function adminRequest(method, adminPath, body) {
|
||||
return apiRequest(method, normalizeAdminPath(adminPath), body);
|
||||
}
|
||||
|
||||
async function listAccounts(options = {}) {
|
||||
const params = {
|
||||
page: options.page || 1,
|
||||
page_size: options.pageSize || 200,
|
||||
sort_by: options.sortBy || "name",
|
||||
sort_order: options.sortOrder || "asc",
|
||||
lite: options.lite === false ? undefined : 1,
|
||||
search: options.search,
|
||||
platform: options.platform,
|
||||
type: options.type,
|
||||
status: options.status,
|
||||
group: options.group,
|
||||
privacy_mode: options.privacyMode,
|
||||
};
|
||||
return apiRequest("GET", `/api/v1/admin/accounts${encodeQuery(params)}`);
|
||||
}
|
||||
|
||||
async function getAccount(id) {
|
||||
return apiRequest("GET", `/api/v1/admin/accounts/${id}`);
|
||||
}
|
||||
|
||||
async function deleteAccount(id) {
|
||||
return apiRequest("DELETE", `/api/v1/admin/accounts/${id}`);
|
||||
}
|
||||
|
||||
function asArray(value) {
|
||||
if (value === undefined) return [];
|
||||
return Array.isArray(value) ? value : [value];
|
||||
}
|
||||
|
||||
function parseBool(value, name = "value") {
|
||||
if (value === true || value === "true" || value === "1" || value === "yes") return true;
|
||||
if (value === false || value === "false" || value === "0" || value === "no") return false;
|
||||
throw new Error(`${name} must be true or false`);
|
||||
}
|
||||
|
||||
function parseIds(value) {
|
||||
if (!value) throw new Error("requires --ids");
|
||||
return String(value)
|
||||
.split(",")
|
||||
.map((id) => id.trim())
|
||||
.filter(Boolean)
|
||||
.map((id) => {
|
||||
const n = Number(id);
|
||||
if (!Number.isFinite(n)) throw new Error(`invalid id: ${id}`);
|
||||
return n;
|
||||
});
|
||||
}
|
||||
|
||||
function readJsonPayload(flags, { required = true } = {}) {
|
||||
if (flags.json !== undefined) return JSON.parse(flags.json);
|
||||
if (flags.file !== undefined) return JSON.parse(fs.readFileSync(path.resolve(flags.file), "utf8"));
|
||||
if (required) throw new Error("requires --json or --file");
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function accountListOptions(flags) {
|
||||
return {
|
||||
page: Number(flags.page || 1),
|
||||
pageSize: Number(flags["page-size"] || 200),
|
||||
search: flags.search,
|
||||
platform: flags.platform,
|
||||
type: flags.type,
|
||||
status: flags.status,
|
||||
group: flags.group,
|
||||
privacyMode: flags["privacy-mode"],
|
||||
sortBy: flags["sort-by"] || "name",
|
||||
sortOrder: flags["sort-order"] || "asc",
|
||||
lite: flags.lite === undefined ? true : parseBool(flags.lite, "--lite"),
|
||||
};
|
||||
}
|
||||
|
||||
function printJson(data) {
|
||||
console.log(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
async function accountData(flags) {
|
||||
const params = {};
|
||||
if (flags.ids) {
|
||||
params.ids = parseIds(flags.ids).join(",");
|
||||
} else {
|
||||
const map = {
|
||||
platform: "platform",
|
||||
type: "type",
|
||||
status: "status",
|
||||
group: "group",
|
||||
search: "search",
|
||||
"privacy-mode": "privacy_mode",
|
||||
"sort-by": "sort_by",
|
||||
"sort-order": "sort_order",
|
||||
};
|
||||
for (const [flag, param] of Object.entries(map)) {
|
||||
if (flags[flag]) params[param] = flags[flag];
|
||||
}
|
||||
}
|
||||
if (flags["include-proxies"] !== undefined) {
|
||||
params.include_proxies = String(parseBool(flags["include-proxies"], "--include-proxies"));
|
||||
}
|
||||
return adminRequest("GET", `/admin/accounts/data${encodeQuery(params)}`);
|
||||
}
|
||||
|
||||
async function commandAccounts(args) {
|
||||
const sub = args.positional[1];
|
||||
if (sub === "list") {
|
||||
const data = await listAccounts(accountListOptions(args.flags));
|
||||
printJson(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "export") {
|
||||
const data = await accountData(args.flags);
|
||||
if (args.flags.file) {
|
||||
fs.writeFileSync(path.resolve(args.flags.file), JSON.stringify(data, null, 2));
|
||||
printJson({ file: path.resolve(args.flags.file), count: Array.isArray(data) ? data.length : data.count });
|
||||
} else {
|
||||
printJson(data);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "import-data") {
|
||||
const file = args.flags.file;
|
||||
if (!file) throw new Error("accounts import-data requires --file");
|
||||
const payload = {
|
||||
data: JSON.parse(fs.readFileSync(path.resolve(file), "utf8")),
|
||||
skip_default_group_bind: Boolean(args.flags["skip-default-group-bind"]),
|
||||
};
|
||||
printJson(await adminRequest("POST", "/admin/accounts/data", payload));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "create") {
|
||||
printJson(await adminRequest("POST", "/admin/accounts", readJsonPayload(args.flags)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "update") {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error("accounts update requires <id>");
|
||||
printJson(await adminRequest("PUT", `/admin/accounts/${id}`, readJsonPayload(args.flags)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "get") {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error("accounts get requires <id>");
|
||||
const data = await getAccount(id);
|
||||
printJson(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "delete") {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error("accounts delete requires <id>");
|
||||
const data = await deleteAccount(id);
|
||||
printJson(data);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "keep-only") {
|
||||
const name = args.flags.name;
|
||||
if (!name) throw new Error("accounts keep-only requires --name");
|
||||
const data = await listAccounts({ pageSize: Number(args.flags["page-size"] || 500) });
|
||||
const items = data.items || [];
|
||||
const keep = items.find((item) => item.name === name);
|
||||
if (!keep) throw new Error(`account not found: ${name}`);
|
||||
const targets = items.filter((item) => item.name !== name);
|
||||
const results = [];
|
||||
for (const item of targets) {
|
||||
const out = await deleteAccount(item.id);
|
||||
results.push({ id: item.id, name: item.name, result: out });
|
||||
}
|
||||
printJson({ kept: { id: keep.id, name: keep.name }, deleted: results });
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "usage") {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error("accounts usage requires <id>");
|
||||
const params = encodeQuery({ source: args.flags.source, force: args.flags.force ? "true" : undefined });
|
||||
printJson(await adminRequest("GET", `/admin/accounts/${id}/usage${params}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "stats") {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error("accounts stats requires <id>");
|
||||
printJson(await adminRequest("GET", `/admin/accounts/${id}/stats${encodeQuery({ days: args.flags.days || 30 })}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "today-stats") {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error("accounts today-stats requires <id>");
|
||||
printJson(await adminRequest("GET", `/admin/accounts/${id}/today-stats`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "batch-today-stats") {
|
||||
printJson(await adminRequest("POST", "/admin/accounts/today-stats/batch", { account_ids: parseIds(args.flags.ids) }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "set-status") {
|
||||
const id = args.positional[2];
|
||||
const status = args.positional[3];
|
||||
if (!id || !status) throw new Error("accounts set-status requires <id> <status>");
|
||||
printJson(await adminRequest("PUT", `/admin/accounts/${id}`, { status }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "set-schedulable") {
|
||||
const id = args.positional[2];
|
||||
const schedulable = args.positional[3];
|
||||
if (!id || schedulable === undefined) throw new Error("accounts set-schedulable requires <id> <true|false>");
|
||||
printJson(await adminRequest("POST", `/admin/accounts/${id}/schedulable`, { schedulable: parseBool(schedulable, "schedulable") }));
|
||||
return;
|
||||
}
|
||||
|
||||
const noBodyPostById = {
|
||||
"clear-error": "clear-error",
|
||||
"clear-rate-limit": "clear-rate-limit",
|
||||
"recover-state": "recover-state",
|
||||
"reset-quota": "reset-quota",
|
||||
refresh: "refresh",
|
||||
test: "test",
|
||||
"sync-models": "models/sync-upstream",
|
||||
"set-privacy": "set-privacy",
|
||||
};
|
||||
if (noBodyPostById[sub]) {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error(`accounts ${sub} requires <id>`);
|
||||
printJson(await adminRequest("POST", `/admin/accounts/${id}/${noBodyPostById[sub]}`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "models") {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error("accounts models requires <id>");
|
||||
printJson(await adminRequest("GET", `/admin/accounts/${id}/models`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "apply-oauth") {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error("accounts apply-oauth requires <id>");
|
||||
printJson(await adminRequest("POST", `/admin/accounts/${id}/apply-oauth-credentials`, readJsonPayload(args.flags)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "batch-create") {
|
||||
const payload = readJsonPayload(args.flags);
|
||||
const accounts = Array.isArray(payload) ? payload : payload.accounts;
|
||||
if (!Array.isArray(accounts)) throw new Error("batch-create payload must be an array or {accounts:[...]}");
|
||||
printJson(await adminRequest("POST", "/admin/accounts/batch", { accounts }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "batch-update-credentials") {
|
||||
printJson(await adminRequest("POST", "/admin/accounts/batch-update-credentials", readJsonPayload(args.flags)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "bulk-update") {
|
||||
const patch = readJsonPayload(args.flags, { required: false }) || {};
|
||||
const payload = args.flags.ids ? { account_ids: parseIds(args.flags.ids), ...patch } : patch;
|
||||
if (!Array.isArray(payload.account_ids) || payload.account_ids.length === 0) {
|
||||
throw new Error("accounts bulk-update requires --ids or payload.account_ids");
|
||||
}
|
||||
printJson(await adminRequest("POST", "/admin/accounts/bulk-update", payload));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "batch-refresh") {
|
||||
printJson(await adminRequest("POST", "/admin/accounts/batch-refresh", { account_ids: parseIds(args.flags.ids) }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "batch-clear-error") {
|
||||
printJson(await adminRequest("POST", "/admin/accounts/batch-clear-error", { account_ids: parseIds(args.flags.ids) }));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "temp-unschedulable") {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error("accounts temp-unschedulable requires <id>");
|
||||
printJson(await adminRequest("GET", `/admin/accounts/${id}/temp-unschedulable`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "reset-temp-unschedulable") {
|
||||
const id = args.positional[2];
|
||||
if (!id) throw new Error("accounts reset-temp-unschedulable requires <id>");
|
||||
printJson(await adminRequest("DELETE", `/admin/accounts/${id}/temp-unschedulable`));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "crs-preview") {
|
||||
printJson(await adminRequest("POST", "/admin/accounts/sync/crs/preview", readJsonPayload(args.flags)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "crs-sync") {
|
||||
printJson(await adminRequest("POST", "/admin/accounts/sync/crs", readJsonPayload(args.flags)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "import-codex-session") {
|
||||
printJson(await adminRequest("POST", "/admin/accounts/import/codex-session", readJsonPayload(args.flags)));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "antigravity-default-model-mapping") {
|
||||
printJson(await adminRequest("GET", "/admin/accounts/antigravity/default-model-mapping"));
|
||||
return;
|
||||
}
|
||||
|
||||
if (sub === "import-json") {
|
||||
const file = args.flags.file;
|
||||
const templateName = args.flags["template-name"];
|
||||
const dryRun = Boolean(args.flags["dry-run"]);
|
||||
const skipNames = new Set(asArray(args.flags["skip-name"]));
|
||||
if (!file) throw new Error("accounts import-json requires --file");
|
||||
if (!templateName) throw new Error("accounts import-json requires --template-name");
|
||||
|
||||
const raw = JSON.parse(fs.readFileSync(path.resolve(file), "utf8"));
|
||||
const accounts = raw.accounts || [];
|
||||
const live = await listAccounts({ pageSize: 500 });
|
||||
const liveItems = live.items || [];
|
||||
const template = liveItems.find((item) => item.name === templateName);
|
||||
if (!template) throw new Error(`template account not found in backend: ${templateName}`);
|
||||
|
||||
const templateGroupIds = template.group_ids || [];
|
||||
const templateConcurrency = template.concurrency;
|
||||
const templatePriority = template.priority;
|
||||
const templateModelMapping = (template.credentials && template.credentials.model_mapping) || {};
|
||||
|
||||
const existingNames = new Set(liveItems.map((item) => item.name));
|
||||
const planned = accounts.filter((acc) => acc.name !== templateName && !skipNames.has(acc.name));
|
||||
|
||||
if (dryRun) {
|
||||
printJson({
|
||||
template: {
|
||||
name: template.name,
|
||||
concurrency: templateConcurrency,
|
||||
priority: templatePriority,
|
||||
group_ids: templateGroupIds,
|
||||
model_mapping: templateModelMapping,
|
||||
},
|
||||
to_import: planned.map((acc) => ({
|
||||
name: acc.name,
|
||||
exists: existingNames.has(acc.name),
|
||||
})),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const results = [];
|
||||
for (const acc of planned) {
|
||||
if (existingNames.has(acc.name)) {
|
||||
results.push({ name: acc.name, skipped: true, reason: "already exists" });
|
||||
continue;
|
||||
}
|
||||
const payload = {
|
||||
...acc,
|
||||
group_ids: templateGroupIds,
|
||||
concurrency: templateConcurrency,
|
||||
priority: templatePriority,
|
||||
credentials: {
|
||||
...acc.credentials,
|
||||
model_mapping: templateModelMapping,
|
||||
},
|
||||
};
|
||||
const created = await apiRequest("POST", "/api/v1/admin/accounts", payload);
|
||||
results.push({ name: acc.name, id: created.id, skipped: false });
|
||||
}
|
||||
printJson({ imported: results });
|
||||
return;
|
||||
}
|
||||
|
||||
throw new Error(`unknown accounts subcommand: ${sub || "(missing)"}`);
|
||||
}
|
||||
|
||||
async function commandGroups(args) {
|
||||
const sub = args.positional[1];
|
||||
if (sub === "all") {
|
||||
printJson(await adminRequest("GET", "/admin/groups/all"));
|
||||
return;
|
||||
}
|
||||
throw new Error(`unknown groups subcommand: ${sub || "(missing)"}`);
|
||||
}
|
||||
|
||||
async function commandProxies(args) {
|
||||
const sub = args.positional[1];
|
||||
if (sub === "all") {
|
||||
printJson(await adminRequest("GET", "/admin/proxies/all"));
|
||||
return;
|
||||
}
|
||||
throw new Error(`unknown proxies subcommand: ${sub || "(missing)"}`);
|
||||
}
|
||||
|
||||
async function commandCrudResource(args, name, basePath, options = {}) {
|
||||
const sub = args.positional[1];
|
||||
const id = args.positional[2];
|
||||
if (sub === "list") {
|
||||
printJson(await adminRequest("GET", basePath));
|
||||
return;
|
||||
}
|
||||
if (sub === "get") {
|
||||
if (!id) throw new Error(`${name} get requires <id>`);
|
||||
printJson(await adminRequest("GET", `${basePath}/${id}`));
|
||||
return;
|
||||
}
|
||||
if (sub === "create") {
|
||||
printJson(await adminRequest("POST", basePath, readJsonPayload(args.flags)));
|
||||
return;
|
||||
}
|
||||
if (sub === "update") {
|
||||
if (!id) throw new Error(`${name} update requires <id>`);
|
||||
printJson(await adminRequest("PUT", `${basePath}/${id}`, readJsonPayload(args.flags)));
|
||||
return;
|
||||
}
|
||||
if (sub === "delete") {
|
||||
if (!id) throw new Error(`${name} delete requires <id>`);
|
||||
printJson(await adminRequest("DELETE", `${basePath}/${id}`));
|
||||
return;
|
||||
}
|
||||
if (options.toggle && sub === "toggle") {
|
||||
const enabled = args.positional[3];
|
||||
if (!id || enabled === undefined) throw new Error(`${name} toggle requires <id> <true|false>`);
|
||||
printJson(await adminRequest("PUT", `${basePath}/${id}`, { enabled: parseBool(enabled, "enabled") }));
|
||||
return;
|
||||
}
|
||||
throw new Error(`unknown ${name} subcommand: ${sub || "(missing)"}`);
|
||||
}
|
||||
|
||||
async function commandApi(args) {
|
||||
const method = args.positional[1];
|
||||
const pathname = args.positional[2];
|
||||
if (!method || !pathname) throw new Error("api requires <GET|POST|PUT|DELETE> <admin-path>");
|
||||
const body = readJsonPayload(args.flags, { required: false });
|
||||
printJson(await adminRequest(method.toUpperCase(), pathname, body));
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
const root = args.positional[0];
|
||||
if (!root) {
|
||||
usage();
|
||||
process.exit(1);
|
||||
}
|
||||
if (root === "accounts") {
|
||||
await commandAccounts(args);
|
||||
return;
|
||||
}
|
||||
if (root === "groups") {
|
||||
await commandGroups(args);
|
||||
return;
|
||||
}
|
||||
if (root === "proxies") {
|
||||
await commandProxies(args);
|
||||
return;
|
||||
}
|
||||
if (root === "error-rules") {
|
||||
await commandCrudResource(args, "error-rules", "/admin/error-passthrough-rules", { toggle: true });
|
||||
return;
|
||||
}
|
||||
if (root === "tls-profiles") {
|
||||
await commandCrudResource(args, "tls-profiles", "/admin/tls-fingerprint-profiles");
|
||||
return;
|
||||
}
|
||||
if (root === "api") {
|
||||
await commandApi(args);
|
||||
return;
|
||||
}
|
||||
throw new Error(`unknown command: ${root}`);
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error.message);
|
||||
process.exit(1);
|
||||
});
|
||||
Reference in New Issue
Block a user