-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcpa
More file actions
executable file
·364 lines (332 loc) · 11.3 KB
/
Copy pathcpa
File metadata and controls
executable file
·364 lines (332 loc) · 11.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
#!/usr/bin/env bash
#
# cpa — CLIProxyAPI 启停包装脚本(nohup 后台方式)
#
# 用法:
# cpa on 启动 cli-proxy-api 后台运行,并打印关键信息
# cpa off 停止 cli-proxy-api
# cpa status 查看运行状态
# cpa info 只打印关键信息,不启停
# cpa log 实时查看日志 (tail -F)
# cpa help 帮助
#
# 可选环境变量:
# CPA_BIN 二进制路径(默认自动探测)
# CPA_CONFIG 配置文件路径(默认自动探测)
# CPA_ENV .env 文件路径(默认自动探测,与配置文件同目录优先)
# CPA_LOG 日志文件路径(默认 ~/Library/Logs/cliproxyapi.log)
# CPA_PIDFILE PID 文件路径(默认 ${TMPDIR:-/tmp}/cpa.pid)
#
# .env 支持:
# 若存在 .env,启动前会自动 source(变量导出到当前环境)。
# 若 config.yaml 中包含 ${VAR_NAME} 占位符,脚本会生成一份临时配置文件
# /tmp/cpa-config.*.yaml 供给 CPA 使用;停止时自动清理。
# 真实 API Key 不会写回原始 config.yaml。
set -u
# 解析脚本自身所在目录(跟随软链),用于在 cpa 同目录探测二进制与 config
__cpa_resolve_self() {
local src="${BASH_SOURCE[0]:-$0}"
case "$src" in
*/*) : ;;
*) src="$(command -v "$src" 2>/dev/null)" || src="$0" ;;
esac
while [ -L "$src" ]; do
local d; d="$(cd -P "$(dirname "$src")" 2>/dev/null && pwd)"
src="$(readlink "$src")"
case "$src" in /*) : ;; *) src="$d/$src" ;; esac
done
cd -P "$(dirname "$src")" 2>/dev/null && pwd
}
SELF_DIR="$(__cpa_resolve_self)"
LOG="${CPA_LOG:-$HOME/Library/Logs/cliproxyapi.log}"
PIDFILE="${CPA_PIDFILE:-${TMPDIR:-/tmp}/cpa.pid}"
# ---------- 路径探测 ----------
detect_bin() {
if [ -n "${CPA_BIN:-}" ]; then
printf '%s\n' "$CPA_BIN"; return 0
fi
local c p
# 优先:脚本同目录(cpa 通常与二进制一起安装)
for p in "$SELF_DIR/cli-proxy-api" "$SELF_DIR/cliproxyapi"; do
if [ -f "$p" ] && [ -x "$p" ]; then printf '%s\n' "$p"; return 0; fi
done
# PATH 查找(-f 排除同名目录被误判为可执行)
for c in cli-proxy-api cliproxyapi; do
p="$(command -v "$c" 2>/dev/null)"
if [ -n "$p" ] && [ -f "$p" ]; then printf '%s\n' "$p"; return 0; fi
done
for p in /opt/homebrew/opt/cliproxyapi/bin/cliproxyapi \
/usr/local/opt/cliproxyapi/bin/cliproxyapi \
/opt/homebrew/bin/cliproxyapi \
/usr/local/bin/cliproxyapi; do
if [ -f "$p" ] && [ -x "$p" ]; then printf '%s\n' "$p"; return 0; fi
done
return 1
}
detect_config() {
if [ -n "${CPA_CONFIG:-}" ]; then
printf '%s\n' "$CPA_CONFIG"; return 0
fi
local p bdir
# 优先:脚本同目录下的 config.yaml
if [ -f "$SELF_DIR/config.yaml" ]; then
printf '%s\n' "$SELF_DIR/config.yaml"; return 0
fi
# 二进制同目录下的 config.yaml(standalone 安装最常见)
bdir="$(dirname "$(detect_bin 2>/dev/null)" 2>/dev/null)"
if [ -n "$bdir" ] && [ -f "$bdir/config.yaml" ]; then
printf '%s\n' "$bdir/config.yaml"; return 0
fi
for p in "$HOME/.cli-proxy-api/config.yaml" \
/opt/homebrew/etc/cliproxyapi.conf \
/usr/local/etc/cliproxyapi.conf; do
if [ -f "$p" ]; then printf '%s\n' "$p"; return 0; fi
done
return 1
}
# 探测 .env 文件:与 config.yaml 始终同目录,故只查 config 所在目录(可被 CPA_ENV 显式覆盖)
detect_env() {
if [ -n "${CPA_ENV:-}" ]; then
printf '%s\n' "$CPA_ENV"; return 0
fi
local config_dir
config_dir="$(dirname "$1" 2>/dev/null)"
if [ -f "$config_dir/.env" ]; then
printf '%s\n' "$config_dir/.env"; return 0
fi
return 1
}
# 加载 .env,自动导出变量
load_env() {
local envfile="$1"
set -a
# shellcheck source=/dev/null
. "$envfile"
set +a
}
# 判断配置是否包含 ${VAR} 占位符
needs_render() {
python3 - "$1" <<'PY'
import sys, re
with open(sys.argv[1], "r", encoding="utf-8") as f:
text = f.read()
sys.exit(0 if re.search(r'\$\{[A-Za-z_][A-Za-z0-9_]*\}', text) else 1)
PY
}
# 渲染占位符为临时配置文件,返回临时文件路径
render_config() {
local template="$1"
local tmpfile
# macOS mktemp 要求 X 在文件名末尾,因此先不加后缀,生成后再重命名
tmpfile="$(mktemp "${TMPDIR:-/tmp}/cpa-config.XXXXXX")"
mv "$tmpfile" "$tmpfile.yaml"
tmpfile="$tmpfile.yaml"
python3 - "$template" "$tmpfile" <<'PY'
import sys, re, os
template_path, out_path = sys.argv[1], sys.argv[2]
def repl(match):
name = match.group(1)
return os.environ.get(name, match.group(0))
with open(template_path, "r", encoding="utf-8") as f:
text = f.read()
# 只替换 ${VAR} 形式;未定义的变量保留原样
rendered = re.sub(r'\$\{([A-Za-z_][A-Za-z0-9_]*)\}', repl, text)
with open(out_path, "w", encoding="utf-8") as f:
f.write(rendered)
PY
printf '%s\n' "$tmpfile"
}
# 读取端口 + 管理密码状态(只输出非敏感信息,绝不输出密码值)
read_port_key() {
python3 - "$1" <<'PY'
import sys, re
try:
text = open(sys.argv[1], encoding="utf-8", errors="replace").read()
except Exception:
text = ""
port = None
key_status = "未配置(配置文件中没有该字段)"
for line in text.splitlines():
s = line.strip()
if s.startswith("#") or ":" not in s:
continue
m = re.match(r"(port|secret-key)\s*:\s*(.*)", s)
if not m:
continue
k, v = m.group(1), m.group(2).strip()
if (v.startswith('"') and v.endswith('"')) or (v.startswith("'") and v.endswith("'")):
v = v[1:-1]
if k == "port":
digits = re.sub(r"\D", "", v)
if digits:
port = digits
elif k == "secret-key":
if v == "":
key_status = "为空(管理界面会被禁用 → 需设置后重启服务)"
elif v.startswith(("$2a$", "$2b$", "$2y$")):
key_status = "已设置(bcrypt 哈希,原明文无法找回)"
else:
key_status = "已设置(明文或环境变量引用)"
print(port or "8317")
print(key_status)
PY
}
print_info() {
local bin="$1" config="$2"
local pk port key_status
pk="$(read_port_key "$config")"
port="$(printf '%s\n' "$pk" | head -1)"
key_status="$(printf '%s\n' "$pk" | tail -1)"
echo ""
echo " ── CLIProxyAPI 关键信息 ──"
echo " 二进制 : $bin"
echo " 配置文件 : $config"
echo " WebUI 入口 : http://localhost:$port/management.html"
echo " 管理密码字段 : remote-management.secret-key"
echo " (在配置文件的 remote-management: 段下;这是 WebUI 登录密码,"
echo " 与 api-keys 里的 sk-* 是两回事)"
echo " 密码当前状态 : $key_status"
echo " 日志文件 : $LOG"
echo ""
}
# 打印 .env 相关状态:已找到则显示路径;未找到则提示如何改用环境变量(不阻断默认启动)
print_env_hint() {
local config="$1"
local config_dir envfile
config_dir="$(dirname "$config" 2>/dev/null)"
envfile="$(detect_env "$config" 2>/dev/null)"
if [ -n "${envfile:-}" ]; then
echo " .env : ${envfile}(已找到,API Key 将按环境变量加载)"
else
echo " .env : 未找到(当前按 config.yaml 原样加载,API Key 为明文)"
echo " 提示:为避免明文,可把 API Key 写成 \${VAR_NAME} 占位符,"
echo " 并将真实值写入 $config_dir/.env(与 config.yaml 同目录)"
echo " 例如 .env 内容: CPA_CLAUDE_API_KEY=sk-xxxx"
fi
echo ""
}
is_running() {
[ -f "$PIDFILE" ] || return 1
local pid
pid="$(cat "$PIDFILE" 2>/dev/null)"
[ -n "$pid" ] && kill -0 "$pid" 2>/dev/null
}
cmd_on() {
local bin config envfile effective_config
bin="$(detect_bin)" || { echo "✗ 未找到 cli-proxy-api,请先安装:brew install cliproxyapi" >&2; exit 1; }
config="$(detect_config)" || { echo "✗ 未找到配置文件(默认 ~/.cli-proxy-api/config.yaml)" >&2; exit 1; }
# 如果存在 .env,先加载环境变量;未找到则提示(但仍按默认方式启动)
envfile="$(detect_env "$config" 2>/dev/null)"
if [ -n "${envfile:-}" ]; then
load_env "$envfile"
echo "✔ 已加载环境变量: $envfile"
else
echo "ℹ 未找到 .env 文件(默认读取 config.yaml 原样启动)"
print_env_hint "$config"
fi
if is_running; then
echo "✔ cli-proxy-api 已在运行 (PID $(cat "$PIDFILE"))"
print_info "$bin" "$config"
return 0
fi
# 若配置含占位符,生成临时渲染文件;否则直接用原配置
if needs_render "$config"; then
effective_config="$(render_config "$config")"
echo "✔ 已生成临时渲染配置: $effective_config"
else
effective_config="$config"
fi
mkdir -p "$(dirname "$LOG")"
nohup "$bin" -config "$effective_config" >>"$LOG" 2>&1 &
local pid=$!
echo "$pid" > "$PIDFILE"
sleep 1.5
if kill -0 "$pid" 2>/dev/null; then
echo "✔ cli-proxy-api 已启动 (PID $pid)"
else
echo "✗ 启动失败,日志见: $LOG" >&2
tail -5 "$LOG" 2>/dev/null >&2
rm -f "$PIDFILE"
# 失败时也清理可能遗留的临时配置
rm -f "${TMPDIR:-/tmp}"/cpa-config.*.yaml
exit 1
fi
print_info "$bin" "$config"
}
cmd_off() {
local pid
if [ -f "$PIDFILE" ]; then
pid="$(cat "$PIDFILE" 2>/dev/null)"
if [ -n "$pid" ] && kill -0 "$pid" 2>/dev/null; then
kill "$pid"
local i=0
while kill -0 "$pid" 2>/dev/null && [ "$i" -lt 20 ]; do sleep 0.3; i=$((i+1)); done
if kill -0 "$pid" 2>/dev/null; then
echo "✗ 进程未响应,强制结束…" >&2
kill -9 "$pid" 2>/dev/null
fi
fi
rm -f "$PIDFILE"
fi
# 兜底:清理按名字匹配的残留进程(大小写不匹配,不会误杀本脚本)
pkill -f 'cli-proxy-api' 2>/dev/null
pkill -f 'cliproxyapi' 2>/dev/null
# 清理 envsubst 产生的临时配置文件
rm -f "${TMPDIR:-/tmp}"/cpa-config.*.yaml
echo "✔ cli-proxy-api 已停止"
}
cmd_status() {
local bin config
if is_running; then
echo "✔ cli-proxy-api 运行中 (PID $(cat "$PIDFILE"))"
else
echo "✗ cli-proxy-api 未运行"
fi
bin="$(detect_bin 2>/dev/null)"
config="$(detect_config 2>/dev/null)"
if [ -n "$bin" ] && [ -n "$config" ]; then
print_info "$bin" "$config"
print_env_hint "$config"
fi
}
cmd_info() {
local bin config
bin="$(detect_bin)" || bin="(未找到,请先 brew install cliproxyapi)"
config="$(detect_config)" || config="(未找到)"
if [ -f "$config" ]; then
print_info "$bin" "$config"
print_env_hint "$config"
else
echo " 二进制 : $bin"
echo " 配置文件 : $config"
fi
}
cmd_log() {
if [ ! -f "$LOG" ]; then
echo "日志文件不存在: $LOG" >&2
exit 1
fi
tail -n 100 -F "$LOG"
}
usage() {
cat <<EOF
用法: cpa <命令>
命令:
on 启动 cli-proxy-api(后台运行)并打印关键信息
off 停止 cli-proxy-api
status 查看运行状态
info 只打印关键信息,不启停
log 实时查看日志(Ctrl+C 退出)
help 显示本帮助
环境变量: CPA_BIN / CPA_CONFIG / CPA_ENV / CPA_LOG / CPA_PIDFILE
EOF
}
case "${1:-}" in
on) cmd_on ;;
off) cmd_off ;;
status|st) cmd_status ;;
info) cmd_info ;;
log) cmd_log ;;
help|-h|--help|"") usage ;;
*) echo "未知命令: $1" >&2; usage >&2; exit 1 ;;
esac