Skip to content

feat(vpc): 企业 VPC 从进程级环境变量下沉到账号级 - #152

Closed
zjzhang-cn wants to merge 10 commits into
shuishuipingan:mainfrom
zjzhang-cn:feat/account-vpc-v196
Closed

zjzhang-cn wants to merge 10 commits into
shuishuipingan:mainfrom
zjzhang-cn:feat/account-vpc-v196

Conversation

@zjzhang-cn

Copy link
Copy Markdown
Contributor

与 #150 的关系(请先看这段)

这个 PR 基于刚发布的 v1.9.6(含 #150 的 QD_CN_VPC_ENDPOINT 实现),把 VPC 配置从
进程级环境变量改为账号级,因此会移除 #150 引入的环境变量。这是一次有意的方向调整,
不是对 #150 的修补,所以标为 draft,等维护者判断要不要走这条路。

如果更希望保持向后兼容,我可以改一版保留 QD_CN_VPC_ENDPOINT 作为默认值、账号级
vpcEndpoint 只做覆盖
的版本——那样不破坏任何现有部署。

动机

#150 之后仍有两个实际问题:

  1. 一个网关只能接一个实例。同机所有 CN 账号被迫共用同一个域,换实例要重启进程。
  2. VPC 账号的推理与模型发现走的是区域默认域。feat(vpc): 支持国内版企业 VPC 专属域(QD_CN_VPC_ENDPOINT) #150 覆盖了 openapi / gateway 的
    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 处取值收敛到这里
  • 设备授权 start/poll 与 PAT 导入绑定同一实例(发码与兑换不能跨实例)
  • 保留 feat(vpc): 支持国内版企业 VPC 专属域(QD_CN_VPC_ENDPOINT) #150 的全部加固:55 字符实例名上限、空端口/空 query 拒绝、重定向只允许本实例
    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)

  • 添加账号弹窗在「国内版」下出现 VPC 输入框,国际版隐藏且不发送
  • 账号行新增 VPC 徽标与编辑按钮

移除 QD_CN_VPC_ENDPOINT

  • 不再读取;设置了只打印一次 WARN(静默忽略会让人以为还生效)
  • 非法旧值不再导致启动失败(之前会 SystemExit(1))

验证

  • 全量回归(基于 v1.9.6):143 suites / 3960 checks,0 failed
  • 新增 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。

企业 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 映射判定,环境变量
相关的断言改为「已失效」,隔离用例改为按账号验证。
@zjzhang-cn

Copy link
Copy Markdown
Contributor Author
Image

@shuishuipingan shuishuipingan left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

审查结论:方向有价值,但当前不合并(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 发布。

Zhang, Zhenjiang added 5 commits November 11, 2026 10:38
按 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 时显示默认实例域,而不是仍显示公网域。
@shuishuipingan
shuishuipingan marked this pull request as ready for review November 11, 2026 05:30
@shuishuipingan

Copy link
Copy Markdown
Owner

账号级多实例 VPC 的方向有价值。根据维护者决定,本 PR 关闭、不合并;后续由维护者基于当前主分支独立实现,保留 QD_CN_VPC_ENDPOINT 默认值兼容,并覆盖实例级重定向隔离、非法配置 fail-closed、导入往返和 CLI 实例扫描。感谢提出场景与方案。

@shuishuipingan

Copy link
Copy Markdown
Owner

按维护者决定已关闭且未合并。本次从主分支独立实现已落地 0b0885f,版本 v1.9.7:保留环境默认值、账号三态覆盖;同实例重定向隔离、非法配置出站前拒绝、导入往返、配置实例扫描、冻结 OAuth/PAT、账号快照及旧请求回写保护均有行为回归。本地 144 套件 / 4024 checks 通过,新增后端79项、前端39项。正式发布 CI 与镜像核验进行中。感谢提出多实例场景。

@shuishuipingan

Copy link
Copy Markdown
Owner

独立实现已完成发布 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 。

@zjzhang-cn
zjzhang-cn deleted the feat/account-vpc-v196 branch November 11, 2026 12:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants