refactor(rename)!: llmsproxy → modelrouter(module / 二进制 / 路径 / env)

产品名早已是 ModelRouter,但仓库标识符仍沿用旧名。这次统一,读写两侧都做兼容,
已部署的机器不会因为顺序问题立刻失效。

- go module `llmsproxy` → `modelrouter`;全仓 import 与 `cmd/modelrouter` 同步。
- `Makefile BINARY=modelrouter`;发行包名、systemd unit、/usr/share 资源、
  `/usr/local/bin` 目标全部改为 modelrouter。
- 默认配置目录 `/etc/llmsproxy` → `/etc/modelrouter`。**读取侧回退**:插件与
  `storeDir` 仍会检查旧路径,所以"先换二进制、后改 unit"不会立即失效。
- 环境变量 `LLMSPROXY_*`(PLUGIN_DIR / XDISPLAY_BASE / ARCH / LIVE_ZEN /
  BUILD_DIR)改为 `MODELROUTER_*`;`LLMS_PROXY_MASTER_KEY` →
  `MODELROUTER_MASTER_KEY`。旧名在读取侧继续接受。
- 桌面端内嵌核心 `bin/llmsproxy` → `bin/modelrouter`,并保留旧文件名回退,
  避免升级到一半的 profile 被判成"core binary missing"。
- 新增 `packaging/migrate-to-modelrouter.sh`:**默认 dry-run**,复制而非移动、
  旧目录永不删除、支持 `--rollback`;随 release tar 分发并由 core-dist 校验。
- `deploy.sh` 增加改名保护:发现旧 `/etc/llmsproxy` 而新配置不存在时**中止**,
  否则会部署出一个"能启动但没有 key、没有源"的空实例。
- 临时目录前缀 `/tmp/llmsproxy-*` → `/tmp/modelrouter-*`;清理逻辑同时回收两代。

兼容别名(`__llmsproxy_*` 全局、`llmsproxy` SDK 别名、旧路径/旧 env)刻意保留,
它们是升级路径而非遗留;docs/plugins.md 已标注为废弃。

本提交未包含:`adapters/opencodezen.lua`、`adapter_opencodezen_test.go`、
`adapter_opencodezen_live_test.go`(OpenCode UA/免费池的既有改动,另批提交)。
This commit is contained in:
JianFeeeee
2026-10-26 22:28:08 +08:00
parent de7553eaa2
commit 688dce88d1
107 changed files with 548 additions and 308 deletions

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// callback 平台的授权 URL 是最容易"看起来对、实际不可用"的地方:URL 能拼出来、

View File

@ -9,7 +9,7 @@ import (
"testing"
"time"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 三个 callback 平台的回调路径**各是注册值**,且校验规则不同:

View File

@ -3,7 +3,7 @@ package core
import (
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ 无凭据账号的批量清理:**只删没有凭据的**,有凭据的一律不动。

View File

@ -8,7 +8,7 @@ import (
"sync"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ codearts 的模型目录必须从**两个**签名端点拉取并合并。

View File

@ -8,7 +8,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// CodeArts 端到端:起真实回调服务器 → 模上游回 code → 走真实 STS 交换

View File

@ -4,7 +4,7 @@ import (
"fmt"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// CodeArts 的 portal 拒绝 <10000 的回调端口(参考 listenOnCallbackPort 的

View File

@ -7,7 +7,7 @@ import (
"net/http/httptest"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 有道每日活动领取是**三步流程**,参考注释点名:无可用活动是**正常业务状态**,

View File

@ -2,7 +2,7 @@ package core
import (
"encoding/json"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
"net/http"
"net/http/httptest"
"testing"

View File

@ -10,7 +10,7 @@ import (
"testing"
"time"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 有道登录闭环的判据:portal 会把授权重定向回 **本机 loopback 接收器**

View File

@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// loomyHarness:Loomy 微信扫码长轮询的测试台。

View File

@ -8,7 +8,7 @@ import (
"sync"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 远端模型目录的判据:**远端为主 + 兜底补位**(照参考 reconcileWithFallback)。

View File

@ -9,7 +9,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// channelsMoreTest:新引入的三条真实流程 + 两个边界流的行为对齐测试。

View File

@ -5,12 +5,12 @@ import (
"path/filepath"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ 插件加载时必须清掉上一进程留下的孤儿回调文件。
//
// 真实事故:生产 `/etc/llmsproxy/plugins/` 攒了 **18 个 callback-*.port**
// 真实事故:生产 `/etc/modelrouter/plugins/` 攒了 **18 个 callback-*.port**
// (对应的 .json 早已消失)。它们永远不会被清,因为:
//
// · loopbacks 追踪表只活在内存(plugin.state.loopbacks);
@ -25,7 +25,7 @@ func TestPluginLoadSweepsOrphanCallbackFiles(t *testing.T) {
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir)
t.Setenv("MODELROUTER_PLUGIN_DIR", pluginDir)
// 摆上一轮的残留(模拟上一进程被强杀后的现场)
orphans := []string{

View File

@ -4,7 +4,7 @@ import (
"encoding/json"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// TestChannelsPluginEndToEnd 是「同类插件」的完整验收:

View File

@ -9,7 +9,7 @@ import (
"testing"
"time"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ 「为什么没有可用账号」必须区分**真的用尽**与**查不到**。

View File

@ -4,7 +4,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
"fmt"
"net/http"

View File

@ -4,7 +4,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// qoderReason 调真实的 transform_error 钩子(不自己重算)。

View File

@ -9,7 +9,7 @@ import (
"testing"
"time"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// Qoder 活动领取有三条参考用**抓包 + 用户报障**定位过的「会误报」陷阱,

View File

@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// qoderHarness:Qoder PKCE 设备码轮的测试台。

View File

@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// raccoonHarness:Raccoon 内置二维码扫码登录的测试台。

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ 删除账号必须**真的把源撤掉** —— 且撤不掉时不能静默。
@ -190,8 +190,14 @@ func TestChannelsDeclaresEveryKernelActionItCalls(t *testing.T) {
// ② 抽出实际调用的动作
called := map[string]bool{}
for _, m := range regexp.MustCompile(`__llmsproxy_kernel\.call\(\s*"([a-z_]+)"`).FindAllStringSubmatch(s, -1) {
called[m[1]] = true
// 插件只允许走公开 SDK(modelrouter.kernel);旧名 __llmsproxy_kernel 是
// 兼容别名,新代码用它就说明插件又在依赖内核私有全局了。
for _, m := range regexp.MustCompile(`\b(?:modelrouter|__llmsproxy_kernel)\.kernel\.call\(\s*"([a-z_]+)"|\b__llmsproxy_kernel\.call\(\s*"([a-z_]+)"`).FindAllStringSubmatch(s, -1) {
name := m[1]
if name == "" {
name = m[2] // 旧名的分支
}
called[name] = true
}
if len(called) == 0 {
t.Fatal("没扫描到任何内核调用(判据失效)")

View File

@ -3,7 +3,7 @@ package core
import (
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ account_reorder 必须**真的改变内核里的源顺序**。

View File

@ -33,8 +33,8 @@ func TestAccountPoolStoreIsAtomicAndConsistent(t *testing.T) {
storePath := filepath.Join(dir, ".channels-store.json")
t.Logf("插件存储目录: %s", dir)
// ⚠️ 必须在**测试目录**里,绝不能是生产路径
if _, err := os.Stat("/etc/llmsproxy/plugins/.channels-store.json"); err == nil {
if filepath.Clean(dir) == "/etc/llmsproxy/plugins" {
if _, err := os.Stat("/etc/modelrouter/plugins/.channels-store.json"); err == nil {
if filepath.Clean(dir) == "/etc/modelrouter/plugins" {
t.Fatal("测试正在使用生产存储目录 —— 隔离失效(这会造成真实数据事故)")
}
}

View File

@ -7,7 +7,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// TestChannelsStoreIsPluginOwned 是「插件自持持久化」的回归。
@ -23,7 +23,7 @@ func TestChannelsStoreIsPluginOwned(t *testing.T) {
// 固定插件目录:两次「重启」必须共用同一目录,否则测不出持久化。
// 这也是防止测试写进生产插件目录的那道隔离(kernelTestCore 会 respect 它)。
dir := t.TempDir()
t.Setenv("LLMSPROXY_PLUGIN_DIR", dir)
t.Setenv("MODELROUTER_PLUGIN_DIR", dir)
load := func() *lua.Plugins {
c := kernelTestCore(t)
ps := c.Plugins()
@ -128,6 +128,10 @@ func TestChannelsMigratesLegacyKernelState(t *testing.T) {
c := kernelTestCore(t)
dir = c.Config().PluginDir
// 网关不再自动创建 plugin_dir(插件不由内核安装),测试自己建。
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
legacyPath := filepath.Join(dir, ".channels.state.json")
if err := os.WriteFile(legacyPath, b, 0o600); err != nil {
t.Fatal(err)
@ -179,7 +183,7 @@ func TestChannelsMigrationKeepsLegacyOnWriteFailure(t *testing.T) {
// (<dir>/.channels-store.json.tmp)**占成一个目录**,io.open(tmp,"w") 必失败。
// 不用 chmod:测试以 root 跑,root 绕过权限位,chmod 拦不住。
sub := t.TempDir()
t.Setenv("LLMSPROXY_PLUGIN_DIR", sub)
t.Setenv("MODELROUTER_PLUGIN_DIR", sub)
legacy := map[string]interface{}{
"version": 1,

View File

@ -11,8 +11,8 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/config"
"llmsproxy/internal/lua"
"modelrouter/internal/config"
"modelrouter/internal/lua"
)
// mustBox 构造一个真 SecretBox(与运行期同一实现,密钥落 runtime dir)。

View File

@ -9,7 +9,7 @@ import (
"sync"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ TRAE 的 batch 模型目录解析:两个**真实缺陷**必须在移植时一起带上

View File

@ -4,7 +4,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ TRAE 的**推理请求**必须用 Cloud-IDE-JWT 头族,不是 Bearer。

View File

@ -6,7 +6,7 @@ import (
"net/http/httptest"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// Trae 签到是最容易被"看起来对"骗过的地方:请求能发出去、上游可能也返回 200,

View File

@ -6,7 +6,7 @@ import (
"os"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 手工移植的确定性派生必须与参考**逐字节一致**,否则签名的设备身份是错的

View File

@ -6,7 +6,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ TRAE ExchangeToken 的凭据在 **Result 里**,不在顶层。

View File

@ -9,7 +9,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ TRAE 回调只带 code(PKCE 形态)时必须**明确报出**「暂不支持 PKCE」,

View File

@ -10,7 +10,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// traeHarness:TRAE callback 流的测试台。假上游模拟 ExchangeToken;

View File

@ -5,7 +5,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// TestChannelsVncStackEndToEnd 验证 VNC 兜底链路在**真插件**上跑通:

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// 显示号泄漏是「测试单独跑绿、全量跑红」的根源,值得一条判据钉住它。
@ -34,7 +34,7 @@ func TestXDisplaySelectionReclaimsStaleLocks(t *testing.T) {
// (见 kernelTestCore);硬编码 90 段会让本判据造出**根本不会被扫描到**的
// 陈旧锁,于是"没复用"这个断言永远失败 —— 那是判据与被测对象脱节。
base := 300
if v := os.Getenv("LLMSPROXY_XDISPLAY_BASE"); v != "" {
if v := os.Getenv("MODELROUTER_XDISPLAY_BASE"); v != "" {
if n, err := strconv.Atoi(v); err == nil && n > 0 {
base = n
}

View File

@ -7,7 +7,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// ★ ZCode 的身份块是 3012 准入的**唯一开关**。
@ -269,7 +269,7 @@ func TestZcodeAdapterFailsLoudlyWithoutAnthropicBase(t *testing.T) {
if err := os.MkdirAll(pluginDir, 0755); err != nil {
t.Fatal(err)
}
t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir)
t.Setenv("MODELROUTER_PLUGIN_DIR", pluginDir)
c := kernelTestCore(t)
ps := c.Plugins()

View File

@ -10,7 +10,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// zcodeHarness:ZCode 两段式服务端中介授权流的测试台。

View File

@ -5,7 +5,7 @@ import (
"sync"
"testing"
"llmsproxy/internal/config"
"modelrouter/internal/config"
)
// newTestConfig returns an in-memory config backed by temp files so

View File

@ -8,7 +8,7 @@ import (
"sync/atomic"
"testing"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// readBundledChannels 读内置 channels 插件源(供行为/变异测试共用)。

View File

@ -6,8 +6,8 @@ import (
"fmt"
"strings"
"llmsproxy/internal/config"
"llmsproxy/internal/lua"
"modelrouter/internal/config"
"modelrouter/internal/lua"
)
// 内核动作实现(R4-c)

View File

@ -6,8 +6,8 @@ import (
"path/filepath"
"testing"
"llmsproxy/internal/config"
"llmsproxy/internal/lua"
"modelrouter/internal/config"
"modelrouter/internal/lua"
)
// 本文件验证 R4-c/R4-d 的闭环:**插件自己驱动外部源的链接认证,然后把源接进内核**。
@ -22,12 +22,12 @@ func kernelTestCore(t *testing.T) *Core {
dir := t.TempDir()
pluginDir := filepath.Join(dir, "plugins")
// 插件的自有存储位置由插件自己决定(读 config.yaml 的 plugin_dir,或
// LLMSPROXY_PLUGIN_DIR 覆盖)。测试**必须**指向临时目录 —— 否则插件会读到
// /etc/llmsproxy/config.yaml 并写进**生产插件目录**(真实事故:一次全量测试
// MODELROUTER_PLUGIN_DIR 覆盖)。测试**必须**指向临时目录 —— 否则插件会读到
// /etc/modelrouter/config.yaml 并写进**生产插件目录**(真实事故:一次全量测试
// 把 26 个测试账号写进了生产 .channels-store.json)。
// 调用方已显式设定时尊重它:多次「重启」要共用同一目录才能测持久化。
if os.Getenv("LLMSPROXY_PLUGIN_DIR") == "" {
t.Setenv("LLMSPROXY_PLUGIN_DIR", pluginDir)
if os.Getenv("MODELROUTER_PLUGIN_DIR") == "" {
t.Setenv("MODELROUTER_PLUGIN_DIR", pluginDir)
}
// ★ 显示器号与端口用**高段号**,避开生产。
//
@ -36,8 +36,8 @@ func kernelTestCore(t *testing.T) *Core {
// 并去抢,结果 `Xvfb 未就绪(/tmp/.X11-unix/X91 未出现)`,而生产上正跑着
// 一套浏览器栈。判据本身没错,错的是**两个 /tmp 互不可见**而号段重叠。
// 故测试整体挪到 300+。
if os.Getenv("LLMSPROXY_XDISPLAY_BASE") == "" {
t.Setenv("LLMSPROXY_XDISPLAY_BASE", "300")
if os.Getenv("MODELROUTER_XDISPLAY_BASE") == "" {
t.Setenv("MODELROUTER_XDISPLAY_BASE", "300")
}
cfg := &config.Config{
Path: filepath.Join(dir, "config.yaml"),

View File

@ -4,7 +4,7 @@ import (
"testing"
"time"
"llmsproxy/internal/lua"
"modelrouter/internal/lua"
)
// TestNoKernelActionDeadlocksFromLua 对**每个**内核动作做同一条断言:

View File

@ -7,7 +7,7 @@ import (
"testing"
"time"
"llmsproxy/internal/config"
"modelrouter/internal/config"
)
// AutoChainFor sits on the request hot path and reads keyAutoChains with no

View File

@ -4,8 +4,8 @@ import (
"path/filepath"
"testing"
"llmsproxy/internal/config"
"llmsproxy/internal/scheduler"
"modelrouter/internal/config"
"modelrouter/internal/scheduler"
)
// Per-key AUTO chains let an admin give one user their own scheduling chain

View File

@ -4,7 +4,7 @@ import (
"net/url"
"testing"
"llmsproxy/internal/config"
"modelrouter/internal/config"
)
// The plugin outbound allowlist is built from the hosts of configured sources.

View File

@ -6,7 +6,7 @@ import (
"strings"
"testing"
"llmsproxy/internal/config"
"modelrouter/internal/config"
)
// Startup must be idempotent, and every credential consumer must see PLAINTEXT.