本文件是当前上游行为的参考,随代码一起维护。 时间点快照、逆向过程与证据位置见
RECON.md—— 那份是历史记录, 不应被当作「现在仍然如此」的依据。两者冲突时以本文件与代码为准,并回头修本文件。对齐版本:CodeArts Agent 26.9.902(
plugin-version/ide-version), 观测时间 2026-10。上游随时可能变更。2026-10 变更:免费权益(benefit)通道整体下线 ——
benefit-gateway-config返回enabled:false,gateway/config、benefit/claim、user/tokens/balance一律报4000: invalid channel, group not found。本文件已按「只剩积分通道」重写。
置信度标记:✅ 实测跑通 🔶 代码确认、未实跑
| 用途 | 方法 + 路径 | 主机 | 置信度 |
|---|---|---|---|
| 推理(OpenAI 兼容) | POST /api/v2/chat/completions |
https://snap-access.cn-north-4.myhuaweicloud.com |
✅ |
| 备用域名 | 同上 | https://snap-access.cn-north-4.myhuaweicloud.cn |
🔶 |
| benefit 模型清单 | GET /api/v1/gateway/config |
https://opengw.developer.huaweicloud.com |
❌ 已下线(4000: invalid channel, group not found) |
| 每日额度 | GET /api/v1/user/tokens/balance |
同上 | ❌ 已下线(同上) |
| 积分套餐与用量 | GET /snap-manager/v1/statistics/plugin |
https://snap-access.cn-north-4.myhuaweicloud.com |
🔶 代码确认(Agent 26.9.902);路径实测存在(未带凭据返回 401) |
| benefit 通道开关 | GET /v1/benefit-gateway-config |
https://snap-access.cn-north-4.myhuaweicloud.com |
✅ 返回 {"enabled":false} |
| 每日免费 Token 领取 | GET /api/v1/benefit/claim(幂等) |
https://opengw.developer.huaweicloud.com |
❌ 已下线(同上) |
| 可用模型清单 | GET /v1/model/builtin(需 Agent-Type: PromptCenter) |
https://snap-access.cn-north-4.myhuaweicloud.com |
✅ |
| 运营活动清单 | GET /v1/ops/delivery(需 Agent-Type: PromptCenter) |
同上 | ✅ |
| 运营活动领取 | POST /v1/ops/claim(幂等,带 idempotentKey) |
同上 | ✅ |
| 邀请码生成 | POST /v1/ops/invite/generate |
同上 | ✅ |
| 令牌刷新(OAuth2) | POST /v1/oauth2/tokens |
https://sts.cn-north-4.myhuaweicloud.com |
🔶 |
OAuth 客户端标识:client_id = codearts-agent(= 客户端 product.json 的 urlProtocol)。
对应代码常量集中在 internal/codearts/client.go 顶部。改端点只改那里。
上游会据此做路由、埋点与限流,缺省值与 CodeArts Agent 26.9.902 对齐。
| 头 | 缺省值 | 说明 |
|---|---|---|
plugin-name |
codemate_vscode |
插件标识 |
plugin-version |
26.9.902 |
插件版本 |
ide-name |
VSCode |
宿主 IDE |
ide-version |
26.9.902 |
宿主版本 |
app-id |
CodeAgent3.0 |
应用标识 |
X-Language |
zh-cn |
语言 |
x-ot-client-type |
IDE |
客户端类型 |
x-ot-function |
agent-general |
功能路由 |
x-ot-session-id / user-session-id |
ses_<hash> |
必须按凭据复用,见 §6 |
x-ot-trace-id / x-ot-span-id / x-snap-traceid |
随机 | 链路追踪 |
user-msg-id |
msg_<random> |
消息标识 |
created-time |
UTC ISO8601 | 请求时间 |
model-id / model-name |
模型名 | 必须与请求体 model 完全一致,见 §7 |
maas_type |
不再发送 | 免费权益通道已下线,见 §3 |
X-Security-token |
STS token | 仅临时凭据时携带 |
Authorization |
SDK-HMAC-SHA256 ... |
见 §4 |
可在控制台「设置 → 上游身份头」覆盖前四项(plugin-name / plugin-version /
ide-name / ide-version),便于上游版本变更后无需改代码即可跟随。
| 通道 | 请求头 | 计费 | 模型清单来源 | 典型模型 |
|---|---|---|---|---|
| credit | 不带 maas_type |
按积分计费(真实消耗) | /v1/model/builtin |
glm-5.3、GLM-5.2、openpangu-2.0-pro、deepseek-v4.1-flash … |
| benefit(已下线) | maas_type: benefit |
曾消耗每日福利 Token | /api/v1/gateway/config |
glm-5.3-flash、deepseek-v4-flash-0731、deepseek-v4-pro-0813、deepseek-v4.1-flash |
| education / examination | maas_type: education 等 |
— | — | 需学生认证 / 考试资格,普通账号 403 |
2026-10 实测(benefit 通道关闭后):
GET /v1/benefit-gateway-config -> 200 {"enabled":false,"is_examination_domain":false}
GET /api/v1/gateway/config -> 200 {"error_code":"4000","error_msg":"invalid channel, group not found"}
GET /api/v1/benefit/claim -> 200 {"error_code":"4000","error_msg":"invalid channel, group not found"}
GET /api/v1/user/tokens/balance -> 200 {"error_code":"4000","error_msg":"invalid channel, group not found"}
GET /v1/model/builtin -> 200(8 个模型,全部 enable:true)
因此网关不再发送 maas_type,也不再保留 benefit 通道的模型清单 / 额度 / 领取逻辑。
原来那条「无法识别的模型名必须落到 benefit 通道」的约束随之失效 ——
现在无法识别的模型名直接报 404,绝不静默换成别的模型扣费。
积分通道的「额度」不在已下线的 benefit 接口里,而在 snap-manager 的统计插件接口。
网关 GET /admin/api/credits 用它给控制台填「积分额度」列。
GET https://snap-access.cn-north-4.myhuaweicloud.com/snap-manager/v1/statistics/plugin
Authorization: SDK-HMAC-SHA256 ... # 与其它 snap-access 接口同一套签名
响应关键字段(package + metrics[]):
| 字段 | 含义 |
|---|---|
package.status |
normal / closed / freeze(控制台映射为 active / expired / frozen) |
package.is_credit_package / is_token_package |
套餐类型 |
metrics[name=usageTotalPackageCredit] |
积分总额:package_credit_amount / package_credit_used / package_credit_remain / package_credit_expiring_amount / value(百分比) |
metrics[name=usageBasicPackageCredit] 等 |
基础 / 按需 / 赠送积分的分类用量 |
metrics[name=usageTokenChatMessages] |
token 套餐用量(积分套餐通常为 0) |
end_date |
额度重置日期 |
没有 usageTotalPackageCredit 时表示当前账号不是积分套餐(token 套餐 / 白名单 / 未开通),
控制台显示「无积分套餐」而不是 0。
逐字对齐 @huaweicloud/huaweicloud-sdk-core 的 JS 实现。有两处与官方文档不同,
必须一起复刻,否则上游必然回 APIG.0301:
- CanonicalURI 每段单独编码后强制补尾斜杠
/api/v2/chat/completions→/api/v2/chat/completions/ - CanonicalHeaders 每行自带尾
\n,拼装时又补一个\n→SignedHeaders之前多一个空行
拼接顺序:
CanonicalRequest =
METHOD \n
CanonicalURI \n
CanonicalQuery \n
CanonicalHeaders(每行 "k:v\n",末尾已含 \n) \n
SignedHeaders(按字典序,';' 连接) \n
sha256Hex(payload)
StringToSign =
"SDK-HMAC-SHA256" \n
X-Sdk-Date \n
sha256Hex(CanonicalRequest)
Signature = hex(HMAC-SHA256(SecretKey, StringToSign))
Authorization = SDK-HMAC-SHA256 Access=<AK>, SignedHeaders=<...>, Signature=<...>
其他细节:
- URL 编码只保留
A-Za-z0-9-._~,其余一律%XX大写。 不能用 Go 的url.PathEscape—— 它会放过$&+,:;=@,导致签名不一致。 - 查询串:键、值各自排序后再编码。
- 只有
PUT/PATCH/POST的 body 参与签名;其余方法按空 body。 - header 名小写化后参与签名;只对值做
trim,不对名字做trim。
实现:internal/codearts/signer.go。
金标准测试:internal/codearts/signer_test.go —— 期望值由独立的 Python 复刻算出,
任何一处 canonical 拼装改动都会让测试失败。改签名逻辑前先读这个测试。
%APPDATA%\codearts-agent\Local State
└─ os_crypt.encrypted_key → DPAPI 保护的主密钥(Windows)
%APPDATA%\codearts-agent\User\globalStorage\state.vscdb (SQLite)
└─ ItemTable['secret://{"extensionId":"huaweicloud.authentication",
"key":"HuaweiCloudSession"}']
└─ 值形如 v10:<12B nonce><ciphertext> → AES-256-GCM
解出来的 JSON 含 accessKey / secretKey / securitytoken / accessToken /
refreshToken / loginContext(DPoP 私钥 + PKCE verifier)/ 过期时间。
平台限制:DPAPI 只有 Windows 有。非 Windows 平台无法自动读取, 必须通过控制台「手工录入」或「从 session JSON 导入」(后者带 refresh_token 与 DPoP 私钥,导入后仍可自动续期)。
实现:internal/codearts/session_windows.go(Windows)、session_other.go(降级)、
sqlite.go(极简只读 + 定长原地写回)。
上游按 x-ot-session-id 统计并发会话,每账号上限 3 个。超出时返回
HTTP 400 + error_code TM.00001041。
因此网关必须按凭据推导并复用同一个 session id(ses_<hash(domainID|AK)>),
而不是每请求随机。凭据轮换(AK 变化)后自然换新。
见 Session.UpstreamSessionID()。
glm-5.3 stream=false → HTTP 400,body 长度 0
glm-5.3 stream=true → 200,正常 SSE(含工具调用)
网关对策:检测到「4xx + 空 body」自动改用流式,本地聚合后以非流式返回;
并把该模型标记为 stream_only,后续直接走流式。见 registry.Model.StreamOnly。
body.model = "glm-5.3 "(尾随空格)
→ 406 {"error_code":"InferHub.001001011.406","error_msg":"The model not equals requestHeader's Model-Id"}
改写模型名时(别名 / 强制模型)必须同时改写 model-id 与 model-name 两个头。
见 rewriteModel() 与 Client.baseHeaders()。
上游常用 200 + {"error_code":"...","error_msg":"..."} 表达失败,HTTP 状态码本身不可信。
真实状态码编码在 error_code 里,实测三种形态:
| 形态 | 例子 | 提取出的状态码 |
|---|---|---|
| 纯数字业务码 | "4005" |
不在 400–599 → 兜底 400 |
| 纯数字状态码 | "400" / "422" |
400 / 422 |
复合码 服务.业务号.状态码 |
InferHub.002002009.404 |
取末段 → 404 |
网关在 statusFromCode() 里做这个提取,取不到一律兜底 400,绝不返回 200。
历史 bug:早期实现固定写
Status: http.StatusOK,于是日志里出现自相矛盾的 「上游 HTTP 200: The request param is invalid」,真实业务码被丢弃;更严重的是 状态码 200 让这类错误无法被IsClientRequestError识别,进而把健康账号标成error、 累加failures并触发熔断。
拿到真实状态码之后,网关才能回答关键问题:这是请求的问题,还是账号的问题?
| 判定 | 条件 | 处置 |
|---|---|---|
| 客户端请求错误 | 400 / 413 / 422,且不是空 body、不是凭据类错误码 |
直接返回客户端,不计账号故障、不换号重试 |
| 临时故障 | 429 / 5xx / TM.00001041 |
退避重试,不计账号故障 |
| 账号 / 上游故障 | 其余(含 APIG.* STS5.* 用 400 表达的情况) |
计入账号失败,换号重试 |
实测的参数类业务码(temperature=99):
{"error_code":"InferHub.001001005.400",
"details":[{"error_code":"RANGE_VALIDATOR",
"error_msg":"temperature -> temperature should be between 0 and 2"}]}→ 网关对外 400 invalid_request_error,账号保持 ok、failures 不变。
/v1/ops/claim 重复调用不会重复发放(带 idempotentKey,服务端按 campaign 去重)。
因此「每日自动领取活动福利」可以放心按本地日期轮询,重复执行无副作用。
(原 /api/v1/benefit/claim 已随 benefit 通道下线,见 §3。)
| 上游错误码 | 含义 | 网关处理 |
|---|---|---|
TM.00001041 |
并发会话数已达上限(HTTP 400) | 按临时故障退避重试(不计入账号失败),仍失败则回 429 |
APIG.0301 / APIG.0303 |
签名校验失败 / 凭据失效 | 翻译成「登录态失效,请重新登录」→ 502 credentials_error |
STS5.1804 |
refresh_token 已过期 | 同上 |
STS5.1806 |
refresh_token 已被使用(单次 + 轮转) | 同上;提示「IDE 与网关同时续期会互相作废」 |
invalid_grant |
refresh_token 失效 | 同上 |
4005 |
该通道不支持此模型 | 400 invalid_request_error(statusFromCode 兜底) |
InferHub.001001005.400 |
请求参数越界(temperature / max_tokens / tools 超长等) |
400 invalid_request_error;不计账号故障、不换号重试 |
InferHub.001001011.406 |
模型名与 model-id 不一致 |
400 invalid_request_error |
InferHub.002002009.404 |
复合码,末段是真实状态码 | 按末段映射(此处 404) |
TM.00001027 |
未通过学生认证(education 通道) | 400(普通账号不应命中) |
上面
APIG.*/STS5.*/invalid_grant即使以 HTTP 400 返回,也必须计入账号故障:IsClientRequestError里有credentialCodes排除表,不能只看状态码是不是 4xx。
凭据失效是唯一需要人工介入的错误,网关会明确提示「请重新登录 CodeArts Agent」,
并刻意不报成 overloaded_error,避免客户端无限重试。
# 1) 直连上游的签名 PoC(不经过网关)
python tools/decrypt_session.py # 先确认凭据能读出来
python tools/probe_api.py # 签名直打上游:模型 / 额度 / 对话
# 2) 经过网关的端到端
go run ./cmd/probe
# 3) 本机 session → 2API 全能力(非流式/流式/思维链/工具调用/Anthropic)
python tools/local_session_e2e.py上游行为变化时:
- 先改本文件(记录新事实与置信度);
- 改
internal/codearts/对应实现; - 更新测试向量(尤其
signer_test.go,它是金标准); - 若影响用户可见行为,同步
README.md的「错误语义」与TROUBLESHOOTING.md。