Repository navigation
Conversation
Sub2API 原有插件只能在选号完成之后介入(openai.oauth.outbound_transport.v1), 插件无法影响"用哪个账号"。本 PR 新增调度能力,让插件在宿主完成全部硬门槛 过滤后对合法候选集表达一次偏好。 设计边界(三条,共同决定该能力可安全上线): 1. 插件只提名,不决策。候选集已经过状态/平台/分组/传输能力/隐私/模型能力/ 配额/利润门全部过滤,插件只能从给定集合里挑,不可能引入非法账号。 2. 提名零副作用。只把提名账号移到 selectionOrder 首位,不改变集合。抢并发槽位、 lane 水合、粘性写入、失败重放全部仍是宿主原生逻辑;提名账号抢不到槽时宿主 沿原序列继续尝试,最坏退化为原生排序。 3. 失败一律降级。未启用/超时(200ms)/RPC 错误/进程退出/提名越界 全部 fail-open。 另:跨池不搬运。提名账号若属于订阅池而当前在选常规池,置顶自然失败 —— 刻意为之, 跨池搬运会破坏 SubscriptionPriority 语义。 实现要点: - 调度路由从 runtimes 映射派生,不维护独立状态,因此启用/停用/失败切换/多实例 对齐等既有生命周期逻辑自动生效,不会出现"插件停了但调度还生效"。 - 一次选号只询问插件一次(而非每个候选池各一次),保证候选集完整稳定。 - 决策 Layer 只在提名账号确实被选中时才改写为 plugin_scheduler,否则保持 load_balance —— 抢槽失败后宿主自己选了别人,归因插件会让指标失真。 - 候选只含决策必需字段,不含账号名/凭据/代理/分组。 兼容性: - 未实现该 RPC 的既有插件返回 Unimplemented,宿主静默跳过,转发不受影响。 - 能力白名单从单值校验改为 supportedPluginCapabilities 映射;manifest.schema.json 的 capabilities.id 从 const 改为 enum。 - 调度能力独立于 TransportAPIVersion,不影响既有插件。 测试: - 20 个单元测试覆盖提名语义、跨池边界、归因、灰度、fail-open 各分支。 - 2 个端到端测试用真实 .s2plugin 拉起插件进程,验证 go-plugin 握手、gRPC 帧序、 配置协商、提名语义、出站转发,以及"429 后解除亲和并改提名"的闭环。 - 验收测试断言"宿主读出的值 == 清单声明的值",不写死版本号与插件 ID, 因此对任意版本和任意插件变体都成立。 - 完整 internal/service 包回归通过(208s)。
为方便试用,为本分支构建测试镜像并推送到 GHCR(配了 DockerHub 凭据时也推 Docker Hub)。 标签策略:全部带 test- 前缀,与正式镜像完全隔离 ghcr.io/<owner>/sub2api:test-plugin-scheduling ghcr.io/<owner>/sub2api:test-plugin-scheduling-<sha> 为什么需要它:调度能力是宿主侧改动,而插件包是管理后台上传的(装不进镜像), 所以想试用完整能力必须有一个已编译进调度能力的镜像。手工构建需要 Go + Node 工具链与十几分钟。 文档补充「两个变体怎么选」:原版宿主只能装基础包,测试镜像可装完整包。
CI 的 golangci-lint 报了 4 个问题(其余检查 test/backend-security/frontend 全过): 1. plugin_scheduling.go: sanitizePluginReason 是死代码。 改成"提名归因"方案后调用点已移除,函数忘了删,连带 strings import 一起清理。 2. plugin_scheduling.go: strings.Builder.WriteRune/WriteByte 返回值未检查 (随死代码一并消失)。 3. plugin_scheduling_integration_test.go: bytes.Buffer.Write 返回值未检查。 bytes.Buffer 的 Write 永不返回错误(只可能内存耗尽 panic), 显式 `_, _ =` 丢弃并加注释,避免后来者误以为是漏了错误处理。 本地用与 CI 同版本的 golangci-lint v2.13 验证:`0 issues`。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
背景
Sub2API 原有插件能力只有
openai.oauth.outbound_transport.v1(出站传输),它在宿主选完账号之后才被调用。实测确认(openai_account_scheduler.go、gateway_scheduling.go、lane_sticky.go对pluginManager零引用):选号之前不存在任何插件钩子。本 PR 新增调度能力,让插件在宿主完成全部硬门槛过滤后,对合法候选集表达一次偏好。
配套插件实现见私有仓库
设计边界
1. 插件只提名,不决策。 调用时机在
selectByLoadBalance内部、全部过滤之后:插件拿到的候选已经全部合法,不可能提名被过滤掉的账号。
2. 提名零副作用。 只把提名账号移到
selectionOrder首位,不改变集合。抢槽(Redis 在途计数)、lane 水合、粘性写入、失败重放全部仍是原生逻辑。提名账号抢不到槽时宿主沿原序列继续尝试,最坏退化为原生排序,请求不会失败。3. 失败一律降级。 未启用 / 超时(200ms) / RPC 错误 / 进程退出 / 提名越界,全部 fail-open。
另:跨池不搬运。提名账号若属于订阅池而当前在选常规池,置顶自然失败 —— 刻意为之,跨池搬运会破坏
SubscriptionPriority语义。实现要点
runtimes映射派生,不维护独立状态。启用/停用/失败切换/多实例对齐等既有生命周期逻辑自动生效,不会出现"插件停了但调度还生效"。plugin_scheduler,否则保持load_balance。兼容性
Unimplemented,宿主静默跳过,转发能力不受影响。supportedPluginCapabilities映射;manifest.schema.json的capabilities.id从const改为enum。plugin_bindings.capability是自由字符串,唯一索引已按(capability, platform, account_type)分桶)。测试
go build ./...go vet -tags=unit ./internal/...gofmt -linternal/service回归验收测试断言"宿主读出的值 == 清单声明的值"(
installation.Version == manifest.version、GetInfo.plugin_id == manifest.id),不写死具体版本号与插件 ID —— 这样对任意版本、任意插件变体都成立,失败信息也能直接指向真正的不变量。未运行的项
-tags=integration(需 Docker:真 PG/Redis)文档
新增
backend/pkg/pluginapi/docs/scheduling.md,说明契约、设计边界、观测字段与部署流程。