File size: 18,871 Bytes
6d60378
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
// Package headers 构造三类上游请求头(common / chat / billing / refresh)。
// 规则来自 docs/api-reference.md §0/§4/§6。
package upstream

import (
	"crypto/sha256"
	"encoding/hex"
	"net/http"
	"strings"

	"github.com/linguo2625469/workbuddy2api-panel/internal/auth"
	"github.com/linguo2625469/workbuddy2api-panel/internal/session"
)

const (
	// defaultClientVersion 出站 WorkBuddy 客户端版本段(UA 的 `WorkBuddy/<ver>` 与
	// 白名单头组的 X-IDE-Version)。对齐官方 WorkBuddy Desktop 分发包版本(5.5.4)。
	// config upstream.client_version 可覆盖(空 = 内置默认)。
	defaultClientVersion = "5.5.4"
	// defaultCliVersion 出站 UA 中 `CLI/<ver>` 段版本。对齐官方内置 CLI(2.137.1)。
	// config upstream.cli_version 可覆盖(空 = 内置默认)。
	defaultCliVersion = "2.137.1"

	originRefererCN     = "https://www.codebuddy.cn"
	originRefererGlobal = "https://www.workbuddy.ai"
)

// originRefererFor 按账号 realm 返回 Origin/Referer 基础域:
// global → https://www.workbuddy.ai;cn(含全局开关未开)→ https://www.codebuddy.cn。
func originRefererFor(a *auth.Auth) string {
	if a != nil && a.IsGlobal() {
		return originRefererGlobal
	}
	return originRefererCN
}

// clientVersion 生效的 WorkBuddy 客户端版本:Client.ClientVersion 非空则取之,
// 否则内置默认 defaultClientVersion。
func (c *Client) clientVersion() string {
	if c != nil && c.ClientVersion != "" {
		return c.ClientVersion
	}
	return defaultClientVersion
}

// cliVersion 生效的 CLI 版本:Client.CliVersion 非空则取之,否则内置默认 defaultCliVersion。
func (c *Client) cliVersion() string {
	if c != nil && c.CliVersion != "" {
		return c.CliVersion
	}
	return defaultCliVersion
}

// defaultWorkBuddyUAFor 组装默认客户端出站 UA(官方桌面端 RestOperations 层形状):
// `WorkBuddy/<clientVersion> <platform>/<clientVersion> CLI/<cliVersion>`。
// 平台段(第二段)品牌按 realm 切换——CN 用 applicationName 同值 `WorkBuddy`,
// global 用官方国际版 productName `WorkBuddy AI`(intl 项目逆向证据:
// `WorkBuddy/5.5.2 WorkBuddy AI/5.5.2 CLI/5.5.2`)。
// global 账号送错平台段(`WorkBuddy` 非 `WorkBuddy AI`)可能触发上游 403 code 11140
// "request illegal" 风控。官方无任何 UA 随机化,故默认确定性。
func (c *Client) defaultWorkBuddyUAFor(a *auth.Auth) string {
	platform := "WorkBuddy"
	if a != nil && a.IsGlobal() {
		platform = "WorkBuddy AI"
	}
	return "WorkBuddy/" + c.clientVersion() + " " + platform + "/" + c.clientVersion() + " CLI/" + c.cliVersion()
}

// defaultWorkBuddyUA 返回 CN 形态的默认 UA(默认账号形态即 CN,零回归兼容既有调用/测试)。
func (c *Client) defaultWorkBuddyUA() string {
	return c.defaultWorkBuddyUAFor(nil)
}

// userAgent 返回当前出站 UA(客户端出站路径:chat/refresh/FetchModels)。
// 优先级:Client.UserAgent(config user_agent)显式覆盖 > 按账号 realm 的默认 WorkBuddy 三段式。
// 显式覆盖兼容既有覆盖逻辑:用户配了即以用户值为准(自定义品牌/版本),
// 未配则走官方桌面端默认形态(global 换 `WorkBuddy AI` 平台段)。
func (c *Client) userAgent(a *auth.Auth) string {
	if c != nil && c.UserAgent != "" {
		return c.UserAgent
	}
	return c.defaultWorkBuddyUAFor(a)
}

// billingUA 白名单类(billing/checkin/banner)出站 UA:单段 `WorkBuddy/<clientVersion>`
// (官方 banner 显式覆写形态,不带 CLI 段)。默认生效(伪造官方桌面端指纹);
// 显式 client_name="SaaS" 才不设 UA(还原旧行为,Go 默认 UA)。
func (c *Client) billingUA() string {
	if c == nil || c.attributionClientName() == "SaaS" {
		return ""
	}
	return "WorkBuddy/" + c.clientVersion()
}

// resolveDeviceToken 解析本次请求的 X-Device-Token 取值。
// 优先级:auth.Auth.DeviceToken(每号)> Client.DeviceToken(config 全局)> 文件兜底。
// 三者皆空/读失败则返回空串(调用方不注入该头,优雅降级)。
func (c *Client) resolveDeviceToken(a *auth.Auth) string {
	if a != nil && a.DeviceToken != "" {
		return a.DeviceToken
	}
	if c != nil && c.DeviceToken != "" {
		return c.DeviceToken
	}
	if c != nil && c.DeviceTokenFile != "" {
		return readDeviceTokenFile(c.DeviceTokenFile)
	}
	return ""
}

// injectDeviceToken 在 req 注入 X-Device-Token 头(仅当取到非空 token)。
func (c *Client) injectDeviceToken(req *http.Request, a *auth.Auth) {
	if tok := c.resolveDeviceToken(a); tok != "" {
		req.Header.Set("X-Device-Token", tok)
	}
}

// deriveAccountStableID 按 uid + 用途盐稳定派生 36 hex 设备/会话标识。
// 跨重启稳定(固定盐 "wb2a:",不随进程换——这是与 session 包派生盐的本质差异:
// 那是会话键维度的进程级随机盐,重启换新;本函数是账号维度,必须跨重启恒定)、
// 账号间互异(uid 不同则不同)、同 uid 同用途恒同值(幂等)。用 sha256 与项目
// 既有派生(session/ids.go、cache_key.go)保持一致;截 36 hex 提供更长熵。
//
// 两个用途:
//   - purpose="machine" → X-Machine-ID(设备级,跨会话稳定)
//   - purpose="session" → X-Session-ID(账号固定会话,跨重启稳定)
//
// 与 injectDeviceToken 的 X-Device-Token 并存不冲突:那是登录时上游签发的
// 真实设备令牌(有则发,权威);本对头是「每账号一台固定虚拟设备」的稳定指纹,
// 防多号被上游按设备指纹缺失/漂移关联风控。两者是不同头族,官方桌面端都发。
func deriveAccountStableID(uid, purpose string) string {
	sum := sha256.Sum256([]byte("wb2a:" + purpose + ":" + uid))
	return hex.EncodeToString(sum[:18]) // 36 hex chars
}

// injectAccountStableHeaders 在 req 注入 X-Machine-ID / X-Session-ID:按 uid 稳定
// 派生,跨重启固定、账号间互异。uid 为空时不注入(匿名请求无设备标识,上游不要求)。
func (c *Client) injectAccountStableHeaders(req *http.Request, a *auth.Auth) {
	if a == nil || a.UID == "" {
		return
	}
	req.Header.Set("X-Machine-ID", deriveAccountStableID(a.UID, "machine"))
	req.Header.Set("X-Session-ID", deriveAccountStableID(a.UID, "session"))
}

// CommonHeaders 设置所有 API 共享的请求头。
func (c *Client) CommonHeaders(req *http.Request, a *auth.Auth) {
	req.Header.Set("Content-Type", "application/json")
	// Accept 非流式默认 application/json(D6:去掉宽松的 text/plain, */*)。
	// chat 流式路径在 ChatHeaders 覆盖为 event-stream。
	req.Header.Set("Accept", "application/json")
	req.Header.Set("X-Requested-With", "XMLHttpRequest")
	origin := originRefererFor(a)
	req.Header.Set("Origin", origin)
	req.Header.Set("Referer", origin+"/")
	req.Header.Set("User-Agent", c.userAgent(a))
	// X-CodeBuddy-Request: 1(官方客户端风控闸门头,所有 API 请求必带,D1)。
	req.Header.Set("X-CodeBuddy-Request", "1")
	// Accept-Language 按 realm 切(D5):CN zh-CN,global en-US。官方客户端按账号域
	// 发对应语言标识,对齐避免上游风控按语言缺失误判。
	req.Header.Set("Accept-Language", acceptLanguageFor(a))
	// X-Machine-ID / X-Session-ID:按 uid 稳定派生的账号级设备头(见
	// injectAccountStableHeaders)。注入在 CommonHeaders——chat 经 ChatHeaders
	// 叠加 CommonHeaders 天然继承;billing 域另行注入,全出站覆盖。
	c.injectAccountStableHeaders(req, a)
}

// acceptLanguageFor 按账号 realm 返回 Accept-Language:global → en-US,cn → zh-CN。
func acceptLanguageFor(a *auth.Auth) string {
	if a != nil && a.IsGlobal() {
		return "en-US"
	}
	return "zh-CN"
}

// injectCodeBuddyRequest 在 req 注入 X-CodeBuddy-Request: 1。
// billing 域未走 CommonHeaders,单独注入保证全出站覆盖(D1)。
func (c *Client) injectCodeBuddyRequest(req *http.Request) {
	req.Header.Set("X-CodeBuddy-Request", "1")
}

// ChatMeta 一次 chat 出站的会话头族元数据(issue #35:后台按 X-Conversation-Request-ID
// 聚合请求,官方客户端一次 user send 内所有 tool call/重试/换号复用同一个 ID)。
// handler 在轮转循环外生成 conversationID / conversationRequestID,循环内每次出站
// 复用同值;TraceID 透传入站值(空 = 回落 conversationRequestID)。
// messageID(消息级,每条独立)由 ChatHeaders 内部生成,无需外部可见。
type ChatMeta struct {
	ConversationID        string // X-Conversation-ID:body 提取的入站值,空则不发(透传优先,不伪造)
	ConversationRequestID string // X-Conversation-Request-ID / X-Root-Request-ID:聚合主键,必发
	TraceID               string // X-Trace-ID:入站透传值,空则回落 conversationRequestID
}

// ChatHeaders 在 common 之上加 chat 专属的账号头。
// 缺省字段用 X-No-* 约定(与 CodeBuddy 官方 CLI 一致)。
// clientIP 为本次请求的客户端 IP(按参数传递,不读共享字段——避免并发串扰);
// PassthroughIP=false 或 clientIP 为空时不注入 IP 头。
// meta 为会话头族元数据(纯新增,不改既有头),见 injectConversationHeaders。
func (c *Client) ChatHeaders(req *http.Request, a *auth.Auth, clientIP string, meta ChatMeta) {
	c.CommonHeaders(req, a)
	// chat 流式 Accept 覆盖 CommonHeaders 的非流式默认(D6)。
	req.Header.Set("Accept", "application/json, text/event-stream")
	// AccessToken 加锁快照:keepalive 定时刷新会在 a.mu 内改写它,锁外直读构成数据竞争
	// (见 auth.AccessTokenValue 注释)。
	if at := a.AccessTokenValue(); at != "" {
		req.Header.Set("Authorization", "Bearer "+at)
	} else {
		req.Header.Set("X-No-Authorization", "1")
	}
	if a.UID != "" {
		req.Header.Set("X-User-Id", a.UID)
	} else {
		req.Header.Set("X-No-User-Id", "1")
	}
	// 安全红线:绝不在 chat 请求里携带 X-Refresh-Token。
	// 企业与域头按 realm 分发:CN 走既有分支(EnterpriseID/Domain 原样透传,缺省 X-No-*);
	// global 账号由 injectGlobalChatHeaders 统一覆写为国际客户端形态
	// (X-No-Enterprise-Id=1 声明无企业 + X-Domain=www.workbuddy.ai 声明国际版域),
	// 且不回退 X-Domain 到登录会话原值——对齐 intl 项目出站头。
	if a != nil && !a.IsGlobal() {
		if a.EnterpriseID != "" {
			req.Header.Set("X-Enterprise-Id", a.EnterpriseID)
		} else {
			req.Header.Set("X-No-Enterprise-Id", "1")
		}
		if d := a.DomainValue(); d != "" {
			req.Header.Set("X-Domain", d)
		} else {
			req.Header.Set("X-No-Department-Info", "1")
		}
	} else {
		c.injectGlobalChatHeaders(req, a)
	}
	// 用量归属头:默认伪造 WorkBuddy 桌面端指纹(client_name="SaaS" 还原旧行为)。
	c.injectAttribution(req)
	// 客户端 IP 透传(仅 PassthroughIP=true 且本次请求带 IP)。
	c.injectClientIP(req, clientIP)
	// 设备风控头:auth 每号 > config 全局 > 文件兜底;空则不注入。
	c.injectDeviceToken(req, a)
	// 会话头族(对话/请求/消息/B3 链路),见 injectConversationHeaders。
	c.injectConversationHeaders(req, meta)
}

// injectGlobalChatHeaders global 账号(无企业 ID)的 chat 专属声明头,对齐 intl 项目
// (ANALYSIS-global-chat-solutions.md):
//   - X-No-Enterprise-Id: 1  个人账号无企业 ID,显式声明(避免上游按缺省/可疑判定)
//   - X-Domain: www.workbuddy.ai  显式声明国际版域(与 Origin/Referer 同域)
//
// 仅 global realm 注入;CN 账号走既有 X-No-Department-Info 等分支,零回归。
func (c *Client) injectGlobalChatHeaders(req *http.Request, a *auth.Auth) {
	if a == nil || !a.IsGlobal() {
		return
	}
	req.Header.Set("X-No-Enterprise-Id", "1")
	req.Header.Set("X-Domain", "www.workbuddy.ai")
}

// injectConversationHeaders 注入官方客户端会话头族(issue #35 后台聚合)。
// 头族分四层,各司其职:
//   - X-Conversation-ID:会话级,多轮稳定(body 的 conversationId)。空则不发——
//     透传客户端原值优先,客户端没给就不伪造,避免误导后台建错会话。
//   - X-Conversation-Request-ID:**对话轮级聚合主键**,必发。一次 user send 内的
//     所有 tool call/重试/换号/降级复用同一个 → 后台按它聚合成一条(不再碎片化)。
//   - X-Conversation-Message-ID = X-Request-ID:消息级,每条独立(32 位 hex)。
//   - X-Root-Request-ID:= conversationRequestID(根请求追踪)。
//   - X-Trace-ID:入站透传或 = conversationRequestID。
//   - X-B3-TraceId / X-B3-SpanId / X-B3-Sampled:链路族。B3 规范只认 16/32 hex
//     TraceId 与 16 hex SpanId;入站 conversationRequestID 非法时 TraceId 回落
//     messageID(恒 32 hex),SpanId 取 messageID[:16](每消息新)。
func (c *Client) injectConversationHeaders(req *http.Request, meta ChatMeta) {
	convReqID := meta.ConversationRequestID
	if convReqID == "" {
		// 零值 meta(直接调 ChatHeaders 的调用方/测试)也要保证聚合主键必发:
		// 本级补一个 32 hex,调用方(handler)已生成稳定的不走到这里。
		convReqID = session.NewMessageID()
	}
	messageID := session.NewMessageID()
	if meta.ConversationID != "" {
		req.Header.Set("X-Conversation-ID", meta.ConversationID)
	}
	req.Header.Set("X-Conversation-Request-ID", convReqID)
	req.Header.Set("X-Conversation-Message-ID", messageID)
	req.Header.Set("X-Request-ID", messageID)
	req.Header.Set("X-Root-Request-ID", convReqID)
	traceID := meta.TraceID
	if traceID == "" {
		traceID = convReqID
	}
	req.Header.Set("X-Trace-ID", traceID)
	b3Trace := convReqID
	if !validTraceID(b3Trace) {
		b3Trace = messageID // 非法 B3 TraceId → 回落恒 32 hex 的消息级 ID
	}
	req.Header.Set("X-B3-TraceId", b3Trace)
	req.Header.Set("X-B3-SpanId", messageID[:16])
	req.Header.Set("X-B3-Sampled", "1")
}

// validTraceID 判断 B3 TraceId 是否合法:16 或 32 位 hex(大小写均可)。
// 官方客户端生成的 conversationRequestId 是 32 位 hex(UUID 去横线),入站透传值
// 可能是任意形状(含横线/超长/非 hex),直接塞进 B3 头会破坏链路关联(issue #35)。
func validTraceID(s string) bool {
	if len(s) != 16 && len(s) != 32 {
		return false
	}
	for i := 0; i < len(s); i++ {
		ch := s[i]
		if !((ch >= '0' && ch <= '9') || (ch >= 'a' && ch <= 'f') || (ch >= 'A' && ch <= 'F')) {
			return false
		}
	}
	return true
}

// attributionClientName 生效的用量归属名:ClientName 非空取之;
// 空默认 "WorkBuddy"(伪造官方桌面端指纹;显式配 "SaaS" 可还原旧行为)。
func (c *Client) attributionClientName() string {
	if c != nil && c.ClientName != "" {
		return c.ClientName
	}
	return "WorkBuddy"
}

// injectAttribution 注入用量归属头(X-Agent-Purpose / X-IDE-* / X-Product)。
// 仅在 chat/completions 路径生效(ChatHeaders 调用)。
//
// 默认(ClientName 空)即对齐官方 WorkBuddy 桌面端指纹:X-Agent-Purpose="conversation"
// + X-IDE-Name/Type/Product="WorkBuddy" + X-IDE-Version=client_version,上游用量归因
// 不再出现 client/agentPurpose 为空的「网关特征」。显式 ClientName="SaaS" 还原旧行为
// (仅 X-Product="SaaS",不设 X-IDE-*);配其他值则四头跟随该值。
func (c *Client) injectAttribution(req *http.Request) {
	name := c.attributionClientName()
	if name == "SaaS" {
		req.Header.Set("X-Product", "SaaS")
		return
	}
	req.Header.Set("X-Agent-Purpose", "conversation")
	req.Header.Set("X-IDE-Name", name)
	req.Header.Set("X-IDE-Type", name)
	req.Header.Set("X-IDE-Version", c.clientVersion())
	req.Header.Set("X-Product", name)
}

// injectClientIP 在 PassthroughIP 开启时把 clientIP 参数透传给上游(三等价头)。
func (c *Client) injectClientIP(req *http.Request, clientIP string) {
	if c == nil || !c.PassthroughIP || clientIP == "" {
		return
	}
	req.Header.Set("X-Forwarded-For", clientIP)
	req.Header.Set("X-Real-IP", clientIP)
	req.Header.Set("X-Client-IP", clientIP)
}

// ExtractClientIP 从入站请求提取客户端 IP 首段(X-Forwarded-For 首段,回落 X-Real-IP)。
func ExtractClientIP(r *http.Request) string {
	if r == nil {
		return ""
	}
	if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
		for i := 0; i < len(xff); i++ {
			if xff[i] == ',' {
				return strings.TrimSpace(xff[:i])
			}
		}
		return strings.TrimSpace(xff)
	}
	if real := strings.TrimSpace(r.Header.Get("X-Real-IP")); real != "" {
		return real
	}
	return ""
}

// BillingHeaders billing 接口请求头。
// UA 语义:默认**不设置**(保持现状,Go 客户端自带默认 UA);仅当显式配置
// c.UserAgent 非空才覆盖——避免默认路径给 billing 引入新的 UA 指纹。
func (c *Client) BillingHeaders(req *http.Request, a *auth.Auth) {
	// AccessToken 加锁快照(同 ChatHeaders:keepalive 可在 a.mu 内改写)。
	req.Header.Set("Authorization", "Bearer "+a.AccessTokenValue())
	req.Header.Set("Accept", "application/json")
	req.Header.Set("Content-Type", "application/json")
	c.injectCodeBuddyRequest(req)
	// Accept-Language 按 realm 切(D5,billing 域未走 CommonHeaders,单独注入)。
	req.Header.Set("Accept-Language", acceptLanguageFor(a))
	if c != nil && c.UserAgent != "" {
		req.Header.Set("User-Agent", c.UserAgent)
	} else if ua := c.billingUA(); ua != "" {
		req.Header.Set("User-Agent", ua)
	}
	if a.UID != "" {
		req.Header.Set("X-User-Id", a.UID)
	}
	if a.EnterpriseID != "" {
		req.Header.Set("X-Enterprise-Id", a.EnterpriseID)
		req.Header.Set("X-Tenant-Id", a.EnterpriseID)
	}
	if d := a.DomainValue(); d != "" {
		req.Header.Set("X-Domain", d)
	}
	// 设备风控头:billing 域(report/travel/balance/checkin)同样注入。
	c.injectDeviceToken(req, a)
}

// RefreshHeaders refresh 端点专属头(X-Refresh-Token 只允许出现在这里)。
func (c *Client) RefreshHeaders(req *http.Request, a *auth.Auth) {
	c.CommonHeaders(req, a)
	req.Header.Set("X-Refresh-Token", a.RefreshToken)
	if a.EnterpriseID != "" {
		req.Header.Set("X-Enterprise-Id", a.EnterpriseID)
	}
	// X-Auth-Refresh-Source 对齐官方客户端 refresh 渠道标识 "plugin"(D3)。
	req.Header.Set("X-Auth-Refresh-Source", "plugin")
}