Repository navigation
feat(vpc): 企业 VPC 从进程级环境变量下沉到账号级 - #152
zjzhang-cn wants to merge 10 commits into
Conversation
企业 VPC 此前只能靠 QD_CN_VPC_ENDPOINT 配置:一个网关只能接一个实例, 同机所有 CN 账号被迫共用同一个域,且换实例要重启进程。 改动: - 账号新增 vpcEndpoint(实例名或实例域),随账号 JSON 落盘;老账号没有该 字段时从 domain 反推(acme.vpc.qoder.com.cn -> acme),升级无需手工迁移 - Account.realm_config() 统一解析 openapi/gateway/website/domain,账号内 12 处取值收敛到这里,避免以后又有人绕过 VPC 直接读区域默认值 - 设备授权 start/poll 与 PAT 导入全程绑定同一实例(发码与兑换不能跨实例) - 推理与模型发现改用选中账号的实例域。此前固定走区域默认域,VPC 账号启动 即 "model discovery failed ... 403",推理报 "105 Login expired" —— 凭证 其实是好的,只是被发到了公网入口 - SSRF 白名单改为按账号登记的三个主机;其他 vpc.qoder.com.cn 域名仍按公网 规则校验(解析到内网照样拒绝) - QD_CN_VPC_ENDPOINT 不再读取,设置了只打印一次告警(静默忽略会让人以为 它还生效);非法旧值也不再让进程起不来 测试:新增 tests/_test_vpc_account.py(47 checks),重写 _test_vpc_endpoint.py 改为验证"变量已失效 + domain 反推 + 白名单边界"(45 checks);issue144 套件 的账号桩从 object() 换成真实 Account(新接口需要)。
- 「添加 Qoder 账号」弹窗在选中「国内版」后出现 VPC 输入框(国际版隐藏 且不随请求发送;接口侧同样 400 拒绝,不只靠前端) - 账号列表「版本」列新增 VPC 徽标,悬浮显示实例名 - 每行操作新增 VPC 按钮(仅国内版),可随时改实例;留空即回公网 - 弹窗切换版本、重开弹窗时同步输入框可见性,避免上次的残留值被带到 国际版登录请求里
- accounts.md:企业 VPC 改为「账号上配」,给出三种设置方式(弹窗、行内 按钮、/accounts/set 或账号 JSON),并补充国际版拒绝、设备授权绑定同一 实例、SSRF 只放行该账号三域等行为 - 补一段升级说明:变量不再读取,但无需手工迁移(domain 已记录实例) - configuration.md:删掉环境变量表格行,出口说明改为指向账号级 VPC - docker-compose.yml:去掉注释掉的 QD_CN_VPC_ENDPOINT,注明改用法
openapi_candidates() 是区域级的,账号级 VPC 落地后它仍然把公网 openapi 排在候选首位:企业账号的额度 / 签到探测会先把凭证发给公网入口(必然 401/404),再回退——既泄漏了凭证,也让 v1.9.6 的「不回退公网」保证失效。 - openapi_candidates(realm, cfg=None) 接受账号自己的 realm 配置 - Account 内部调用传 self.realm_config(),VPC 账号只返回实例 openapi - 无 VPC 时行为与之前完全一致
v1.9.6 的隔离保证建立在全局 vpc_configured() 上;账号级 VPC 之后这些 判断永远为假,等于静默失效: - _fetch_credits_via_source 会用公网遗留的额度来源缓存出站(把企业凭证 发到公网入口),改用 self.realm_config() 判断 - _other_realm_quota_hint 的跨区探测开关同样改为按账号判断:VPC 账号关闭, 非 VPC 账号保留原有诊断能力 顺带把 tests/_test_vpc_endpoint.py 重写为账号级语义:保留 v1.9.6 的解析 加固(55 字符 label、空端口/空 query)与 fake-IP、IPV6 映射判定,环境变量 相关的断言改为「已失效」,隔离用例改为按账号验证。
shuishuipingan
left a comment
There was a problem hiding this comment.
审查结论:方向有价值,但当前不合并(Request changes)
审查 head:b2965608a9707b9624530b7f606dab0825e58a57。该 PR 仍为 draft,正文也明确要求维护者先判断方向。账号级多实例支持值得做,但需要保留 v1.9.6 的部署兼容及实例隔离,当前有以下阻断问题。
P1:重定向允许跨已登记实例转发 Authorization
qoder_accounts.py:1795–1803:源和目标仅检查全局 is_configured_vpc_host,未检查是否同一实例。注册 fixture-a / fixture-b 两个账号后,从 fixture-a-openapi 的 302 跳到 fixture-b-openapi,redirect_request 返回新 Request,且 Authorization 保留。多实例恰是此 PR 的目标,因此这一场景会使 v1.9.6 的“本实例隔离”失效。请以原请求绑定的实例三个主机作为 redirect allowlist,不能使用所有账号主机的并集;补同时配置 A/B 后 A→B 拒绝、A→A 官网允许的行为测试。模型发现目前仍在 qoder_proxy.py:3166 使用 urlopen,也需纳入同一隔离出口。
P1:非法 VPC 配置静默回公网,PAT 可在验证前出站
qoder_accounts.py:328–337 / 1348–1359:解析失败返回 None,realm_config_for 将其当成未配置。Account(vpcEndpoint="bad value").realm_config()["gateway"] 为公网 gateway。import_pat("pt-fixture", realm="cn", vpc="bad value") 则调用公网 /api/v1/jobToken/exchange;使用 http_json mock 在网络层已复现,不需要实际发请求。start_login 同样接收任意 vpc 并回退。请区分“未设置”与“非法非空值”,API 返回 400,文件加载可隔离该坏账号但不能把企业凭证投向公网;intl+vpc 的 PAT 入口也应与 OAuth 一致拒绝。现有测试把“非法值回退公网”当 PASS,需改成 fail-closed。
P1:导入白名单丢失 vpcEndpoint
qoder_accounts.py:5329–5381:normalise_import_row 未保存 vpcEndpoint。仅带 realm=cn/vpcEndpoint=fixture-a/accessToken 的文档经规整后字段丢失、domain 被补成公网,Account 导入后走公网 gateway。显式覆盖与 domain 不一致的导出文件也会被错误反推到旧实例。请校验、规范化并持久化该字段,覆盖导入预览、实际入库、导出再导入的端点一致性测试。
P2:移除 QD_CN_VPC_ENDPOINT 不是完整兼容迁移
qoder_accounts.py:310–319 与 qoder_proxy.py:14443–14452 只告警并忽略旧变量。尚未创建账号的现有容器、新增账号的 OAuth/PAT、没有写实例 domain 的旧账号会改变行为,不能仅凭已落盘 domain 宣称无需迁移。建议保留变量作为默认值;账号字段缺省继承,显式实例覆盖,显式空值选公网,并记录覆盖语义。发布前需覆盖空账号目录升级和新增登录。
P2:VPC CLI 扫描路径丢失
qoder_accounts.py:5109 调用 cli_auth_dirs(realm) 不传实例;新 realm 默认始终公网,因此不会枚举 ~/.qoder-cn/.vpc//.auth,与 docs/accounts.md:159 的承诺不一致。导入也未从实例路径绑定账号配置。请仅在指定/已配置实例范围内扫描并保留来源实例,补临时目录夹具测试。
验证证据
- 原分支全量本地测试:143 suites / 3962 checks,0 failed。
- PR 三平台 CI 为绿色,但上述错误行为未覆盖。
- 补充只用 fixture-token、mock http_json 和 Request 对象的离线探针:4 个关键判据全部复现失败(跨实例跳转、非法账号回公网、导入丢字段、非法 PAT 发公网)。没有访问任何企业实例。
保留 draft,修复上述问题并补回归后再评审合并;本轮不创建新版本/tag/Release,也不触发 Packages 发布。
按 review 修复账号级 VPC 的隔离与兼容问题: - 非法 vpcEndpoint 不再静默回退公网:账号加载保留但标记 vpcError, 推理/签到/额度刷新/代表账号全部排除,realm_config() 抛错; API 入口返回 400,intl+vpc 的 PAT 与 OAuth 一致拒绝。 - 重定向 allowlist 改为按源请求所属实例的三个主机,A->B/公网/HTTP/ 非默认端口全部拒绝;模型发现改用同一隔离 opener。 - 导入/预览/导出统一规范化并持久化 vpcEndpoint;domain 与实例冲突 时整行拒绝;import_pat 同样规范化。 - QD_CN_VPC_ENDPOINT 恢复为默认值:账号缺省继承、显式实例覆盖、 显式空串走公网;继承账号保存时不写 vpcEndpoint 字段。 - CLI 扫描按环境默认/已配置/显式实例枚举 .vpc/<instance>/.auth, 导入时保留来源实例。 - 账号级 website 与错误提示改用 account.realm_config(),避免绕过 VPC。 - 补 VPC 专项与回归测试;全量 143 suites / 3994 checks 通过。
- OAuth/PAT 弹窗补企业 VPC 输入:留空继承默认实例,勾选强制公网才发送显式空串。\n- 账号行 VPC 徽标显示来源(账号配置/环境默认/domain 迁移),配置错误单独提示。\n- 更新账号页静态回归。
- 账号 vpcEndpoint 三态(缺省继承/显式空/显式实例)与非法值 fail-closed。\n- QD_CN_VPC_ENDPOINT 恢复为默认值,账号配置覆盖;升级无需手工迁移。\n- OAuth/PAT 控件、扫描范围、导入冲突处理和重定向隔离说明。
重新登录同一 uid 但切换区域时,不能把旧区域的 vpcEndpoint 带到 intl 账号。
realm 启动日志按 realm_config_for 解析,设置 QD_CN_VPC_ENDPOINT 时显示默认实例域,而不是仍显示公网域。
|
账号级多实例 VPC 的方向有价值。根据维护者决定,本 PR 关闭、不合并;后续由维护者基于当前主分支独立实现,保留 QD_CN_VPC_ENDPOINT 默认值兼容,并覆盖实例级重定向隔离、非法配置 fail-closed、导入往返和 CLI 实例扫描。感谢提出场景与方案。 |
|
按维护者决定已关闭且未合并。本次从主分支独立实现已落地 0b0885f,版本 v1.9.7:保留环境默认值、账号三态覆盖;同实例重定向隔离、非法配置出站前拒绝、导入往返、配置实例扫描、冻结 OAuth/PAT、账号快照及旧请求回写保护均有行为回归。本地 144 套件 / 4024 checks 通过,新增后端79项、前端39项。正式发布 CI 与镜像核验进行中。感谢提出多实例场景。 |
|
独立实现已完成发布 v1.9.7:https://github.com/shuishuipingan/qoder2api-hub/releases/tag/v1.9.7 。本 PR 维持 CLOSED、未合并。正式 tag 的三平台测试与版本一致性全绿(42983998531),Docker 冒烟和双架构发布全绿(42983998648)。已直接核验 GHCR 1.9.7 的 linux/amd64 与 linux/arm64,manifest digest sha256:d15601706e8ed888c027713ba802de1752e53bd06b5e4b79c997a36f7840fb66 。 |

与 #150 的关系(请先看这段)
这个 PR 基于刚发布的 v1.9.6(含 #150 的
QD_CN_VPC_ENDPOINT实现),把 VPC 配置从进程级环境变量改为账号级,因此会移除 #150 引入的环境变量。这是一次有意的方向调整,
不是对 #150 的修补,所以标为 draft,等维护者判断要不要走这条路。
如果更希望保持向后兼容,我可以改一版保留
QD_CN_VPC_ENDPOINT作为默认值、账号级vpcEndpoint只做覆盖的版本——那样不破坏任何现有部署。动机
#150 之后仍有两个实际问题:
REALM_CONFIGS,但gateway_candidates()与该路径在部分调用点仍取区域默认值,表现为启动即
model discovery failed ... 403、聊天报upstream 403 {"code":"105","message":"Login expired"}——凭证其实是好的,只是被发到了公网入口。
改动
账号级 VPC(qoder_accounts.py)
vpcEndpoint(实例名或实例域),随账号 JSON 落盘domain反推(acme.vpc.qoder.com.cn->acme),v1.9.6 创建的账号无需手工迁移
Account.realm_config()统一解析 openapi / gateway / website / domain,账号内12 处取值收敛到这里
HTTPS 默认端口、fake-IP 与 IPv6 映射判定
按账号收敛的三处隔离(v1.9.6 里依赖全局
vpc_configured(),账号级之后会静默失效)openapi_candidates(realm, cfg)接受账号配置,VPC 账号只返回实例 openapi_fetch_credits_via_source不再使用公网遗留的额度来源缓存_other_realm_quota_hint按账号关闭跨区探测(非 VPC 账号保留原能力)路由与接口(qoder_proxy.py)
/accounts/login/start接受vpc,intl + vpc直接 400/accounts/set支持修改vpcEndpoint面板(dashboard.html)
移除
QD_CN_VPC_ENDPOINT验证
tests/_test_vpc_account.py(47 checks)tests/_test_vpc_endpoint.py(70 checks):保留 feat(vpc): 支持国内版企业 VPC 专属域(QD_CN_VPC_ENDPOINT) #150 的解析加固与 fake-IP /IPv6 判定,环境变量断言改为「已失效」,隔离用例改为按账号验证
scripts/check_clean_checkout.py通过兼容性
QD_CN_VPC_ENDPOINT不再生效,但不需要手工迁移:v1.9.6 的账号落盘时domain已经写成实例域,代码会自动识别。已设置该变量的部署会看到一条 WARN。