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:
JianFeeeee
2026-10-13 13:42:57 +08:00
parent 5ab047e48b
commit 6fe6014d92
33 changed files with 83 additions and 83 deletions

View File

@ -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

View File

@ -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`,入口处强制)才是限额权威。**
本插件不参与任何路由或配额决策。

View File

@ -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)
}
}

View File

@ -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"])
}
})

View File

@ -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. 运维授权后,重新接入应当真的把源注册进内核

View File

@ -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)
}
})
}

View File

@ -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"])
}
})
}

View File

@ -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 {

View File

@ -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)

View File

@ -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)
}
})
}

View File

@ -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)
}
})
}

View File

@ -34,6 +34,6 @@ return plugin
t.Fatalf("动作未成功: %#v", m)
}
if _, ok := c.FindKey("sk-kernel-0001"); ok {
t.Fatal("★ 动作报告成功但副作用没发生 —— dispatch 是空转")
t.Fatal("动作报告成功但副作用没发生 —— dispatch 是空转")
}
}

View File

@ -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 {

View File

@ -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'")
}
}

View File

@ -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",

View File

@ -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")
}

View File

@ -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"])
}
}

View File

@ -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)
}
}

View File

@ -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")
}
}

View File

@ -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 {

View File

@ -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 {

View File

@ -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 未被调用")
}
}

View File

@ -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)

View File

@ -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>";
}

View File

@ -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
-- 会查不到,拿到零值。实测:

View File

@ -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>",
},

View File

@ -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 {

View File

@ -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.

View File

@ -3,7 +3,7 @@
# 这个是打包进发行包、供首次安装使用的模板。它故意不含任何可用的凭据:
# 首次启动时程序会生成一个随机 admin key 并写入实际配置(WebUI「密钥」页可见)。
#
# ⚠ 不要在本文件里填一个写死的具体 key。示例文件是公开的,写进去的任何值
# 注意: 不要在本文件里填一个写死的具体 key。示例文件是公开的,写进去的任何值
# 都会成为一个全网皆知的 admin key(旧版示例里的 sk-gw-local-0001 就踩过
# 这个坑:它被直接抄进生产配置并真实可用)。

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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"