feat(lua): 插件 Lua 侧适配器桥接(R4 Lua 绑定)+ 真二进制适配器接手 e2e

补上两个此前报告为"缺口"的东西。其中第一个的旧结论是**错的**,这里更正。

## R4 Lua 绑定:__llmsproxy_adapters

用户问的"为什么 Lua 绑不上"—— 结论是**能绑,之前的归因错了**。

旧结论是「NewTable + PushGoClosure + SetField 是 golua 雷区,会 SIGSEGV」。
反证:内核自己的 buildJSONTable 用的就是这同一个模式,长期稳定。真正的变量
是当时 Phase 4 的**双 VM 半成品**(钩子一个 VM、定时任务一个私有 VM),
bootPluginState 签名与调用点不一致,我改到一半编译不过就仓促回滚,留下的
中间态让 billing 一跑就崩。把双 VM 改回单 VM 之后,同一模式一次就通。

顺带回答一个备选方案(用户提出:Lua 只投递请求、Go 串行执行):不需要。
p.mu 已经天然串行化同一插件的所有入口;再加队列只会引入"谁排空/超时后积压
怎么算"的新问题。而且桥接调用本身是微秒级的(交给 Go 重建一个小池),真正
长耗时的是插件自己去取 token 的 HTTP 请求,那跟桥接无关。

能力(Go 侧强制,非约定):
- install(name, source) → (生效名, nil) | (nil, err)
- update(name, source) → (true, nil) | (nil, err)  只限自己装的
- remove(name)         → (true, nil) | (nil, err)  只限自己装的
- 调用身份是插件**启动时的固定名**,不从 Lua 状态现读 ⇒ 无法冒充别的插件
- 不能影子内置(Lua 侧同样被拒,不是绕过 Go 校验的后门)
- 出错返回 (nil, err) 而非抛异常,与钩子/HTTP 处理器同一套约定

**分片 VM 故意不给**:分片是文档化的「只读、可丢弃」工作单元,而适配器
install 是**全局**副作用(改共享注册表)。给分片等于让"只读"的按需调用悄悄
改写所有请求共用的适配器 —— 正是分片契约要防的不可见变更。

测试 6 个:install/rotate/remove 从 Lua 调用、影子内置被拒、**定时任务里
轮换 token 且请求钩子立即可见**(R4 闭环)、分片无桥接。

## 真二进制 e2e:适配器接手对外服务

用户问到的第二个点。此前 /v1/messages 的 Anthropic 接管只在**进程内**
gateway 层测过(internal/gateway/messages_test.go),绕过了只会在真实进程里
发生的事:适配器磁盘 seed、配置加载、真 HTTP 栈。

新增 e2e/adapter_takeover_test.go,打**真编译的二进制** + 只说 Anthropic 的
假上游(该上游对不符形状的请求主动报错,不做宽容接受):
- 非流式:出向必须是 Anthropic message 形状,不能有 OpenAI choices 外壳
- 流式:必须是 message_start…message_stop 序列,**不得**出现 [DONE]
- 压力:96 并发(48 流式)混合打真二进制,全部 200,无 5xx,无形状串味

压力测试初版把并发上限设成 4 于是大量 429,我把断言改正为:429 是内核**正确**
的容量背压,不算失败;失败的定义是 5xx 或 200 但内容/形状错。现在 96/96 通过。
This commit is contained in:
JianFeeeee
2026-10-11 13:25:11 +08:00
parent 66786bb0e3
commit dafc3b3fc3
6 changed files with 640 additions and 7 deletions

View File

@ -27,6 +27,8 @@ import (
"fmt"
"sort"
"sync"
golua "github.com/aarzilli/golua/lua"
)
// AdapterOwner 标识一个适配器由谁安装。
@ -185,3 +187,76 @@ func (ps *Plugins) PluginAdapters(plugin string) []string {
func (ps *Plugins) AdapterOwnerOf(name string) string {
return ps.adapterOwner().ownedBy(name)
}
// ---- Lua 绑定(R4:插件在运行时改自己的适配器)----
// adapterBridgeGlobal 暴露给插件 Lua 的适配器管理表。
//
// 用一张表而不是三个顶层全局,是为了少占插件可见的全局命名空间。表用
// buildJSONTable 的同一套写法(NewTable + PushGoClosure + SetField),
// 该写法在 json 表上已被长期验证。
const adapterBridgeGlobal = "__llmsproxy_adapters"
// registerAdapterBridge 把适配器管理表装进插件的 Lua 状态。
//
// name 是**该插件自己的启动名**,随每次调用一起交给 Go 侧桥接 —— 归属校验
// 因此来自启动时的固定身份,而不是从 Lua 状态里现读(那样一个分片或任何持有
// 已编译脚本的调用者就能冒充别的插件)。
func registerAdapterBridge(L *golua.State, ps *Plugins, name string) {
L.NewTable()
install := luaInstallAdapter(L, ps, name)
L.PushGoClosure(install)
L.SetField(-2, "install")
update := luaUpdateAdapter(L, ps, name)
L.PushGoClosure(update)
L.SetField(-2, "update")
remove := luaRemoveAdapter(L, ps, name)
L.PushGoClosure(remove)
L.SetField(-2, "remove")
L.SetGlobal(adapterBridgeGlobal)
}
func luaInstallAdapter(L *golua.State, ps *Plugins, name string) golua.LuaGoFunction {
return func(L *golua.State) int {
adapterName := L.ToString(1)
code := L.ToString(2)
effective, err := ps.InstallAdapter(name, adapterName, code)
if err != nil {
L.PushNil()
L.PushString(err.Error())
return 2
}
L.PushString(effective)
L.PushNil()
return 2
}
}
func luaUpdateAdapter(L *golua.State, ps *Plugins, name string) golua.LuaGoFunction {
return func(L *golua.State) int {
adapterName := L.ToString(1)
code := L.ToString(2)
if err := ps.UpdateAdapter(name, adapterName, code); err != nil {
L.PushNil()
L.PushString(err.Error())
return 2
}
L.PushBoolean(true)
L.PushNil()
return 2
}
}
func luaRemoveAdapter(L *golua.State, ps *Plugins, name string) golua.LuaGoFunction {
return func(L *golua.State) int {
adapterName := L.ToString(1)
if err := ps.RemoveAdapter(name, adapterName); err != nil {
L.PushNil()
L.PushString(err.Error())
return 2
}
L.PushBoolean(true)
L.PushNil()
return 2
}
}