mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
chore: 清理 emoji 与修正发布脚本仓库名
产品代码统一为纯文本风格(照参考实现),去掉 emoji:billing/quota/usage 插件、 channels 的提示文案、WebUI 文案,以及 go 测试、docs、packaging 脚本中的 注意/OK/FAIL 等符号化写法。第三方目录(vendor、node_modules、.probe)不动。 另:packaging/upload-release.sh 的仓库名改为真实全名 JianFeeeee/ModelRouter → HeNanMU-EEA/ModelRouter(前者是重定向,POST/PUT 不跟随 会 404)。
This commit is contained in:
28
deploy.sh
28
deploy.sh
@ -122,14 +122,14 @@ backup_old_state() {
|
||||
log "备份旧二进制到 $BACKUP_BIN"
|
||||
if [[ -f "$TARGET_BIN" ]]; then
|
||||
cp -f "$TARGET_BIN" "$BACKUP_BIN"
|
||||
log "✓ 旧二进制已备份 (sha256=$(sha256sum "$BACKUP_BIN" | awk '{print $1}' | cut -c1-16))"
|
||||
log "OK 旧二进制已备份 (sha256=$(sha256sum "$BACKUP_BIN" | awk '{print $1}' | cut -c1-16))"
|
||||
else
|
||||
warn "旧二进制不存在,跳过备份(无回滚点)"
|
||||
fi
|
||||
|
||||
if [[ -f "$TARGET_CONFIG" ]]; then
|
||||
cp -f "$TARGET_CONFIG" "$BACKUP_CONFIG"
|
||||
log "✓ 旧配置已备份到 $BACKUP_CONFIG (sha256=$(sha256sum "$BACKUP_CONFIG" | awk '{print $1}' | cut -c1-16))"
|
||||
log "OK 旧配置已备份到 $BACKUP_CONFIG (sha256=$(sha256sum "$BACKUP_CONFIG" | awk '{print $1}' | cut -c1-16))"
|
||||
else
|
||||
warn "线上配置不存在: $TARGET_CONFIG(首次部署?无配置回滚点)"
|
||||
fi
|
||||
@ -175,9 +175,9 @@ build() {
|
||||
)
|
||||
for c in "${checks[@]}"; do
|
||||
if strings "$NEW_BIN" | grep -qF "$c"; then
|
||||
log " ✓ 新二进制包含: $c"
|
||||
log " OK 新二进制包含: $c"
|
||||
else
|
||||
warn " ⚠ 新二进制不包含: $c(非预期但非致命)"
|
||||
warn " 注意: 新二进制不包含: $c(非预期但非致命)"
|
||||
fi
|
||||
done
|
||||
}
|
||||
@ -207,7 +207,7 @@ atomic_replace_binary() {
|
||||
|
||||
# 验证
|
||||
NEW_SHA256=$(sha256sum "$TARGET_BIN" | awk '{print $1}')
|
||||
log "✓ 二进制已原子替换 (sha256=${NEW_SHA256:0:16})"
|
||||
log "OK 二进制已原子替换 (sha256=${NEW_SHA256:0:16})"
|
||||
}
|
||||
|
||||
# ---------- 原子替换配置 ----------
|
||||
@ -223,7 +223,7 @@ atomic_replace_config() {
|
||||
|
||||
log " rename → $TARGET_CONFIG (原子)"
|
||||
mv -f "$STAGING_CONFIG" "$TARGET_CONFIG"
|
||||
log "✓ 配置已原子替换 (sha256=$(sha256sum "$TARGET_CONFIG" | awk '{print $1}' | cut -c1-16))"
|
||||
log "OK 配置已原子替换 (sha256=$(sha256sum "$TARGET_CONFIG" | awk '{print $1}' | cut -c1-16))"
|
||||
}
|
||||
|
||||
# ---------- 配置预检 ----------
|
||||
@ -236,7 +236,7 @@ verify_config() {
|
||||
|
||||
local out
|
||||
if out=$("$TARGET_BIN" -check -config "$TARGET_CONFIG" 2>&1); then
|
||||
log "✓ 配置合法"
|
||||
log "OK 配置合法"
|
||||
return 0
|
||||
fi
|
||||
|
||||
@ -276,7 +276,7 @@ verify_master_key() {
|
||||
log " 钥匙来源: LLMS_PROXY_MASTER_KEY(环境变量)"
|
||||
# 走真实解密路径验证,而不是只检查格式
|
||||
if out=$("$TARGET_BIN" -show-secrets -config "$TARGET_CONFIG" 2>&1); then
|
||||
log "✓ 凭据可解封($(echo "$out" | grep -cE '^(source|key) ') 项)"
|
||||
log "OK 凭据可解封($(echo "$out" | grep -cE '^(source|key) ') 项)"
|
||||
return 0
|
||||
fi
|
||||
err "凭据无法用 LLMS_PROXY_MASTER_KEY 解封,未替换任何文件:"
|
||||
@ -292,7 +292,7 @@ verify_master_key() {
|
||||
log " 钥匙文件: $key_file ($(stat -c '%A' "$key_file" 2>/dev/null || echo '?'),$(wc -c < "$key_file") 字节)"
|
||||
|
||||
if out=$("$TARGET_BIN" -show-secrets -config "$TARGET_CONFIG" 2>&1); then
|
||||
log "✓ 凭据可解封($(echo "$out" | grep -cE '^(source|key) ') 项)"
|
||||
log "OK 凭据可解封($(echo "$out" | grep -cE '^(source|key) ') 项)"
|
||||
return 0
|
||||
fi
|
||||
err "凭据无法解密(钥匙与配置不匹配),未替换任何文件:"
|
||||
@ -388,7 +388,7 @@ sync_adapters() {
|
||||
grep -qE '\buses\b' "$TARGET_ADAPTERS/$f.lua" 2>/dev/null && ((usage_fixed++)) || true
|
||||
done
|
||||
log " usage 透传修复适配器: ${usage_fixed}/7 (openai/deepseek/github/groq/kimicode/mistral/opencode)"
|
||||
log "✓ 适配器已同步到 $TARGET_ADAPTERS/"
|
||||
log "OK 适配器已同步到 $TARGET_ADAPTERS/"
|
||||
}
|
||||
|
||||
# 同步内置插件到 plugin_dir。
|
||||
@ -449,14 +449,14 @@ sync_plugins() {
|
||||
local name; name="$(basename "$src")"
|
||||
[[ -f "$TARGET_PLUGINS/$name" ]] || warn " 缺少插件: $name"
|
||||
done
|
||||
log "✓ 插件已同步到 $TARGET_PLUGINS/(新增 $added,更新 $updated,未变 $same)"
|
||||
log "OK 插件已同步到 $TARGET_PLUGINS/(新增 $added,更新 $updated,未变 $same)"
|
||||
}
|
||||
|
||||
# ---------- 重启服务 ----------
|
||||
restart_service() {
|
||||
log "重启服务(中断 <500ms)"
|
||||
systemctl restart llmsproxy.service || fail "systemd restart 失败"
|
||||
log "✓ llmsproxy.service restarted"
|
||||
log "OK llmsproxy.service restarted"
|
||||
}
|
||||
|
||||
# ---------- 健康检查 ----------
|
||||
@ -471,7 +471,7 @@ healthcheck() {
|
||||
local code
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 3 "$HEALTH_URL" 2>/dev/null || echo "000")
|
||||
if [[ "$code" == "200" || "$code" == "401" ]]; then
|
||||
log "✓ 服务健康 (status=$code, ${attempt}/${attempts})"
|
||||
log "OK 服务健康 (status=$code, ${attempt}/${attempts})"
|
||||
return 0
|
||||
fi
|
||||
log " 等待中 ($attempt/$attempts, status=$code)"
|
||||
@ -540,7 +540,7 @@ rollback() {
|
||||
local code
|
||||
code=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$HEALTH_URL" 2>/dev/null || echo "000")
|
||||
if [[ "$code" == "200" || "$code" == "401" ]]; then
|
||||
log "✓ 回滚成功,旧版本已恢复服务 (status=$code)"
|
||||
log "OK 回滚成功,旧版本已恢复服务 (status=$code)"
|
||||
log " 新版本部署失败,请检查构建/配置后重试"
|
||||
return 0
|
||||
else
|
||||
|
||||
@ -46,7 +46,7 @@ plugin.ui = {
|
||||
page = {
|
||||
page_id = "hello", -- kebab-case
|
||||
title = "Hello",
|
||||
icon = "👋",
|
||||
icon = "",
|
||||
order = 90, -- 侧栏排序
|
||||
mount = [[<div id="hello">hello</div>]],
|
||||
},
|
||||
@ -211,7 +211,7 @@ plugin.ui = {
|
||||
page = {
|
||||
page_id = "billing", -- 必填,kebab-case。侧栏 data-tab 与 #tab-billing
|
||||
title = "Billing", -- 必填,侧栏文字
|
||||
icon = "💰", -- 可选
|
||||
icon = "", -- 可选
|
||||
order = 40, -- 侧栏排序,默认 100
|
||||
mount = [[...HTML...]],
|
||||
},
|
||||
@ -428,7 +428,7 @@ end
|
||||
因此插件持有**唯一一个**状态,所有入口(`Fire` / `State` / `SetState` / **定时任务**)
|
||||
都用这同一把互斥锁串行化。
|
||||
|
||||
⚠️ 这条约束对**定时任务尤其关键**:任务和钩子必须是同一个 VM,否则任务写的
|
||||
注意: 这条约束对**定时任务尤其关键**:任务和钩子必须是同一个 VM,否则任务写的
|
||||
`plugin.state` 是一个请求路径永远读不到的副本 —— token 轮换看起来成功了,
|
||||
对实际流量完全无效。
|
||||
|
||||
@ -449,7 +449,7 @@ end
|
||||
与适配器相同:`json.encode` / `json.decode`、`log(level, msg)`、
|
||||
`hmac_sha256_hex` / `sha256_hex` / `base64_encode` / `tohex`。
|
||||
|
||||
⚠️ `os.date` 在极简运行时里可能不可用(计费插件因此有降级路径)。
|
||||
注意: `os.date` 在极简运行时里可能不可用(计费插件因此有降级路径)。
|
||||
|
||||
### 6.4 命名
|
||||
|
||||
@ -536,7 +536,7 @@ curl -H "Authorization: Bearer $ADMIN_KEY" \
|
||||
{ "prices": { ... }, "count_failures": true }
|
||||
```
|
||||
|
||||
### 7.5 ⚠️ 计费插件只报表,不执法
|
||||
### 7.5 注意: 计费插件只报表,不执法
|
||||
|
||||
**网关自己的配额会计(`internal/gateway/stats.go`,入口处强制)才是限额权威。**
|
||||
本插件不参与任何路由或配额决策。
|
||||
|
||||
@ -47,7 +47,7 @@ func TestWildcardTokenRuleDoesNotMarkEverySourcePriced(t *testing.T) {
|
||||
// Paid sources must NOT be marked, or their unlisted models bill $0.
|
||||
for _, name := range []string{"paid1", "paid2"} {
|
||||
if _, ok := srcs[name]; ok {
|
||||
t.Errorf("★ wildcard token rule marked %q as priced at 0 — its unlisted "+
|
||||
t.Errorf("wildcard token rule marked %q as priced at 0 — its unlisted "+
|
||||
"models will bill $0 instead of appearing as unpriced", name)
|
||||
}
|
||||
}
|
||||
|
||||
@ -128,7 +128,7 @@ func TestLoomyWechatPollBehavior(t *testing.T) {
|
||||
// 第二拍 404 —— **关键**:已扫码但**不是**完成(早期读反会卡这里)
|
||||
st := h.pollOnce(sid)
|
||||
if st["state"] != "pending" {
|
||||
t.Fatalf("★ 404(已扫码待确认) 必须是 pending 而非 completed, 实得 %v", st["state"])
|
||||
t.Fatalf("404(已扫码待确认) 必须是 pending 而非 completed, 实得 %v", st["state"])
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@ -92,7 +92,7 @@ func TestChannelsPluginEndToEnd(t *testing.T) {
|
||||
}
|
||||
// 且不得出现在 state 里
|
||||
if stateHasSecret(ps, "channels", "tok-1") {
|
||||
t.Fatal("★ 凭据泄漏进了 plugin.state")
|
||||
t.Fatal("凭据泄漏进了 plugin.state")
|
||||
}
|
||||
|
||||
// 5. 运维授权后,重新接入应当真的把源注册进内核
|
||||
|
||||
@ -175,7 +175,7 @@ func TestQoderDevicePollBehavior(t *testing.T) {
|
||||
t.Fatalf("池路径应完成: %v", st)
|
||||
}
|
||||
if _, ok, _ := h.ps.GetSecret("channels", "channels:qoder:"+aid); !ok {
|
||||
t.Fatalf("★ 池路径凭据未落到 channels:qoder:%s", aid)
|
||||
t.Fatalf("池路径凭据未落到 channels:qoder:%s", aid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -159,7 +159,7 @@ func TestRaccoonQrPollBehavior(t *testing.T) {
|
||||
_, sid := hh.begin()
|
||||
st := hh.pollOnce(sid)
|
||||
if st["state"] != "pending" {
|
||||
t.Fatalf("★ 缺 token 的 success 必须 pending(否则产出空凭据并卡死), 实得 %v", st["state"])
|
||||
t.Fatalf("缺 token 的 success 必须 pending(否则产出空凭据并卡死), 实得 %v", st["state"])
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -53,7 +53,7 @@ func TestChannelsStoreIsPluginOwned(t *testing.T) {
|
||||
storePath := filepath.Join(dir, ".channels-store.json")
|
||||
b, err := os.ReadFile(storePath)
|
||||
if err != nil {
|
||||
t.Fatalf("★ 插件自有存储未写出(%s): %v\n"+
|
||||
t.Fatalf("插件自有存储未写出(%s): %v\n"+
|
||||
" —— 状态落盘必须由插件自己负责,不能依赖内核分发路径", storePath, err)
|
||||
}
|
||||
var saved struct {
|
||||
@ -94,7 +94,7 @@ func TestChannelsStoreIsPluginOwned(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("★ 重启后账号 %s 未恢复 —— 持久化未生效", aid)
|
||||
t.Fatalf("重启后账号 %s 未恢复 —— 持久化未生效", aid)
|
||||
}
|
||||
}
|
||||
|
||||
@ -155,7 +155,7 @@ func TestChannelsMigratesLegacyKernelState(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatal("★ 遗留账号未迁移 —— 升级即丢账号")
|
||||
t.Fatal("遗留账号未迁移 —— 升级即丢账号")
|
||||
}
|
||||
|
||||
// ② 自有存储必须已写出
|
||||
@ -164,7 +164,7 @@ func TestChannelsMigratesLegacyKernelState(t *testing.T) {
|
||||
}
|
||||
// ③ 遗留文件必须被删除(否则内核 restore() 会用它覆盖自有存储)
|
||||
if _, err := os.Stat(legacyPath); !os.IsNotExist(err) {
|
||||
t.Fatal("★ 遗留内核文件应被删除,否则下次启动会被它覆盖")
|
||||
t.Fatal("遗留内核文件应被删除,否则下次启动会被它覆盖")
|
||||
}
|
||||
}
|
||||
|
||||
@ -213,7 +213,7 @@ func TestChannelsMigrationKeepsLegacyOnWriteFailure(t *testing.T) {
|
||||
|
||||
// 写失败的态下,遗留文件必须原样保留
|
||||
if _, err := os.Stat(legacyPath); err != nil {
|
||||
t.Fatalf("★ 写自有存储失败时遗留文件被删了 —— 数据被一次迁移弄丢: %v", err)
|
||||
t.Fatalf("写自有存储失败时遗留文件被删了 —— 数据被一次迁移弄丢: %v", err)
|
||||
}
|
||||
// 且自有存储不应存在(写失败了)
|
||||
if _, err := os.Stat(filepath.Join(sub, ".channels-store.json")); err == nil {
|
||||
|
||||
@ -204,7 +204,7 @@ func TestLegacyVaultPlaintextLoad(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(raw), "PLAIN-OLD-TOKEN") {
|
||||
t.Fatalf("★ 重封后旧明文仍在盘上:\n%s", raw)
|
||||
t.Fatalf("重封后旧明文仍在盘上:\n%s", raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), "enc:v1:") {
|
||||
t.Fatalf("重封后应含密文:\n%s", raw)
|
||||
@ -252,7 +252,7 @@ func TestLegacySourceSealedConfig(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(raw), "PLAIN-LEGACY-KEY") {
|
||||
t.Fatalf("★ 旧明文 api_key 升级后仍明文落盘:\n%s", raw)
|
||||
t.Fatalf("旧明文 api_key 升级后仍明文落盘:\n%s", raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), "enc:v1:") {
|
||||
t.Fatalf("落盘应含 enc:v1 密文:\n%s", raw)
|
||||
|
||||
@ -182,7 +182,7 @@ func TestZcodeInitPollBehavior(t *testing.T) {
|
||||
t.Fatalf("池路径应完成: %v", st)
|
||||
}
|
||||
if _, ok, _ := h.ps.GetSecret("channels", "channels:zcode:"+aid); !ok {
|
||||
t.Fatalf("★ 池路径凭据未落到 channels:zcode:%s", aid)
|
||||
t.Fatalf("池路径凭据未落到 channels:zcode:%s", aid)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -66,7 +66,7 @@ func TestDevicePollMutants(t *testing.T) {
|
||||
t.Fatal("基准决策表未全过 —— 测试自身失效")
|
||||
}
|
||||
if newDeviceHarness(t, mutated).runDecisionTable() {
|
||||
t.Fatalf("★ 变异体幸存(%s)—— 该分支无覆盖", mu.name)
|
||||
t.Fatalf("变异体幸存(%s)—— 该分支无覆盖", mu.name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -34,6 +34,6 @@ return plugin
|
||||
t.Fatalf("动作未成功: %#v", m)
|
||||
}
|
||||
if _, ok := c.FindKey("sk-kernel-0001"); ok {
|
||||
t.Fatal("★ 动作报告成功但副作用没发生 —— dispatch 是空转")
|
||||
t.Fatal("动作报告成功但副作用没发生 —— dispatch 是空转")
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,7 +67,7 @@ func TestBillingRulesCRUD(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if n := len(got.DSL.Profiles[0].Rules); n != 2 {
|
||||
t.Fatalf("★ re-adding an existing url appended instead of replacing: %d rules", n)
|
||||
t.Fatalf("re-adding an existing url appended instead of replacing: %d rules", n)
|
||||
}
|
||||
var mode string
|
||||
for _, r := range got.DSL.Profiles[0].Rules {
|
||||
@ -105,7 +105,7 @@ func TestBillingRulesRejectUnknownURLWithWarning(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got.Warnings) == 0 {
|
||||
t.Fatal("★ a rule matching no configured source was accepted with no warning — " +
|
||||
t.Fatal("a rule matching no configured source was accepted with no warning — " +
|
||||
"it will price nothing and every request will be recorded as unpriced")
|
||||
}
|
||||
if !strings.Contains(got.Warnings[0], "typo.example.com") {
|
||||
@ -136,7 +136,7 @@ func TestBillingRulesInvalidIsRejectedNotPersisted(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got.DSL.Profiles[0].Rules) != 1 {
|
||||
t.Errorf("★ a rejected rule was persisted anyway: %d rules", len(got.DSL.Profiles[0].Rules))
|
||||
t.Errorf("a rejected rule was persisted anyway: %d rules", len(got.DSL.Profiles[0].Rules))
|
||||
}
|
||||
// An unparseable price must be refused too, not silently priced at zero.
|
||||
if rr := rulesDo(t, g, http.MethodPost, `{"profile":"p","rule":{"url":"https://y/v1","mode":"token",
|
||||
@ -175,7 +175,7 @@ func TestBillingRulesPersistAndReachThePlugin(t *testing.T) {
|
||||
t.Fatalf("read config: %v", err)
|
||||
}
|
||||
if !strings.Contains(string(raw), "commandcode.ai") {
|
||||
t.Errorf("★ the rule is not in config.yaml on disk — an edit that does not "+
|
||||
t.Errorf("the rule is not in config.yaml on disk — an edit that does not "+
|
||||
"survive a restart:\n%s", raw)
|
||||
}
|
||||
reloaded, err := config.Load(cfg.Path)
|
||||
@ -226,7 +226,7 @@ func TestBillingRulesPersistAndReachThePlugin(t *testing.T) {
|
||||
}
|
||||
cc, ok := sources["commandcode"]
|
||||
if !ok {
|
||||
t.Fatalf("★ the rule was saved but never injected: prices has %v, "+
|
||||
t.Fatalf("the rule was saved but never injected: prices has %v, "+
|
||||
"no commandcode entry. Every request on it stays unpriced.", keysOf(sources))
|
||||
}
|
||||
if cc == nil {
|
||||
|
||||
@ -241,7 +241,7 @@ func TestPeriodEmptyWindowFlagsTruncated(t *testing.T) {
|
||||
t.Errorf("empty window reqs = %d, want 0", got.Total.Reqs)
|
||||
}
|
||||
if !got.Truncated {
|
||||
t.Error("★ an empty bounded window must be flagged truncated — otherwise a " +
|
||||
t.Error("an empty bounded window must be flagged truncated — otherwise a " +
|
||||
"rotated-away week reads as 'no traffic that week'")
|
||||
}
|
||||
}
|
||||
|
||||
@ -992,7 +992,7 @@
|
||||
adPoolPeak: "峰值",
|
||||
adPoolSteps: "步长 (扩/缩)",
|
||||
cModel: "模型",
|
||||
cImageModels: "🖼 图像模型(不可选,请到「生图」页测试)",
|
||||
cImageModels: "图像模型(不可选,请到「生图」页测试)",
|
||||
urlHint: "填主机地址,通常不带 /v1 —— 网关会自动拼接路径。但各家约定不同:若填 https://x.com/v1,对话路径会正确,而图像生成会变成 /v1/v1/… 而 404。不确定就用上方「自动探测模型」,它会探测并填好正确形式。",
|
||||
navImage: "生图",
|
||||
imgWelcome: "输入提示词开始生成",
|
||||
@ -1326,7 +1326,7 @@
|
||||
adPoolPeak: "Peak",
|
||||
adPoolSteps: "Steps (grow/shrink)",
|
||||
cModel: "Model",
|
||||
cImageModels: "🖼 Image models (not selectable — use the Image tab)",
|
||||
cImageModels: "Image models (not selectable — use the Image tab)",
|
||||
urlHint: "Host address, usually WITHOUT /v1 — the gateway appends the path. Vendors differ: with https://x.com/v1 chat works but image generation becomes /v1/v1/… and 404s. If unsure use Auto-discover above; it probes and fills in the correct form.",
|
||||
navImage: "Image",
|
||||
imgWelcome: "Describe an image to generate it",
|
||||
|
||||
@ -344,7 +344,7 @@ func TestPluginPagesAreReachableByGoTab(t *testing.T) {
|
||||
body := ui[goTab : goTab+end]
|
||||
|
||||
if !strings.Contains(body, "PLUGIN_PAGES.forEach") {
|
||||
t.Error("★ goTab() does not toggle plugin pages — a plugin page keeps its " +
|
||||
t.Error("goTab() does not toggle plugin pages — a plugin page keeps its " +
|
||||
"`hidden` class forever, so the sidebar entry does nothing")
|
||||
}
|
||||
|
||||
|
||||
@ -909,7 +909,7 @@ func TestBillingTracksCacheUsage(t *testing.T) {
|
||||
}
|
||||
// Only the first request reported a cache number.
|
||||
if total["cache_reported_reqs"] != float64(1) {
|
||||
t.Errorf("★ total.cache_reported_reqs = %v, want 1 — a source that never "+
|
||||
t.Errorf("total.cache_reported_reqs = %v, want 1 — a source that never "+
|
||||
"reports cache usage must be distinguishable from one reporting zero hits",
|
||||
total["cache_reported_reqs"])
|
||||
}
|
||||
@ -986,11 +986,11 @@ func TestBillingCacheHitClampedInStats(t *testing.T) {
|
||||
st := ps.State("billing").(map[string]interface{})
|
||||
tot := st["total"].(map[string]interface{})
|
||||
if tot["cache_hit_tokens"] != float64(100) {
|
||||
t.Errorf("★ cache_hit_tokens = %v, want 100 (clamped to prompt_tokens)",
|
||||
t.Errorf("cache_hit_tokens = %v, want 100 (clamped to prompt_tokens)",
|
||||
tot["cache_hit_tokens"])
|
||||
}
|
||||
if tot["cache_fresh_tokens"] != float64(0) {
|
||||
t.Errorf("★ cache_fresh_tokens = %v, want 0, never negative",
|
||||
t.Errorf("cache_fresh_tokens = %v, want 0, never negative",
|
||||
tot["cache_fresh_tokens"])
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,7 +228,7 @@ func TestBillingPageCoversItsData(t *testing.T) {
|
||||
}
|
||||
|
||||
// TestPluginIconIsNotARawEmoji guards the sidebar icon. The billing plugin
|
||||
// declared icon = "💰" and the WebUI drops that verbatim into the button, while
|
||||
// declared icon = "" and the WebUI drops that verbatim into the button, while
|
||||
// every native tab uses an inline SVG styled with `stroke: currentColor`. An
|
||||
// emoji there renders at the wrong size and ignores the theme, so it does not
|
||||
// match its neighbours — which is what the operator reported.
|
||||
@ -335,7 +335,7 @@ func TestBillingTableHeaderMatchesRowColumns(t *testing.T) {
|
||||
td := strings.Count(row, "</td>")
|
||||
|
||||
if th != td {
|
||||
t.Errorf("★ header declares %d columns but row() emits %d cells — the "+
|
||||
t.Errorf("header declares %d columns but row() emits %d cells — the "+
|
||||
"table is misaligned from the first differing column on", th, td)
|
||||
}
|
||||
}
|
||||
|
||||
@ -90,7 +90,7 @@ func TestFastPathIsActuallyUsed(t *testing.T) {
|
||||
"ts": float64(1700000000),
|
||||
}
|
||||
if _, ok := fastToPlain(realistic); !ok {
|
||||
t.Errorf("★ the realistic request payload does NOT take the fast path — " +
|
||||
t.Errorf("the realistic request payload does NOT take the fast path — " +
|
||||
"the JSON round-trip is still on the hot path for real traffic")
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,7 +79,7 @@ return p
|
||||
psOut := ps.Fire(StageRequestEnd, map[string]interface{}{"model": "m"})
|
||||
|
||||
if singleOut["who"] != "from-aaa" {
|
||||
t.Errorf("★ single-plugin path dropped the return value: %v", singleOut)
|
||||
t.Errorf("single-plugin path dropped the return value: %v", singleOut)
|
||||
}
|
||||
// Two plugins both return `who`; the merge order decides the winner, and it
|
||||
// must be deterministic (load order), not scheduler-dependent.
|
||||
@ -120,7 +120,7 @@ return p
|
||||
for i := 0; i < 60; i++ {
|
||||
got := ps.Fire(StageRequestEnd, map[string]interface{}{"model": "m"})["winner"]
|
||||
if got != first {
|
||||
t.Fatalf("★ winner changed between runs: %q then %q — merge depends on goroutine "+
|
||||
t.Fatalf("winner changed between runs: %q then %q — merge depends on goroutine "+
|
||||
"scheduling, so the same configuration behaves differently run to run", first, got)
|
||||
}
|
||||
}
|
||||
@ -164,7 +164,7 @@ return p
|
||||
t.Fatal("healthy plugin has no state")
|
||||
}
|
||||
if n := st["n"]; n != float64(1) {
|
||||
t.Errorf("★ healthy plugin did not run alongside the throwing one: n = %v", n)
|
||||
t.Errorf("healthy plugin did not run alongside the throwing one: n = %v", n)
|
||||
}
|
||||
he := ps.HookErrors()
|
||||
if len(he) == 0 {
|
||||
|
||||
@ -91,7 +91,7 @@ func TestStateSurvivesRestart(t *testing.T) {
|
||||
loadCounter(t, ps2)
|
||||
|
||||
if got := stateN(t, ps2); got != 25 {
|
||||
t.Fatalf("★ total after restart = %v, want 25 — this is the 'restart loses the books' bug", got)
|
||||
t.Fatalf("total after restart = %v, want 25 — this is the 'restart loses the books' bug", got)
|
||||
}
|
||||
}
|
||||
|
||||
@ -160,7 +160,7 @@ func TestPricesOnlyUpdatePersists(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if saved.Prices["rate"] != float64(42) {
|
||||
t.Errorf("★ new price not persisted: %v — a restart would revert to the old price", saved.Prices)
|
||||
t.Errorf("new price not persisted: %v — a restart would revert to the old price", saved.Prices)
|
||||
}
|
||||
if saved.State["n"] != float64(5) {
|
||||
t.Errorf("state was clobbered by a price change: %v", saved.State["n"])
|
||||
@ -189,7 +189,7 @@ return plugin
|
||||
}
|
||||
out := ps.Fire(StageRequestEnd, map[string]interface{}{"model": "m", "source": "s"})
|
||||
if out["cost_usd"] != 1.25 {
|
||||
t.Errorf("★ hook return value lost: payload = %v", out)
|
||||
t.Errorf("hook return value lost: payload = %v", out)
|
||||
}
|
||||
if out["verdict"] != "billed" {
|
||||
t.Errorf("merged field lost: %v", out)
|
||||
@ -269,7 +269,7 @@ func TestCloseFlushesTail(t *testing.T) {
|
||||
|
||||
b, err := os.ReadFile(ps.stateFile("counter"))
|
||||
if err != nil {
|
||||
t.Fatalf("★ Close() did not flush: %v — a shutdown loses the tail", err)
|
||||
t.Fatalf("Close() did not flush: %v — a shutdown loses the tail", err)
|
||||
}
|
||||
var saved struct {
|
||||
State map[string]interface{} `json:"state"`
|
||||
@ -325,7 +325,7 @@ return plugin
|
||||
|
||||
b, err := os.ReadFile(ps.stateFile("ondemand"))
|
||||
if err != nil {
|
||||
t.Fatalf("★ an exported call did not mark the plugin dirty: %v — a plugin "+
|
||||
t.Fatalf("an exported call did not mark the plugin dirty: %v — a plugin "+
|
||||
"with no hooks can never persist its work", err)
|
||||
}
|
||||
var saved struct {
|
||||
|
||||
@ -47,6 +47,6 @@ return plugin`
|
||||
time.Sleep(500 * time.Millisecond)
|
||||
if err := exec.Command("kill", "-0", pid).Run(); err == nil {
|
||||
exec.Command("kill", "-9", pid).Run()
|
||||
t.Fatal("★ unload 后进程仍存活 —— on_unload 未被调用")
|
||||
t.Fatal("unload 后进程仍存活 —— on_unload 未被调用")
|
||||
}
|
||||
}
|
||||
|
||||
@ -260,7 +260,7 @@ end
|
||||
-- 语义:命中任一 window ⇒ 乘以 multiplier。hours 用 {起,止} 闭区间,跨零点
|
||||
-- 用 {{22,24}} 表示 22:00-24:00(24 是"当天最后一刻")。
|
||||
--
|
||||
-- ★ 为什么用 os.date 的 ! 前缀取 UTC:provider 的费率表按 UTC 标注,而网关
|
||||
-- 关键:为什么用 os.date 的 ! 前缀取 UTC:provider 的费率表按 UTC 标注,而网关
|
||||
-- 跑在本地时区(这台机是 Asia/Hong_Kong)。混用本地小时会让峰谷整体偏移 8
|
||||
-- 小时,白天算成夜间——比不做峰谷还糟。
|
||||
local function inPeakWindow(ev)
|
||||
|
||||
@ -1177,7 +1177,7 @@ PLUGIN.ui = {
|
||||
'" style="width:'+Math.min(100,p)+'%"></div></div>'+
|
||||
'<div class="qa-w-d">$'+used.toFixed(2)+' / $'+cap.toFixed(0)+'</div>';
|
||||
if(r.cap_mismatch){
|
||||
h+='<div class="qa-warnl" title="已用+余额 与套餐额度不符,比例仅供参考">⚠ 额度口径可能有变</div>';
|
||||
h+='<div class="qa-warnl" title="已用+余额 与套餐额度不符,比例仅供参考">注意 额度口径可能有变</div>';
|
||||
}
|
||||
return h+"</div>";
|
||||
}
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
-- 取自己的 /api/stats —— 它已按调用 key 隔离(实测:user key 231 次 = 自己的,
|
||||
-- admin 33847 次 = 全量)。
|
||||
--
|
||||
-- ★ 必须传**掩码**,不能传完整 key。
|
||||
-- 关键:必须传**掩码**,不能传完整 key。
|
||||
-- /api/stats 的统计 map 以 keyID(req.Key)(即 "***f9a9ed")为键 —— by_key
|
||||
-- 返回的就是它,面板传的也是它。而 admin 身份下 ?key= 接受任意值:传完整 key
|
||||
-- 会查不到,拿到零值。实测:
|
||||
|
||||
@ -220,7 +220,7 @@ p.ui = {
|
||||
page = {
|
||||
page_id = "billing",
|
||||
title = "Billing",
|
||||
icon = "💰",
|
||||
icon = "",
|
||||
order = 50,
|
||||
mount = "<div id=billing>hi</div><script>console.log('m')</script>",
|
||||
},
|
||||
|
||||
@ -98,7 +98,7 @@ func TestSecretVaultEncryptedAtRest(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if strings.Contains(string(raw), "SUPER-TOKEN-123") {
|
||||
t.Fatalf("★ 保险库明文落盘! 文件内容含 token:\n%s", raw)
|
||||
t.Fatalf("保险库明文落盘! 文件内容含 token:\n%s", raw)
|
||||
}
|
||||
if !strings.Contains(string(raw), testEncPrefix) {
|
||||
t.Fatalf("落盘值应带 %s 前缀(统一密文形态):\n%s", testEncPrefix, raw)
|
||||
@ -140,7 +140,7 @@ func TestSecretVaultPlaintextFallbackAndUpgrade(t *testing.T) {
|
||||
}
|
||||
raw, _ := os.ReadFile(filepath.Join(dir, ".secrets", "channels.json"))
|
||||
if strings.Contains(string(raw), "OLD-PLAIN") {
|
||||
t.Fatalf("★ 升级写后旧明文值仍在盘上:\n%s", raw)
|
||||
t.Fatalf("升级写后旧明文值仍在盘上:\n%s", raw)
|
||||
}
|
||||
var m map[string]string
|
||||
if err := json.Unmarshal(raw, &m); err != nil {
|
||||
@ -169,7 +169,7 @@ func TestSecretVaultWrongMasterKey(t *testing.T) {
|
||||
// 换一把钥匙(模拟 master.key 丢失重建)
|
||||
ps.SetSecretBox(failBox{})
|
||||
if _, _, err := ps.GetSecret("channels", "k"); err == nil {
|
||||
t.Fatal("★ 换 master key 后读库未报错 —— 会静默吞凭据")
|
||||
t.Fatal("换 master key 后读库未报错 —— 会静默吞凭据")
|
||||
}
|
||||
}
|
||||
|
||||
@ -205,7 +205,7 @@ func TestSecretVaultConcurrentPlugins(t *testing.T) {
|
||||
// 跨插件必须不可见
|
||||
other := "plug" + string(rune('A'+(p+1)%plugins))
|
||||
if _, ok, _ := ps.GetSecret(other, key); ok {
|
||||
t.Errorf("★ 凭据串台: %s 的键在 %s 可见", name, other)
|
||||
t.Errorf("凭据串台: %s 的键在 %s 可见", name, other)
|
||||
return
|
||||
}
|
||||
if g == 1 && i%10 == 9 {
|
||||
|
||||
@ -32,7 +32,7 @@ func TestResultIsEmptyMatchesTheReasoningOnlyResponse(t *testing.T) {
|
||||
}
|
||||
|
||||
// The same response WITH text is a normal answer.
|
||||
if resultIsEmpty(&types.UnifiedResponse{Content: "Hi! 👋", ReasoningContent: "hmm"}) {
|
||||
if resultIsEmpty(&types.UnifiedResponse{Content: "Hi! ", ReasoningContent: "hmm"}) {
|
||||
t.Error("a response with content is not empty")
|
||||
}
|
||||
// A tool-calling agent turn has no text and is perfectly valid.
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# 这个是打包进发行包、供首次安装使用的模板。它故意不含任何可用的凭据:
|
||||
# 首次启动时程序会生成一个随机 admin key 并写入实际配置(WebUI「密钥」页可见)。
|
||||
#
|
||||
# ⚠ 不要在本文件里填一个写死的具体 key。示例文件是公开的,写进去的任何值
|
||||
# 注意: 不要在本文件里填一个写死的具体 key。示例文件是公开的,写进去的任何值
|
||||
# 都会成为一个全网皆知的 admin key(旧版示例里的 sk-gw-local-0001 就踩过
|
||||
# 这个坑:它被直接抄进生产配置并真实可用)。
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@ for f in "$DIST"/llmsproxy_*.deb "$DIST"/llmsproxy-*.rpm "$DIST"/*.tar.gz; do
|
||||
[ -f "$f" ] || continue
|
||||
sz=$(stat -c%s "$f")
|
||||
[ "$sz" -gt 100000 ] || { echo "[core-dist] FATAL: $f suspiciously small ($sz B)"; exit 1; }
|
||||
log " ✓ $f ($((sz/1024)) KB)"
|
||||
log " OK $f ($((sz/1024)) KB)"
|
||||
done
|
||||
# dpkg-deb takes ONE archive; every extra argument is read as an additional
|
||||
# control-component name. With the glob unquoted this used to expand to three
|
||||
@ -133,7 +133,7 @@ for want in llmsproxy config.example.yaml llmsproxy.service plugins/billing.lua;
|
||||
echo " (release notes claim the billing plugin ships with the core)"
|
||||
exit 1
|
||||
fi
|
||||
log " ✓ archive contains $want"
|
||||
log " OK archive contains $want"
|
||||
done
|
||||
# Directories are matched by their CONTENTS, not by name: tar written with
|
||||
# `tar -czf` from a populated tree lists "…/adapters/openai.lua" and never a bare
|
||||
@ -144,6 +144,6 @@ if [ "$n_adapters" -lt 10 ]; then
|
||||
echo "[core-dist] FATAL: archive has only $n_adapters adapters (expected >= 10)"
|
||||
exit 1
|
||||
fi
|
||||
log " ✓ archive contains $n_adapters adapters"
|
||||
log " OK archive contains $n_adapters adapters"
|
||||
|
||||
log "done: $DIST"
|
||||
@ -53,7 +53,7 @@ if command -v python3 >/dev/null 2>&1; then
|
||||
fi
|
||||
|
||||
if [ ${#missing[@]} -eq 0 ]; then
|
||||
echo "✓ 浏览器栈依赖齐全"
|
||||
echo "OK 浏览器栈依赖齐全"
|
||||
echo " chromium: ${chromium}"
|
||||
echo " noVNC: ${novnc}"
|
||||
exit 0
|
||||
@ -98,24 +98,24 @@ echo
|
||||
echo "复核安装结果:"
|
||||
ok=1
|
||||
for b in "${BINS[@]}"; do
|
||||
if command -v "$b" >/dev/null 2>&1; then echo " ✓ $b"
|
||||
else echo " ✗ $b"; ok=0; fi
|
||||
if command -v "$b" >/dev/null 2>&1; then echo " OK $b"
|
||||
else echo " FAIL $b"; ok=0; fi
|
||||
done
|
||||
chromium=""
|
||||
for c in "${CHROMIUM_CANDIDATES[@]}"; do
|
||||
if command -v "$c" >/dev/null 2>&1; then chromium="$c"; break; fi
|
||||
done
|
||||
[ -n "$chromium" ] && echo " ✓ $chromium" || { echo " ✗ chromium"; ok=0; }
|
||||
[ -n "$chromium" ] && echo " OK $chromium" || { echo " FAIL chromium"; ok=0; }
|
||||
novnc=""
|
||||
for d in "${NOVNC_DIRS[@]}"; do
|
||||
[ -f "$d/vnc.html" ] && { novnc="$d"; break; }
|
||||
done
|
||||
[ -n "$novnc" ] && echo " ✓ noVNC ($novnc)" || { echo " ✗ noVNC"; ok=0; }
|
||||
python3 -c 'import websocket' >/dev/null 2>&1 && echo " ✓ python3 websocket-client" || { echo " ✗ python3 websocket-client"; ok=0; }
|
||||
[ -n "$novnc" ] && echo " OK noVNC ($novnc)" || { echo " FAIL noVNC"; ok=0; }
|
||||
python3 -c 'import websocket' >/dev/null 2>&1 && echo " OK python3 websocket-client" || { echo " FAIL python3 websocket-client"; ok=0; }
|
||||
|
||||
if [ "$ok" = 1 ]; then
|
||||
echo
|
||||
echo "✓ 浏览器栈就绪。重启 llmsproxy 后,flow=vnc 的平台即可用 VNC 兜底登录。"
|
||||
echo "OK 浏览器栈就绪。重启 llmsproxy 后,flow=vnc 的平台即可用 VNC 兜底登录。"
|
||||
else
|
||||
echo
|
||||
echo "WARN: 仍有缺失项,VNC 兜底在该机器上不可用。插件的 deps/status 端点会一样报告。" >&2
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
set -uo pipefail
|
||||
|
||||
TOKEN="$(tr -d '\n\r' < /tmp/gc_token)"
|
||||
REPO="JianFeeeee/ModelRouter"
|
||||
REPO="HeNanMU-EEA/ModelRouter"
|
||||
TAG="${1:?usage: upload-release.sh <tag> <file>...}"
|
||||
shift
|
||||
|
||||
@ -43,7 +43,7 @@ try:
|
||||
except Exception:
|
||||
print("")' 2>/dev/null)"
|
||||
if [ -z "$url" ]; then
|
||||
echo "✗ 取得上传地址失败: ${resp:0:140}"
|
||||
echo "FAIL 取得上传地址失败: ${resp:0:140}"
|
||||
continue
|
||||
fi
|
||||
|
||||
@ -63,9 +63,9 @@ print(subprocess.run(args, capture_output=True, text=True).stdout)
|
||||
' "$f" "$url" 2>/dev/null)"
|
||||
|
||||
if [ "$code" = "200" ] || [ "$code" = "201" ] || [ "$code" = "204" ]; then
|
||||
echo "✓ HTTP=$code"
|
||||
echo "OK HTTP=$code"
|
||||
else
|
||||
echo "✗ HTTP=$code"
|
||||
echo "FAIL HTTP=$code"
|
||||
fi
|
||||
sleep 1
|
||||
done
|
||||
|
||||
@ -43,7 +43,7 @@ for entry in "${checks[@]}"; do
|
||||
if [ "$sz" -lt "$min" ]; then
|
||||
fail "$(basename "$match") is only $sz bytes (min $min) — likely a degenerate shell like the 1.3.0 264KB exe"
|
||||
fi
|
||||
log " ✓ $(basename "$match") ($((sz/1024)) KB)"
|
||||
log " OK $(basename "$match") ($((sz/1024)) KB)"
|
||||
done
|
||||
|
||||
log "all artifacts healthy"
|
||||
|
||||
Reference in New Issue
Block a user