mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-09 18:57:21 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 6bec260513 |
47
deploy.sh
47
deploy.sh
@ -393,13 +393,17 @@ sync_adapters() {
|
|||||||
|
|
||||||
# 同步内置插件到 plugin_dir。
|
# 同步内置插件到 plugin_dir。
|
||||||
#
|
#
|
||||||
# 为什么需要它:新内置插件(如 channels.lua)过去只能靠内核的 SeedBundled 落地,
|
# 内置插件是**内核发行物的一部分**(go:embed 进二进制,随版本演进),与内置适配器
|
||||||
# 而旧版 SeedBundled "目录已存在就什么都不做",于是升级后新插件永远不会出现在
|
# 同性质。因此这里的纪律与 sync_adapters 完全一致:**覆盖内置同名文件**(带备份),
|
||||||
# 生产 —— 它在二进制里、却不在 /api/plugins。内核端已修(改为按文件补齐),
|
# 保留目录里运行时新增/运维自加的 .lua。
|
||||||
# 这里再显式同步一道,让部署与二进制内容对齐、不依赖内核那一侧的行为。
|
|
||||||
#
|
#
|
||||||
# 只补缺:已存在的插件文件一律不动。运维自己改过、或运行时新增的插件必须跨
|
# 早期实现是「只补缺,已存在一律不动」,看着安全,实际造成一个功能性缺口:
|
||||||
# 部署存活;覆盖它们会静默抹掉线上正在用的逻辑。
|
# 内置插件本身的**修复无法下发**。v2.0.3/v2.0.4/v2.0.5 三个渠道修复就这样
|
||||||
|
# 全部卡在仓库里 —— 二进制换了、/etc/llmsproxy/plugins/channels.lua 还是旧版,
|
||||||
|
# 生产行为毫无变化。内置源码的正确性由仓库保证,部署必须与二进制内容对齐。
|
||||||
|
#
|
||||||
|
# 「只补缺」原本想保护的是「运维改过 / 运行时新增的插件」,但那些都是**非内置
|
||||||
|
# 名字**:内置名一律以发行物为准,其余名字一律不动。两者不是一回事。
|
||||||
sync_plugins() {
|
sync_plugins() {
|
||||||
log "同步内置插件"
|
log "同步内置插件"
|
||||||
|
|
||||||
@ -408,24 +412,35 @@ sync_plugins() {
|
|||||||
|
|
||||||
mkdir -p "$TARGET_PLUGINS"
|
mkdir -p "$TARGET_PLUGINS"
|
||||||
|
|
||||||
# 备份将被触及的文件(只会是本次新增的那些)
|
# 备份将被覆盖/新增的内置插件(非内置名字不进入循环,天然不动)
|
||||||
BACKUP_DIR="/etc/llmsproxy/plugins.bak.$(date +%Y%m%d%H%M%S)"
|
BACKUP_DIR="/etc/llmsproxy/plugins.bak.$(date +%Y%m%d%H%M%S)"
|
||||||
local added=0
|
|
||||||
mkdir -p "$BACKUP_DIR"
|
mkdir -p "$BACKUP_DIR"
|
||||||
|
local added=0 updated=0 same=0
|
||||||
for src in "$SRC_PLUGINS"/*.lua; do
|
for src in "$SRC_PLUGINS"/*.lua; do
|
||||||
[[ -f "$src" ]] || continue
|
[[ -f "$src" ]] || continue
|
||||||
local name; name="$(basename "$src")"
|
local name; name="$(basename "$src")"
|
||||||
if [[ -f "$TARGET_PLUGINS/$name" ]]; then
|
if [[ -f "$TARGET_PLUGINS/$name" ]]; then
|
||||||
continue # 已存在:运维版本优先,不覆盖
|
if cmp -s "$src" "$TARGET_PLUGINS/$name"; then
|
||||||
|
((same++)) || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
cp -f "$TARGET_PLUGINS/$name" "$BACKUP_DIR/$name.bak" 2>/dev/null || true
|
||||||
|
cp -f "$src" "$TARGET_PLUGINS/$name"
|
||||||
|
chmod 0644 "$TARGET_PLUGINS/$name"
|
||||||
|
log " ~ 更新内置插件: $name"
|
||||||
|
((updated++)) || true
|
||||||
|
else
|
||||||
|
cp -f "$src" "$TARGET_PLUGINS/$name"
|
||||||
|
chmod 0644 "$TARGET_PLUGINS/$name"
|
||||||
|
log " + 新增内置插件: $name"
|
||||||
|
((added++)) || true
|
||||||
fi
|
fi
|
||||||
cp -f "$src" "$TARGET_PLUGINS/$name"
|
|
||||||
chmod 0644 "$TARGET_PLUGINS/$name"
|
|
||||||
log " + 新增内置插件: $name"
|
|
||||||
((added++)) || true
|
|
||||||
done
|
done
|
||||||
if (( added == 0 )); then
|
# 没有任何改动时不必留空备份目录
|
||||||
log " 无新增内置插件(均已存在)"
|
if (( updated == 0 && added == 0 )); then
|
||||||
rmdir "$BACKUP_DIR" 2>/dev/null || true
|
rmdir "$BACKUP_DIR" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
log " 旧插件已备份到 $BACKUP_DIR"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 校验:内嵌插件目录里的每个 .lua 都必须在目标目录存在
|
# 校验:内嵌插件目录里的每个 .lua 都必须在目标目录存在
|
||||||
@ -434,7 +449,7 @@ sync_plugins() {
|
|||||||
local name; name="$(basename "$src")"
|
local name; name="$(basename "$src")"
|
||||||
[[ -f "$TARGET_PLUGINS/$name" ]] || warn " 缺少插件: $name"
|
[[ -f "$TARGET_PLUGINS/$name" ]] || warn " 缺少插件: $name"
|
||||||
done
|
done
|
||||||
log "✓ 插件已同步到 $TARGET_PLUGINS/(新增 $added,已有文件未改动)"
|
log "✓ 插件已同步到 $TARGET_PLUGINS/(新增 $added,更新 $updated,未变 $same)"
|
||||||
}
|
}
|
||||||
|
|
||||||
# ---------- 重启服务 ----------
|
# ---------- 重启服务 ----------
|
||||||
|
|||||||
@ -368,3 +368,51 @@ func TestBuddyBillingHeadersMatchReference(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBuddyBalanceParseDoubleNested 逐行锁定参考 fetchCreditBalance 的解析口径:
|
||||||
|
// 响应是**双层嵌套** data.Response.Data.Accounts[](与签到端点的单层 data 不同,
|
||||||
|
// 这是该接口最容易错的地方),且只累加**有效包**的 CycleCapacityRemain ——
|
||||||
|
// 失效包服务端仍返回,并进总额会让数字虚高(参考实测 155.67 → 655.67)。
|
||||||
|
func TestBuddyBalanceParseDoubleNested(t *testing.T) {
|
||||||
|
_, ps := poolCore(t)
|
||||||
|
|
||||||
|
// 两个有效包 + 一个失效包(ExpiredTime 在 2020 年)
|
||||||
|
body := `{"code":0,"msg":"OK","data":{"Response":{"Data":{"Accounts":[
|
||||||
|
{"PackageName":"A","Status":1,"CycleCapacityRemain":100.5,"ExpiredTime":"2099-01-01 00:00:00"},
|
||||||
|
{"PackageName":"B","Status":1,"CycleCapacityRemain":55.17000031,"ExpiredTime":"2099-01-01 00:00:00"},
|
||||||
|
{"PackageName":"C","Status":1,"CycleCapacityRemain":500,"ExpiredTime":"2020-01-01 00:00:00"}
|
||||||
|
]}}}}`
|
||||||
|
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
fmt.Fprint(w, body)
|
||||||
|
}))
|
||||||
|
defer ts.Close()
|
||||||
|
|
||||||
|
invoke(t, ps, "set_endpoints", fmt.Sprintf(
|
||||||
|
`{"platform":"buddy","balance":{"url":%q,"method":"POST","body":"{}"}}`, ts.URL))
|
||||||
|
invoke(t, ps, "account_add", `{"platform":"buddy","credential":"BD-AT","nickname":"b"}`)
|
||||||
|
|
||||||
|
j := invoke(t, ps, "credits_balances", `{"platform":"buddy"}`)
|
||||||
|
var row map[string]interface{}
|
||||||
|
for _, a := range asArr(j["accounts"]) {
|
||||||
|
row = a.(map[string]interface{})
|
||||||
|
}
|
||||||
|
bal, _ := row["balance"].(map[string]interface{})
|
||||||
|
if bal == nil {
|
||||||
|
t.Fatalf("应解析出余额, 实得 error=%v", row["error"])
|
||||||
|
}
|
||||||
|
// 100.5 + 55.17 = 155.67(失效包的 500 不计入)
|
||||||
|
if got, _ := bal["total"].(float64); got != 155.67 {
|
||||||
|
t.Fatalf("余额 = %v, 期望 155.67(只累加有效包、两位小数)", bal["total"])
|
||||||
|
}
|
||||||
|
|
||||||
|
// 单层 data(缺 Response 层)必须判为解析失败,不能当 0
|
||||||
|
body = `{"code":0,"data":{"TotalDosage":609}}`
|
||||||
|
j = invoke(t, ps, "credits_balances", `{"platform":"buddy"}`)
|
||||||
|
for _, a := range asArr(j["accounts"]) {
|
||||||
|
row = a.(map[string]interface{})
|
||||||
|
}
|
||||||
|
if row["balance"] != nil {
|
||||||
|
t.Fatalf("双层缺失时应报错而非当 0(参考语义:区分「查不到」与「余额为 0」): %v", row)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -331,12 +331,57 @@ local PLATFORMS = {
|
|||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
-- 余额(对齐参考 USER_RESOURCE_PATH /v2/billing/meter/get-user-resource)
|
-- 余额(对齐参考 USER_RESOURCE_PATH /v2/billing/meter/get-user-resource)
|
||||||
|
--
|
||||||
|
-- ⚠️ 响应是**双层嵌套** data.Response.Data.Accounts[](与签到端点的单层
|
||||||
|
-- data 不同),这是本接口最容易解析错的地方。
|
||||||
|
--
|
||||||
|
-- ⚠️ 刻意**不用**服务端的 TotalDosage:它是**终身口径**且取整(实测同一
|
||||||
|
-- 响应里 TotalDosage=655 而 IDE 显示 155.67)。正确算法是遍历 Accounts[]
|
||||||
|
-- 现算:只累加**有效包**的 CycleCapacityRemain(失效包服务端仍会返回,
|
||||||
|
-- 但不能用于扣费,并进总额会让数字虚高),最后按两位小数规整。
|
||||||
balance = {
|
balance = {
|
||||||
url = "https://copilot.tencent.com/v2/billing/meter/get-user-resource",
|
url = "https://copilot.tencent.com/v2/billing/meter/get-user-resource",
|
||||||
method = "POST", body = "{}",
|
method = "POST", body = "{}",
|
||||||
total_fn = function(rec)
|
total_fn = function(rec)
|
||||||
local d = rec.data or rec
|
local data = rec.data
|
||||||
return tonumber(d.totalCredit or d.total_credit or d.balance or d.amount) or 0
|
if type(data) ~= "table" then return nil end
|
||||||
|
local resp = data.Response
|
||||||
|
if type(resp) ~= "table" then return nil end
|
||||||
|
local inner = resp.Data
|
||||||
|
if type(inner) ~= "table" then return nil end
|
||||||
|
local accounts = inner.Accounts
|
||||||
|
if type(accounts) ~= "table" then return nil end
|
||||||
|
local now = os.time and os.time() or 0
|
||||||
|
local sum = 0
|
||||||
|
for _, pkg in ipairs(accounts) do
|
||||||
|
if type(pkg) == "table" then
|
||||||
|
-- 失效判定:Status 显式已过期,或 ExpiredTime / DeductionEndTime 已过。
|
||||||
|
local active = true
|
||||||
|
local st = pkg.Status
|
||||||
|
if st == 2 or st == "2" or st == "EXPIRED" or st == "expired" then active = false end
|
||||||
|
local function past(t)
|
||||||
|
if type(t) ~= "string" or t == "" then return false end
|
||||||
|
local Y, Mo, D, H, Mi, S = t:match("^(%d%d%d%d)%-(%d%d)%-(%d%d)[T ](%d%d):(%d%d):(%d%d)")
|
||||||
|
if Y then
|
||||||
|
local ts = os.time({ year = tonumber(Y), month = tonumber(Mo), day = tonumber(D),
|
||||||
|
hour = tonumber(H), min = tonumber(Mi), sec = tonumber(S), isdst = false })
|
||||||
|
return ts and now >= ts
|
||||||
|
end
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
if past(pkg.ExpiredTime) then active = false end
|
||||||
|
local ded = tonumber(pkg.DeductionEndTime)
|
||||||
|
if ded and ded > 0 then
|
||||||
|
-- 毫秒时间戳
|
||||||
|
if now >= math.floor(ded / 1000) then active = false end
|
||||||
|
end
|
||||||
|
if active then
|
||||||
|
sum = sum + (tonumber(pkg.CycleCapacityRemain) or 0)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
-- 两位小数规整(服务端精确值带浮点噪声,如 55.67000031)
|
||||||
|
return math.floor(sum * 100 + 0.5) / 100
|
||||||
end,
|
end,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user