flower-trellis 0.4.0-beta.5 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,14 +1,13 @@
1
1
  {
2
- "syncedAt": "2026-07-02T01:39:38.800Z",
2
+ "syncedAt": "2026-07-02T11:06:42.782Z",
3
3
  "syncedFrom": "vendor/skill-garden",
4
- "sourceCommit": "39a09335cc3e2878c1e1121137dd55524882b13f",
4
+ "sourceCommit": "a70185e3b372058363296e81e2138f4f239d6e3b",
5
5
  "common": {
6
6
  "codexSkills": [
7
7
  "craft-rpa",
8
8
  "craft-slides",
9
9
  "humanize-writing",
10
10
  "open-idea",
11
- "sub2api-account-json-fix",
12
11
  "torrent-analyze"
13
12
  ],
14
13
  "claudeSkills": [
@@ -16,7 +15,6 @@
16
15
  "craft-slides",
17
16
  "humanize-writing",
18
17
  "open-idea",
19
- "sub2api-account-json-fix",
20
18
  "torrent-analyze"
21
19
  ]
22
20
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "flower-trellis",
3
- "version": "0.4.0-beta.5",
3
+ "version": "0.4.0",
4
4
  "description": "一键安装/升级 Trellis 并自动融合 skill-garden 强化包(默认 Claude + agents)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -40,7 +40,6 @@ const SKILL_DESCRIPTION_OVERRIDES = {
40
40
  "plan-version": "规划版本任务、波次和分工",
41
41
  push: "提交、推送并同步任务进度",
42
42
  "re-implement": "需求变更后重新实现",
43
- "sub2api-account-json-fix": "批量补全并推送 sub2api 账号 JSON",
44
43
  "sync-prd": "根据代码或需求变化同步 PRD",
45
44
  "torrent-analyze": "解析磁链或种子 hash 并整理信息",
46
45
  "trellis-analyze-task": "深度分析并细化任务",
@@ -1,47 +0,0 @@
1
- ---
2
- name: sub2api-account-json-fix
3
- description: 修正当前项目里的 sub2api 账号导出 JSON,并把这些账号直接推送到 sub2api admin 接口。用于批量推送 `sub2api-account-*.json` 到远端 sub2api,尤其适合先同步模板里的 `model_mapping`、`allow_overages`,再按指定 group_ids 创建账号。
4
- argument-hint: <template-json> [target-json...]
5
- disable-model-invocation: true
6
- ---
7
-
8
- # Sub2API 账号推送
9
-
10
- 先确认模板账号,再运行项目内的推送脚本。
11
-
12
- ## 规则
13
-
14
- - 先用模板账号补齐目标账号的 `credentials.model_mapping`。
15
- - 确保目标账号包含 `extra.allow_overages`。
16
- - 把 `name` 改成 `credentials.email`。
17
- - 然后把代理推送到 sub2api,再逐个创建账号。
18
- - 创建账号后默认立刻关闭调度。
19
- - 如果账号带有 `refresh_token`,创建后默认立刻调用一次 refresh,确保远端拿到最新 access token。
20
- - 如果 `.claude/skills/sub2api-account-json-fix/env/push.env` 不存在,继续检查系统环境变量。
21
- - 如果地址或 token 仍未配置,或者 token 还是 `admin-xxxx` 这种示例值,先向用户索要真实值,不要直接推送。
22
-
23
- ## 技能目录配置
24
-
25
- 优先读取:
26
-
27
- - `.claude/skills/sub2api-account-json-fix/env/push.env`
28
-
29
- 支持这些键:
30
-
31
- - `SUB2API_ACCOUNT_JSON_FIX_PUSH_ADMIN_BASE_URL`
32
- - `SUB2API_ACCOUNT_JSON_FIX_PUSH_TOKEN`
33
- - `SUB2API_ACCOUNT_JSON_FIX_PUSH_GROUP_IDS`
34
- - `SUB2API_ACCOUNT_JSON_FIX_PUSH_SCHEDULABLE`
35
- - `SUB2API_ACCOUNT_JSON_FIX_PUSH_REFRESH_AFTER_CREATE`
36
-
37
- ## 执行方式
38
-
39
- 如果用户给了参数,按“第一个参数是模板文件,后续参数是目标文件”的规则执行;否则先向用户确认模板文件。
40
-
41
- 运行:
42
-
43
- ```bash
44
- bash .claude/skills/sub2api-account-json-fix/scripts/run.sh $ARGUMENTS
45
- ```
46
-
47
- 如果用户没有显式要求写回本地文件,就不要自动加 `--write`;先预览或直接推送内存中的规范化结果即可。
@@ -1,8 +0,0 @@
1
- # Claude Code 项目技能的本地配置示例。
2
- # 把本文件复制为同目录下的 push.env 后再填写真实值。
3
-
4
- SUB2API_ACCOUNT_JSON_FIX_PUSH_ADMIN_BASE_URL=http://127.0.0.1:8080/api/v1/admin
5
- SUB2API_ACCOUNT_JSON_FIX_PUSH_TOKEN=admin-xxxx
6
- SUB2API_ACCOUNT_JSON_FIX_PUSH_GROUP_IDS=3,7
7
- SUB2API_ACCOUNT_JSON_FIX_PUSH_SCHEDULABLE=false
8
- SUB2API_ACCOUNT_JSON_FIX_PUSH_REFRESH_AFTER_CREATE=true
@@ -1,27 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
- CLAUDE_SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
6
- SKILL_GARDEN_ROOT="$(cd "$CLAUDE_SKILL_DIR/../../.." && pwd -P)"
7
- MAIN_SCRIPT="$SKILL_GARDEN_ROOT/.codex/skills/sub2api-account-json-fix/scripts/fix_exported_account_json.py"
8
- LOCAL_ENV_FILE="$CLAUDE_SKILL_DIR/env/push.env"
9
-
10
- export SUB2API_ACCOUNT_JSON_FIX_ENV_FILE="$LOCAL_ENV_FILE"
11
-
12
- if [[ -f "$LOCAL_ENV_FILE" ]]; then
13
- set -a
14
- # shellcheck disable=SC1090
15
- source "$LOCAL_ENV_FILE"
16
- set +a
17
- fi
18
-
19
- if [[ $# -lt 1 ]]; then
20
- echo "用法: run.sh <template-json> [target-json...]" >&2
21
- exit 2
22
- fi
23
-
24
- TEMPLATE_FILE="$1"
25
- shift
26
-
27
- python "$MAIN_SCRIPT" --template "$TEMPLATE_FILE" "$@" --push
@@ -1,126 +0,0 @@
1
- ---
2
- name: sub2api-account-json-fix
3
- description: 按模板批量补全 sub2api 账号导出 JSON,修复 `accounts[].name` 与 `credentials.email` 不一致、缺失 `credentials.model_mapping`、缺失 `extra.allow_overages` 等问题,并把这些账号直接推送到 sub2api admin 创建接口。用于处理 `sub2api-account-*.json` 这类导出文件,尤其适合“先规范化 JSON,再批量推送到 sub2api 指定分组”的场景。
4
- ---
5
-
6
- # Sub2API 账号 JSON 补全
7
-
8
- ## 概览
9
-
10
- 先确认模板 JSON,再用脚本批量修复目标文件。默认修复三件事:
11
-
12
- - 把每个账号的 `credentials.model_mapping` 同步成模板账号的值
13
- - 确保每个账号都包含 `extra.allow_overages`
14
- - 把 `name` 改成该账号 `credentials.email`
15
-
16
- 脚本只改这些字段,不会删除其他 `credentials`、`extra`、代理、并发、优先级或 token 字段。
17
-
18
- ## 工作流
19
-
20
- 1. 读取模板文件,默认使用模板中的第一个账号。
21
- 2. 确认模板账号里已经有正确的 `credentials.model_mapping` 和 `extra.allow_overages`。
22
- 3. 先用预览模式检查哪些文件会被修改。
23
- 4. 确认预览结果后,再用 `--write` 真正写回文件。
24
- 5. 如果要直推到 sub2api,先检查技能目录里的 `env/push.env`;没有再看系统环境变量;还没有才向用户询问直推地址、token 和目标分组 IDs。
25
- 6. 如果 `env/push.env` 里还是示例值
26
- `SUB2API_ACCOUNT_JSON_FIX_PUSH_ADMIN_BASE_URL=http://127.0.0.1:8080/api/v1/admin`
27
- 和 `SUB2API_ACCOUNT_JSON_FIX_PUSH_TOKEN=admin-xxxx`,把它视为“未配置”,不要直接直推,先向用户索要真实值。
28
- 7. 创建账号后,默认立刻关闭调度;如果账号带有 `refresh_token`,默认立刻调用一次 refresh。
29
-
30
- ## 快速命令
31
-
32
- 先预览:
33
-
34
- ```bash
35
- python sub2api-account-json-fix/scripts/fix_exported_account_json.py \
36
- --template sub2api-account-20260316021320.json
37
- ```
38
-
39
- 真正写回:
40
-
41
- ```bash
42
- python sub2api-account-json-fix/scripts/fix_exported_account_json.py \
43
- --template sub2api-account-20260316021320.json \
44
- --write
45
- ```
46
-
47
- 只检查是否还有待修复项,并用退出码表示结果:
48
-
49
- ```bash
50
- python sub2api-account-json-fix/scripts/fix_exported_account_json.py \
51
- --template sub2api-account-20260316021320.json \
52
- --check
53
- ```
54
-
55
- 把规范化后的账号直接推送到 `sub2api`:
56
-
57
- ```bash
58
- python sub2api-account-json-fix/scripts/fix_exported_account_json.py \
59
- --template sub2api-account-20260316021320.json \
60
- --push \
61
- --push-admin-base-url http://127.0.0.1:8080/api/v1/admin \
62
- --push-token 'admin-xxxx' \
63
- --push-group-id 3 \
64
- --push-group-id 7
65
- ```
66
-
67
- 也可以预先设置环境变量,调用时不再重复传参:
68
-
69
- ```bash
70
- export SUB2API_ACCOUNT_JSON_FIX_PUSH_ADMIN_BASE_URL='http://127.0.0.1:8080/api/v1/admin'
71
- export SUB2API_ACCOUNT_JSON_FIX_PUSH_TOKEN='admin-xxxx'
72
- export SUB2API_ACCOUNT_JSON_FIX_PUSH_SCHEDULABLE='false'
73
- export SUB2API_ACCOUNT_JSON_FIX_PUSH_REFRESH_AFTER_CREATE='true'
74
- python sub2api-account-json-fix/scripts/fix_exported_account_json.py \
75
- --template sub2api-account-20260316021320.json \
76
- --push
77
- ```
78
-
79
- 更推荐把配置放在技能目录里:
80
-
81
- ```bash
82
- cat > sub2api-account-json-fix/env/push.env <<'EOF'
83
- SUB2API_ACCOUNT_JSON_FIX_PUSH_ADMIN_BASE_URL=http://127.0.0.1:8080/api/v1/admin
84
- SUB2API_ACCOUNT_JSON_FIX_PUSH_TOKEN=admin-xxxx
85
- SUB2API_ACCOUNT_JSON_FIX_PUSH_GROUP_IDS=3,7
86
- SUB2API_ACCOUNT_JSON_FIX_PUSH_SCHEDULABLE=false
87
- SUB2API_ACCOUNT_JSON_FIX_PUSH_REFRESH_AFTER_CREATE=true
88
- EOF
89
- ```
90
-
91
- 指定部分文件:
92
-
93
- ```bash
94
- python sub2api-account-json-fix/scripts/fix_exported_account_json.py \
95
- --template sub2api-account-20260316021320.json \
96
- sub2api-account-20260316015827.json \
97
- sub2api-account-20260316020213.json \
98
- --write
99
- ```
100
-
101
- ## 规则
102
-
103
- - 把模板账号 `credentials.model_mapping` 原样复制到目标账号。
104
- - 只补齐 `extra.allow_overages`,不要覆盖目标账号 `extra` 里的其他键。
105
- - 只有当 `credentials.email` 是非空字符串时,才把 `name` 改成邮箱。
106
- - 默认处理当前工作目录里匹配 `sub2api-account-*.json` 的文件,并自动跳过模板自身。
107
- - 脚本支持一个文件里有多个 `accounts`,会逐个账号处理。
108
-
109
- ## 直推 sub2api
110
-
111
- - 直推固定走 `POST /api/v1/admin/proxies` 和 `POST /api/v1/admin/accounts`。
112
- - 脚本会先确保 JSON 里的代理存在,再按账号逐个创建到 sub2api。
113
- - 如果配置了 `group_ids`,会在创建账号时直接带上 `group_ids`。
114
- - 创建账号后默认会再调用一次 `POST /api/v1/admin/accounts/:id/schedulable`,把调度显式设为关闭。
115
- - 如果账号带有 `refresh_token`,创建账号后默认会再调用一次 `POST /api/v1/admin/accounts/:id/refresh`。
116
- - 直推参数优先级是:命令行参数 > 技能目录 `env/push.env` > 系统环境变量。
117
- - 技能目录配置文件路径固定是 `sub2api-account-json-fix/env/push.env`。
118
- - 支持的键有 `SUB2API_ACCOUNT_JSON_FIX_PUSH_ADMIN_BASE_URL`、`SUB2API_ACCOUNT_JSON_FIX_PUSH_TOKEN`、`SUB2API_ACCOUNT_JSON_FIX_PUSH_GROUP_IDS`、`SUB2API_ACCOUNT_JSON_FIX_PUSH_SCHEDULABLE`、`SUB2API_ACCOUNT_JSON_FIX_PUSH_REFRESH_AFTER_CREATE`。
119
- - 如果 `push.env` 仍然保留示例值 `admin-xxxx`,脚本会拒绝直推,并要求先替换为真实配置。
120
- - 如果要直推但命令行、技能目录配置和系统环境变量都没给地址或 token,调用这个技能的代理应先询问用户,再决定是否执行 `--push`。
121
- - `group_ids` 为空时,是否触发 sub2api 默认分组绑定,取决于 sub2api 当前创建接口的行为。
122
-
123
- ## 资源
124
-
125
- - `scripts/fix_exported_account_json.py`:批量补全 JSON 的主脚本。
126
- - `env/push.env`:可选的技能目录本地配置文件;不存在时不会报错。
@@ -1,4 +0,0 @@
1
- interface:
2
- display_name: "Sub2API 账号 JSON 补全"
3
- short_description: "按模板批量补全 sub2api 账号导出 JSON 字段"
4
- default_prompt: "使用 $sub2api-account-json-fix 按模板批量补全当前目录里的 sub2api 账号导出 JSON。"
@@ -1,8 +0,0 @@
1
- # 把本文件复制为同目录下的 push.env 后再填写真实值。
2
- # 如果仍保留 admin-xxxx 这样的示例值,脚本会把它视为未配置并拒绝直推。
3
-
4
- SUB2API_ACCOUNT_JSON_FIX_PUSH_ADMIN_BASE_URL=http://127.0.0.1:8080/api/v1/admin
5
- SUB2API_ACCOUNT_JSON_FIX_PUSH_TOKEN=admin-xxxx
6
- SUB2API_ACCOUNT_JSON_FIX_PUSH_GROUP_IDS=3,7
7
- SUB2API_ACCOUNT_JSON_FIX_PUSH_SCHEDULABLE=false
8
- SUB2API_ACCOUNT_JSON_FIX_PUSH_REFRESH_AFTER_CREATE=true
@@ -1,890 +0,0 @@
1
- #!/usr/bin/env python3
2
- """按模板补全 sub2api 账号导出 JSON,并可直接推送到 sub2api admin 接口。"""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import copy
8
- import json
9
- import os
10
- import sys
11
- from dataclasses import dataclass
12
- from pathlib import Path
13
- from typing import Any
14
- from urllib import error as urllib_error
15
- from urllib import parse as urllib_parse
16
- from urllib import request as urllib_request
17
-
18
- SKILL_ROOT = Path(__file__).resolve().parent.parent
19
- SKILL_PUSH_ENV_FILE = SKILL_ROOT / "env" / "push.env"
20
-
21
- ENV_PUSH_ENV_FILE_OVERRIDE = "SUB2API_ACCOUNT_JSON_FIX_ENV_FILE"
22
- ENV_PUSH_ADMIN_BASE_URL = "SUB2API_ACCOUNT_JSON_FIX_PUSH_ADMIN_BASE_URL"
23
- ENV_PUSH_TOKEN = "SUB2API_ACCOUNT_JSON_FIX_PUSH_TOKEN"
24
- ENV_PUSH_GROUP_IDS = "SUB2API_ACCOUNT_JSON_FIX_PUSH_GROUP_IDS"
25
- ENV_PUSH_SCHEDULABLE = "SUB2API_ACCOUNT_JSON_FIX_PUSH_SCHEDULABLE"
26
- ENV_PUSH_REFRESH_AFTER_CREATE = "SUB2API_ACCOUNT_JSON_FIX_PUSH_REFRESH_AFTER_CREATE"
27
-
28
- EXAMPLE_PUSH_ADMIN_BASE_URL = "http://127.0.0.1:8080/api/v1/admin"
29
- EXAMPLE_PUSH_TOKEN = "admin-xxxx"
30
-
31
-
32
- @dataclass
33
- class TemplateValues:
34
- """保存从模板账号提取出的补全字段。"""
35
-
36
- model_mapping: dict[str, Any]
37
- allow_overages: bool
38
-
39
-
40
- @dataclass
41
- class FileChangeSummary:
42
- """记录单个文件内的修改统计。"""
43
-
44
- name_fixed: int = 0
45
- model_mapping_fixed: int = 0
46
- allow_overages_fixed: int = 0
47
-
48
- def has_changes(self) -> bool:
49
- """判断当前文件是否发生了任何修改。"""
50
-
51
- return (
52
- self.name_fixed > 0
53
- or self.model_mapping_fixed > 0
54
- or self.allow_overages_fixed > 0
55
- )
56
-
57
-
58
- @dataclass
59
- class PushSummary:
60
- """记录直推 sub2api 的统计结果。"""
61
-
62
- success: int = 0
63
- failed: int = 0
64
-
65
-
66
- def parse_args() -> argparse.Namespace:
67
- """解析命令行参数。"""
68
-
69
- parser = argparse.ArgumentParser(
70
- description="按模板补全 sub2api 账号导出 JSON 的 model_mapping、allow_overages 和 name,并可直接推送到 sub2api。"
71
- )
72
- parser.add_argument(
73
- "--template",
74
- required=True,
75
- help="模板 JSON 文件路径,默认取其第一个账号作为模板来源。",
76
- )
77
- parser.add_argument(
78
- "--template-account-index",
79
- type=int,
80
- default=0,
81
- help="模板文件中作为来源账号的索引,默认 0。",
82
- )
83
- parser.add_argument(
84
- "--glob",
85
- default="sub2api-account-*.json",
86
- help="未显式传入目标文件时使用的匹配模式,默认 sub2api-account-*.json。",
87
- )
88
- mode_group = parser.add_mutually_exclusive_group()
89
- mode_group.add_argument(
90
- "--write",
91
- action="store_true",
92
- help="真正写回文件。未指定时只预览。",
93
- )
94
- mode_group.add_argument(
95
- "--check",
96
- action="store_true",
97
- help="仅检查是否还有待修复项;存在待修复项时退出码为 1。",
98
- )
99
- parser.add_argument(
100
- "--push",
101
- action="store_true",
102
- help="把规范化后的账号直接推送到 sub2api admin 接口。",
103
- )
104
- parser.add_argument(
105
- "--push-admin-base-url",
106
- help="sub2api admin 基础地址,例如 http://127.0.0.1:8080/api/v1/admin。",
107
- )
108
- parser.add_argument(
109
- "--push-token",
110
- help="sub2api admin token。若以 Bearer 开头则走 Authorization,否则走 x-api-key。",
111
- )
112
- parser.add_argument(
113
- "--push-group-id",
114
- action="append",
115
- type=int,
116
- default=[],
117
- help="创建账号时绑定到这些 group_id,可重复传入。",
118
- )
119
- parser.add_argument(
120
- "--push-schedulable",
121
- choices=("true", "false"),
122
- help="创建账号后是否保持可调度。默认 false,也就是创建后立即关闭调度。",
123
- )
124
- parser.add_argument(
125
- "--push-refresh-after-create",
126
- choices=("true", "false"),
127
- help="创建账号后是否立即调用 refresh。默认 true。",
128
- )
129
- parser.add_argument(
130
- "targets",
131
- nargs="*",
132
- help="要处理的目标文件;留空时按 --glob 自动搜索。",
133
- )
134
- return parser.parse_args()
135
-
136
-
137
- def strip_wrapped_quotes(raw_value: str) -> str:
138
- """去掉包裹值的同类引号。"""
139
-
140
- text = str(raw_value or "").strip()
141
- if len(text) >= 2 and text[0] == text[-1] and text[0] in {"'", '"'}:
142
- return text[1:-1]
143
- return text
144
-
145
-
146
- def load_env_file(path: Path) -> dict[str, str]:
147
- """解析简单的 .env 风格文件。"""
148
-
149
- if not path.is_file():
150
- return {}
151
-
152
- result: dict[str, str] = {}
153
- for raw_line in path.read_text(encoding="utf-8").splitlines():
154
- line = raw_line.strip()
155
- if not line or line.startswith("#"):
156
- continue
157
- if line.startswith("export "):
158
- line = line[7:].strip()
159
- key, sep, value = line.partition("=")
160
- if not sep:
161
- continue
162
- normalized_key = key.strip()
163
- if not normalized_key:
164
- continue
165
- result[normalized_key] = strip_wrapped_quotes(value)
166
- return result
167
-
168
-
169
- def parse_group_ids(raw_value: str) -> list[int]:
170
- """把逗号分隔的 group_ids 转成整数列表。"""
171
-
172
- result: list[int] = []
173
- seen: set[int] = set()
174
- for chunk in str(raw_value or "").split(","):
175
- item = chunk.strip()
176
- if not item:
177
- continue
178
- try:
179
- group_id = int(item)
180
- except ValueError as exc:
181
- raise ValueError(f"group_id 不是合法整数: {item}") from exc
182
- if group_id <= 0:
183
- raise ValueError(f"group_id 必须大于 0: {item}")
184
- if group_id not in seen:
185
- seen.add(group_id)
186
- result.append(group_id)
187
- return result
188
-
189
-
190
- def resolve_push_env_file() -> Path:
191
- """解析当前应该读取的 push.env 路径。"""
192
-
193
- override = str(os.getenv(ENV_PUSH_ENV_FILE_OVERRIDE, "")).strip()
194
- if not override:
195
- return SKILL_PUSH_ENV_FILE
196
- return Path(override).expanduser().resolve()
197
-
198
-
199
- def parse_env_bool(raw_value: str) -> bool:
200
- """把环境变量中的布尔字符串转换为布尔值。"""
201
-
202
- normalized = str(raw_value or "").strip().lower()
203
- return normalized in {"1", "true", "yes", "on"}
204
-
205
-
206
- def apply_env_defaults(args: argparse.Namespace) -> argparse.Namespace:
207
- """当命令行未显式传值时,使用技能目录配置和环境变量补全直推配置。"""
208
-
209
- file_env = load_env_file(resolve_push_env_file())
210
- if not args.push_admin_base_url:
211
- args.push_admin_base_url = (
212
- str(file_env.get(ENV_PUSH_ADMIN_BASE_URL, "")).strip()
213
- or str(os.getenv(ENV_PUSH_ADMIN_BASE_URL, "")).strip()
214
- or None
215
- )
216
- if not args.push_token:
217
- args.push_token = (
218
- str(file_env.get(ENV_PUSH_TOKEN, "")).strip()
219
- or str(os.getenv(ENV_PUSH_TOKEN, "")).strip()
220
- or None
221
- )
222
- if not args.push_group_id:
223
- raw_group_ids = (
224
- str(file_env.get(ENV_PUSH_GROUP_IDS, "")).strip()
225
- or str(os.getenv(ENV_PUSH_GROUP_IDS, "")).strip()
226
- )
227
- if raw_group_ids:
228
- args.push_group_id = parse_group_ids(raw_group_ids)
229
- if args.push_schedulable is None:
230
- raw_schedulable = (
231
- str(file_env.get(ENV_PUSH_SCHEDULABLE, "")).strip()
232
- or str(os.getenv(ENV_PUSH_SCHEDULABLE, "")).strip()
233
- )
234
- if raw_schedulable:
235
- args.push_schedulable = "true" if parse_env_bool(raw_schedulable) else "false"
236
- else:
237
- args.push_schedulable = "false"
238
- if args.push_refresh_after_create is None:
239
- raw_refresh_after_create = (
240
- str(file_env.get(ENV_PUSH_REFRESH_AFTER_CREATE, "")).strip()
241
- or str(os.getenv(ENV_PUSH_REFRESH_AFTER_CREATE, "")).strip()
242
- )
243
- if raw_refresh_after_create:
244
- args.push_refresh_after_create = "true" if parse_env_bool(raw_refresh_after_create) else "false"
245
- else:
246
- args.push_refresh_after_create = "true"
247
- return args
248
-
249
-
250
- def is_placeholder_push_config(args: argparse.Namespace) -> bool:
251
- """判断当前直推配置是否仍然是示例占位值。"""
252
-
253
- admin_base_url = str(args.push_admin_base_url or "").strip()
254
- token = str(args.push_token or "").strip()
255
- if token == EXAMPLE_PUSH_TOKEN:
256
- return True
257
- return admin_base_url == EXAMPLE_PUSH_ADMIN_BASE_URL and token == EXAMPLE_PUSH_TOKEN
258
-
259
-
260
- def load_json(path: Path) -> Any:
261
- """读取并解析 JSON 文件。"""
262
-
263
- with path.open("r", encoding="utf-8") as handle:
264
- return json.load(handle)
265
-
266
-
267
- def write_json(path: Path, payload: Any) -> None:
268
- """以统一格式写回 JSON 文件。"""
269
-
270
- text = json.dumps(payload, ensure_ascii=False, indent=2)
271
- path.write_text(text + "\n", encoding="utf-8")
272
-
273
-
274
- def build_target_paths(args: argparse.Namespace, template_path: Path) -> list[Path]:
275
- """构建要处理的目标文件列表,并自动跳过模板文件。"""
276
-
277
- if args.targets:
278
- raw_paths = [Path(item) for item in args.targets]
279
- else:
280
- raw_paths = sorted(Path.cwd().glob(args.glob))
281
-
282
- seen: set[Path] = set()
283
- result: list[Path] = []
284
- template_resolved = template_path.resolve()
285
- for raw_path in raw_paths:
286
- resolved = raw_path.resolve()
287
- if resolved == template_resolved:
288
- continue
289
- if resolved in seen:
290
- continue
291
- seen.add(resolved)
292
- result.append(raw_path)
293
- return result
294
-
295
-
296
- def extract_template_values(template_data: dict[str, Any], account_index: int) -> TemplateValues:
297
- """从模板文件中提取补全时要使用的字段。"""
298
-
299
- accounts = template_data.get("accounts")
300
- if not isinstance(accounts, list) or not accounts:
301
- raise ValueError("模板文件缺少非空的 accounts 数组")
302
- if account_index < 0 or account_index >= len(accounts):
303
- raise ValueError(f"模板账号索引越界: {account_index}")
304
-
305
- template_account = accounts[account_index]
306
- if not isinstance(template_account, dict):
307
- raise ValueError("模板账号结构无效")
308
-
309
- credentials = template_account.get("credentials")
310
- if not isinstance(credentials, dict):
311
- raise ValueError("模板账号缺少 credentials 对象")
312
-
313
- model_mapping = credentials.get("model_mapping")
314
- if not isinstance(model_mapping, dict) or not model_mapping:
315
- raise ValueError("模板账号缺少有效的 credentials.model_mapping")
316
-
317
- extra = template_account.get("extra")
318
- if not isinstance(extra, dict):
319
- raise ValueError("模板账号缺少 extra 对象")
320
-
321
- allow_overages = extra.get("allow_overages")
322
- if not isinstance(allow_overages, bool):
323
- raise ValueError("模板账号缺少布尔类型的 extra.allow_overages")
324
-
325
- return TemplateValues(
326
- model_mapping=copy.deepcopy(model_mapping),
327
- allow_overages=allow_overages,
328
- )
329
-
330
-
331
- def replace_or_insert_key(
332
- payload: dict[str, Any],
333
- key: str,
334
- value: Any,
335
- *,
336
- insert_after: tuple[str, ...] = (),
337
- ) -> tuple[dict[str, Any], bool]:
338
- """替换已有键或按指定相对位置插入新键。"""
339
-
340
- if key in payload:
341
- if payload[key] == value:
342
- return payload, False
343
- updated: dict[str, Any] = {}
344
- for existing_key, existing_value in payload.items():
345
- if existing_key == key:
346
- updated[existing_key] = copy.deepcopy(value)
347
- else:
348
- updated[existing_key] = existing_value
349
- return updated, True
350
-
351
- updated = {}
352
- inserted = False
353
- for existing_key, existing_value in payload.items():
354
- updated[existing_key] = existing_value
355
- if not inserted and existing_key in insert_after:
356
- updated[key] = copy.deepcopy(value)
357
- inserted = True
358
- if not inserted:
359
- updated[key] = copy.deepcopy(value)
360
- return updated, True
361
-
362
-
363
- def normalize_account(
364
- account: dict[str, Any],
365
- template_values: TemplateValues,
366
- ) -> FileChangeSummary:
367
- """补全单个账号对象并返回修改统计。"""
368
-
369
- summary = FileChangeSummary()
370
-
371
- credentials = account.get("credentials")
372
- if not isinstance(credentials, dict):
373
- raise ValueError("账号缺少 credentials 对象")
374
-
375
- updated_credentials, changed = replace_or_insert_key(
376
- credentials,
377
- "model_mapping",
378
- template_values.model_mapping,
379
- insert_after=("expires_at", "email"),
380
- )
381
- if changed:
382
- account["credentials"] = updated_credentials
383
- credentials = updated_credentials
384
- summary.model_mapping_fixed += 1
385
-
386
- email = credentials.get("email")
387
- if isinstance(email, str):
388
- normalized_email = email.strip()
389
- if normalized_email and account.get("name") != normalized_email:
390
- account["name"] = normalized_email
391
- summary.name_fixed += 1
392
-
393
- extra = account.get("extra")
394
- if extra is None:
395
- extra = {}
396
- if not isinstance(extra, dict):
397
- raise ValueError("账号 extra 不是对象")
398
-
399
- updated_extra, changed = replace_or_insert_key(
400
- extra,
401
- "allow_overages",
402
- template_values.allow_overages,
403
- )
404
- if changed or account.get("extra") is None:
405
- account["extra"] = updated_extra
406
- summary.allow_overages_fixed += 1
407
-
408
- return summary
409
-
410
-
411
- def normalize_file(path: Path, template_values: TemplateValues) -> tuple[dict[str, Any], FileChangeSummary]:
412
- """补全单个 JSON 文件中的所有账号。"""
413
-
414
- payload = load_json(path)
415
- if not isinstance(payload, dict):
416
- raise ValueError("JSON 根节点必须是对象")
417
-
418
- accounts = payload.get("accounts")
419
- if not isinstance(accounts, list):
420
- raise ValueError("JSON 缺少 accounts 数组")
421
-
422
- total = FileChangeSummary()
423
- for account in accounts:
424
- if not isinstance(account, dict):
425
- raise ValueError("accounts 中存在非对象元素")
426
- item_summary = normalize_account(account, template_values)
427
- total.name_fixed += item_summary.name_fixed
428
- total.model_mapping_fixed += item_summary.model_mapping_fixed
429
- total.allow_overages_fixed += item_summary.allow_overages_fixed
430
-
431
- return payload, total
432
-
433
-
434
- def print_summary(path: Path, summary: FileChangeSummary, *, action: str) -> None:
435
- """打印单个文件的处理摘要。"""
436
-
437
- print(
438
- f"[{action}] {path}: "
439
- f"name={summary.name_fixed}, "
440
- f"model_mapping={summary.model_mapping_fixed}, "
441
- f"allow_overages={summary.allow_overages_fixed}"
442
- )
443
-
444
-
445
- class Sub2APIClient:
446
- """简易 sub2api admin API 客户端。"""
447
-
448
- def __init__(self, admin_base_url: str, token: str) -> None:
449
- self.admin_base_url = self.normalize_admin_base_url(admin_base_url)
450
- self.token = str(token or "").strip()
451
-
452
- @staticmethod
453
- def normalize_admin_base_url(admin_base_url: str) -> str:
454
- """兼容传入 admin 根路径或具体 accounts/proxies 路径。"""
455
-
456
- url = str(admin_base_url or "").rstrip("/")
457
- for suffix in ("/accounts", "/proxies"):
458
- if url.endswith(suffix):
459
- return url[: -len(suffix)]
460
- return url
461
-
462
- def request(
463
- self,
464
- method: str,
465
- path: str,
466
- *,
467
- payload: Any | None = None,
468
- query: dict[str, Any] | None = None,
469
- ) -> Any:
470
- """发起一次 JSON 请求并返回解包后的 data。"""
471
-
472
- url = self.admin_base_url + path
473
- if query:
474
- normalized_query = {
475
- key: value
476
- for key, value in query.items()
477
- if value is not None and value != ""
478
- }
479
- if normalized_query:
480
- url += "?" + urllib_parse.urlencode(normalized_query)
481
-
482
- headers = {
483
- "Accept": "application/json, text/plain, */*",
484
- }
485
- if payload is not None:
486
- body = json.dumps(payload, ensure_ascii=False).encode("utf-8")
487
- headers["Content-Type"] = "application/json"
488
- else:
489
- body = None
490
-
491
- if self.token.lower().startswith("bearer "):
492
- headers["Authorization"] = self.token
493
- else:
494
- headers["x-api-key"] = self.token
495
-
496
- request = urllib_request.Request(url=url, data=body, headers=headers, method=method.upper())
497
- try:
498
- with urllib_request.urlopen(request, timeout=30) as response:
499
- raw = response.read().decode("utf-8")
500
- return self._unwrap_response(raw)
501
- except urllib_error.HTTPError as exc:
502
- error_body = exc.read().decode("utf-8", errors="replace")
503
- raise RuntimeError(self._format_error(exc.code, error_body)) from exc
504
- except urllib_error.URLError as exc:
505
- raise RuntimeError(f"请求失败: {exc.reason}") from exc
506
-
507
- @staticmethod
508
- def _unwrap_response(raw_text: str) -> Any:
509
- """解包 sub2api 标准响应格式。"""
510
-
511
- if not raw_text.strip():
512
- return None
513
- payload = json.loads(raw_text)
514
- if isinstance(payload, dict) and "code" in payload:
515
- if payload.get("code") == 0:
516
- return payload.get("data")
517
- raise RuntimeError(str(payload.get("message", "接口返回错误")))
518
- return payload
519
-
520
- @staticmethod
521
- def _format_error(status_code: int, raw_text: str) -> str:
522
- """把 HTTP 错误响应整理成可读文本。"""
523
-
524
- try:
525
- payload = json.loads(raw_text)
526
- except json.JSONDecodeError:
527
- payload = None
528
-
529
- if isinstance(payload, dict):
530
- message = payload.get("message")
531
- reason = payload.get("reason")
532
- if message and reason:
533
- return f"HTTP {status_code}: {message} ({reason})"
534
- if message:
535
- return f"HTTP {status_code}: {message}"
536
- compact = raw_text.strip().replace("\n", " ")
537
- if compact:
538
- return f"HTTP {status_code}: {compact[:300]}"
539
- return f"HTTP {status_code}"
540
-
541
-
542
- def validate_push_args(args: argparse.Namespace) -> None:
543
- """校验直推模式必需参数。"""
544
-
545
- if not args.push:
546
- return
547
- push_env_file = resolve_push_env_file()
548
- if not args.push_admin_base_url:
549
- raise ValueError(
550
- "启用 --push 时必须提供 --push-admin-base-url,或在技能目录配置 "
551
- f"{push_env_file},或设置环境变量 {ENV_PUSH_ADMIN_BASE_URL}"
552
- )
553
- if not args.push_token:
554
- raise ValueError(
555
- "启用 --push 时必须提供 --push-token,或在技能目录配置 "
556
- f"{push_env_file},或设置环境变量 {ENV_PUSH_TOKEN}"
557
- )
558
- if is_placeholder_push_config(args):
559
- raise ValueError(
560
- "检测到直推配置仍是示例占位值,请先让用户提供真实的 sub2api admin 地址和 token,"
561
- f"再更新 {push_env_file} 或命令行参数后重试"
562
- )
563
- for group_id in args.push_group_id:
564
- if group_id <= 0:
565
- raise ValueError(f"group_id 必须大于 0: {group_id}")
566
-
567
-
568
- def build_proxy_key(protocol: str, host: str, port: int, username: str, password: str) -> str:
569
- """按 sub2api 的规则生成 proxy_key。"""
570
-
571
- return "|".join(
572
- [
573
- str(protocol or "").strip(),
574
- str(host or "").strip(),
575
- str(port),
576
- str(username or "").strip(),
577
- str(password or "").strip(),
578
- ]
579
- )
580
-
581
-
582
- def build_proxy_key_from_item(item: dict[str, Any]) -> str:
583
- """从代理对象中提取或构造 proxy_key。"""
584
-
585
- proxy_key = str(item.get("proxy_key", "") or "").strip()
586
- if proxy_key:
587
- return proxy_key
588
- return build_proxy_key(
589
- str(item.get("protocol", "") or ""),
590
- str(item.get("host", "") or ""),
591
- int(item.get("port", 0) or 0),
592
- str(item.get("username", "") or ""),
593
- str(item.get("password", "") or ""),
594
- )
595
-
596
-
597
- def list_remote_proxies(client: Sub2APIClient) -> dict[str, dict[str, Any]]:
598
- """读取远端全部代理,并按 proxy_key 建立索引。"""
599
-
600
- page = 1
601
- page_size = 1000
602
- result: dict[str, dict[str, Any]] = {}
603
- while True:
604
- payload = client.request(
605
- "GET",
606
- "/proxies",
607
- query={"page": page, "page_size": page_size},
608
- )
609
- if not isinstance(payload, dict):
610
- raise RuntimeError("代理列表响应格式不正确")
611
- items = payload.get("items")
612
- if not isinstance(items, list):
613
- raise RuntimeError("代理列表缺少 items")
614
- for item in items:
615
- if not isinstance(item, dict):
616
- continue
617
- key = build_proxy_key(
618
- str(item.get("protocol", "") or ""),
619
- str(item.get("host", "") or ""),
620
- int(item.get("port", 0) or 0),
621
- str(item.get("username", "") or ""),
622
- str(item.get("password", "") or ""),
623
- )
624
- result[key] = item
625
- total = int(payload.get("total", 0) or 0)
626
- if len(items) == 0 or page * page_size >= total:
627
- break
628
- page += 1
629
- return result
630
-
631
-
632
- def ensure_remote_proxy(
633
- client: Sub2APIClient,
634
- remote_proxies: dict[str, dict[str, Any]],
635
- item: dict[str, Any],
636
- ) -> int:
637
- """确保远端存在目标代理,并返回 proxy_id。"""
638
-
639
- key = build_proxy_key_from_item(item)
640
- existing = remote_proxies.get(key)
641
- desired_status = str(item.get("status", "") or "").strip().lower()
642
- if existing is None:
643
- created = client.request(
644
- "POST",
645
- "/proxies",
646
- payload={
647
- "name": str(item.get("name", "") or "imported-proxy"),
648
- "protocol": str(item.get("protocol", "") or "").strip(),
649
- "host": str(item.get("host", "") or "").strip(),
650
- "port": int(item.get("port", 0) or 0),
651
- "username": str(item.get("username", "") or "").strip(),
652
- "password": str(item.get("password", "") or "").strip(),
653
- },
654
- )
655
- if not isinstance(created, dict):
656
- raise RuntimeError("创建代理返回格式不正确")
657
- existing = created
658
- remote_proxies[key] = existing
659
-
660
- proxy_id = int(existing.get("id", 0) or 0)
661
- if proxy_id <= 0:
662
- raise RuntimeError("远端代理缺少有效 id")
663
-
664
- current_status = str(existing.get("status", "") or "").strip().lower()
665
- if desired_status in {"active", "inactive"} and current_status != desired_status:
666
- updated = client.request(
667
- "PUT",
668
- f"/proxies/{proxy_id}",
669
- payload={"status": desired_status},
670
- )
671
- if isinstance(updated, dict):
672
- remote_proxies[key] = updated
673
- return proxy_id
674
-
675
-
676
- def build_create_account_payload(
677
- account: dict[str, Any],
678
- proxy_id: int | None,
679
- group_ids: list[int],
680
- ) -> dict[str, Any]:
681
- """把导出 JSON 中的账号对象转换为创建接口所需 payload。"""
682
-
683
- payload = {
684
- "name": account.get("name"),
685
- "notes": account.get("notes"),
686
- "platform": account.get("platform"),
687
- "type": account.get("type"),
688
- "credentials": account.get("credentials"),
689
- "extra": account.get("extra") or {},
690
- "proxy_id": proxy_id,
691
- "concurrency": int(account.get("concurrency", 0) or 0),
692
- "priority": int(account.get("priority", 0) or 0),
693
- "rate_multiplier": account.get("rate_multiplier"),
694
- "expires_at": account.get("expires_at"),
695
- "auto_pause_on_expired": account.get("auto_pause_on_expired", True),
696
- }
697
- if group_ids:
698
- payload["group_ids"] = group_ids
699
- return payload
700
-
701
-
702
- def set_account_schedulable(client: Sub2APIClient, account_id: int, schedulable: bool) -> None:
703
- """创建账号后显式设置调度开关。"""
704
-
705
- client.request(
706
- "POST",
707
- f"/accounts/{account_id}/schedulable",
708
- payload={"schedulable": schedulable},
709
- )
710
-
711
-
712
- def should_refresh_after_create(account: dict[str, Any], refresh_after_create: bool) -> bool:
713
- """判断当前账号创建后是否应触发 refresh。"""
714
-
715
- if not refresh_after_create:
716
- return False
717
- if str(account.get("type", "") or "").strip() != "oauth":
718
- return False
719
- credentials = account.get("credentials")
720
- if not isinstance(credentials, dict):
721
- return False
722
- refresh_token = str(credentials.get("refresh_token", "") or "").strip()
723
- return refresh_token != ""
724
-
725
-
726
- def refresh_account_after_create(client: Sub2APIClient, account_id: int) -> None:
727
- """创建账号后触发一次远端 refresh。"""
728
-
729
- client.request("POST", f"/accounts/{account_id}/refresh", payload={})
730
-
731
-
732
- def push_files_by_create(
733
- client: Sub2APIClient,
734
- prepared_files: list[tuple[Path, dict[str, Any], FileChangeSummary]],
735
- args: argparse.Namespace,
736
- ) -> PushSummary:
737
- """通过 `/accounts` 逐个创建账号到 sub2api。"""
738
-
739
- summary = PushSummary()
740
- remote_proxies = list_remote_proxies(client)
741
- target_schedulable = str(args.push_schedulable or "false").lower() == "true"
742
- refresh_after_create = str(args.push_refresh_after_create or "true").lower() == "true"
743
-
744
- for path, payload, _ in prepared_files:
745
- proxies = payload.get("proxies")
746
- accounts = payload.get("accounts")
747
- if not isinstance(proxies, list) or not isinstance(accounts, list):
748
- print(f"[错误] {path}: 不是合法的 sub2api bundle JSON", file=sys.stderr)
749
- summary.failed += 1
750
- continue
751
-
752
- proxy_id_by_key: dict[str, int] = {}
753
- for item in proxies:
754
- if not isinstance(item, dict):
755
- continue
756
- try:
757
- proxy_id_by_key[build_proxy_key_from_item(item)] = ensure_remote_proxy(
758
- client, remote_proxies, item
759
- )
760
- except Exception as exc: # noqa: BLE001
761
- summary.failed += 1
762
- print(
763
- f"[错误] {path}: 代理 {item.get('name') or item.get('host') or '-'} 推送失败: {exc}",
764
- file=sys.stderr,
765
- )
766
-
767
- for account in accounts:
768
- if not isinstance(account, dict):
769
- summary.failed += 1
770
- print(f"[错误] {path}: accounts 中存在非法元素", file=sys.stderr)
771
- continue
772
-
773
- try:
774
- proxy_key = str(account.get("proxy_key", "") or "").strip()
775
- proxy_id = proxy_id_by_key.get(proxy_key) if proxy_key else None
776
- if proxy_key and proxy_id is None:
777
- raise RuntimeError(f"找不到对应代理: {proxy_key}")
778
- created = client.request(
779
- "POST",
780
- "/accounts",
781
- payload=build_create_account_payload(account, proxy_id, args.push_group_id),
782
- )
783
- if not isinstance(created, dict):
784
- raise RuntimeError("创建账号返回格式不正确")
785
- account_id = int(created.get("id", 0) or 0)
786
- if account_id <= 0:
787
- raise RuntimeError("创建账号成功但返回中缺少有效 id")
788
- set_account_schedulable(client, account_id, target_schedulable)
789
- refresh_status = "skipped"
790
- if should_refresh_after_create(account, refresh_after_create):
791
- refresh_account_after_create(client, account_id)
792
- refresh_status = "triggered"
793
- summary.success += 1
794
- group_text = ",".join(str(item) for item in args.push_group_id) if args.push_group_id else "-"
795
- print(
796
- f"[直推/create] {path}: "
797
- f"account_id={account_id}, "
798
- f"name={created.get('name', account.get('name', ''))}, "
799
- f"group_ids={group_text}, "
800
- f"schedulable={str(target_schedulable).lower()}, "
801
- f"refresh={refresh_status}"
802
- )
803
- except Exception as exc: # noqa: BLE001
804
- summary.failed += 1
805
- print(
806
- f"[错误] {path}: 账号 {account.get('name') or '-'} 推送失败: {exc}",
807
- file=sys.stderr,
808
- )
809
-
810
- return summary
811
-
812
-
813
- def push_prepared_files(
814
- args: argparse.Namespace,
815
- prepared_files: list[tuple[Path, dict[str, Any], FileChangeSummary]],
816
- ) -> PushSummary:
817
- """把已规范化的数据推送到 sub2api。"""
818
-
819
- validate_push_args(args)
820
- client = Sub2APIClient(args.push_admin_base_url, args.push_token)
821
- return push_files_by_create(client, prepared_files, args)
822
-
823
-
824
- def main() -> int:
825
- """执行入口。"""
826
-
827
- args = apply_env_defaults(parse_args())
828
- template_path = Path(args.template)
829
- if not template_path.is_file():
830
- print(f"模板文件不存在: {template_path}", file=sys.stderr)
831
- return 2
832
-
833
- try:
834
- template_data = load_json(template_path)
835
- if not isinstance(template_data, dict):
836
- raise ValueError("模板 JSON 根节点必须是对象")
837
- template_values = extract_template_values(template_data, args.template_account_index)
838
- except Exception as exc: # noqa: BLE001
839
- print(f"读取模板失败: {exc}", file=sys.stderr)
840
- return 2
841
-
842
- target_paths = build_target_paths(args, template_path)
843
- if not target_paths:
844
- print("没有找到需要处理的目标文件", file=sys.stderr)
845
- return 2
846
-
847
- changed_files = 0
848
- had_error = False
849
- action = "写入" if args.write else "预览"
850
- prepared_files: list[tuple[Path, dict[str, Any], FileChangeSummary]] = []
851
-
852
- for path in target_paths:
853
- try:
854
- payload, summary = normalize_file(path, template_values)
855
- except Exception as exc: # noqa: BLE001
856
- had_error = True
857
- print(f"[错误] {path}: {exc}", file=sys.stderr)
858
- continue
859
-
860
- if not summary.has_changes():
861
- print_summary(path, summary, action="跳过")
862
- else:
863
- changed_files += 1
864
- if args.write:
865
- write_json(path, payload)
866
- print_summary(path, summary, action=action)
867
- prepared_files.append((path, payload, summary))
868
-
869
- print(f"总目标文件: {len(target_paths)}")
870
- print(f"发生修改的文件: {changed_files}")
871
-
872
- if had_error:
873
- return 2
874
- if args.push:
875
- try:
876
- push_summary = push_prepared_files(args, prepared_files)
877
- except Exception as exc: # noqa: BLE001
878
- print(f"直推失败: {exc}", file=sys.stderr)
879
- return 2
880
- print(f"直推成功: {push_summary.success}")
881
- print(f"直推失败: {push_summary.failed}")
882
- if push_summary.failed > 0:
883
- return 2
884
- if args.check and changed_files > 0:
885
- return 1
886
- return 0
887
-
888
-
889
- if __name__ == "__main__":
890
- sys.exit(main())
@@ -1,26 +0,0 @@
1
- #!/usr/bin/env bash
2
- set -euo pipefail
3
-
4
- SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
5
- CODEX_SKILL_DIR="$(cd "$SCRIPT_DIR/.." && pwd -P)"
6
- MAIN_SCRIPT="$CODEX_SKILL_DIR/scripts/fix_exported_account_json.py"
7
- LOCAL_ENV_FILE="$CODEX_SKILL_DIR/env/push.env"
8
-
9
- export SUB2API_ACCOUNT_JSON_FIX_ENV_FILE="$LOCAL_ENV_FILE"
10
-
11
- if [[ -f "$LOCAL_ENV_FILE" ]]; then
12
- set -a
13
- # shellcheck disable=SC1090
14
- source "$LOCAL_ENV_FILE"
15
- set +a
16
- fi
17
-
18
- if [[ $# -lt 1 ]]; then
19
- echo "用法: run.sh <template-json> [target-json...]" >&2
20
- exit 2
21
- fi
22
-
23
- TEMPLATE_FILE="$1"
24
- shift
25
-
26
- python "$MAIN_SCRIPT" --template "$TEMPLATE_FILE" "$@" --push