package syncer import ( "encoding/json" "strings" ) // credential 是从一个 auth 文件里抽出来的、同步对账需要的**最小字段集**。 // // 为什么只留这几个:冲突裁决只需要回答"哪一端的 accessToken 更新", // 判据就是 expiresAt(外加 refreshToken 有无)。没必要把整份凭证建模一遍—— // 少建模就少一处会被上游改动作废的地方。 type credential struct { UID string Nickname string ExpiresAt int64 HasRefresh bool Realm string Domain string } // parseCredential 解析 auth 文件的两种磁盘形态,返回 (凭证, 是否为可用凭证)。 // // 形态(与网关 auth.Parse 同口径): // // 嵌套形 {"auth":{...},"account":{...}} —— 插件 OAuth 输出 // 扁平形 {"accessToken":...,"uid":...} —— 手写 / 旧版 // // ⚠️ 这里**刻意不复用 internal/auth**,尽管那是网关自己的解析器。 // 原因:本工具要做成**独立 exe**(用户换掉上游新版的 wb2api.exe 时,启动器不受影响), // 一旦 import 网关内部包,启动器和网关的源码目录就绑死了——上游重构 internal/auth // 会让启动器连编译都过不去。自带一份容错解析,代价是几十行,换来的是两边解耦。 // // 容错原则:解析不出来**不是错误**。返回 false 表示"这只是一堆字节",对账会退化成 // 按内容哈希直接比较(仍能正确同步,只是失去了 expiresAt 这档更精细的裁决能力)。 func parseCredential(raw []byte) (credential, bool) { var probe map[string]json.RawMessage if len(raw) == 0 || json.Unmarshal(raw, &probe) != nil { return credential{}, false } var c credential if _, nested := probe["auth"]; nested { var n struct { Auth struct { AccessToken string `json:"accessToken"` RefreshToken string `json:"refreshToken"` ExpiresAt int64 `json:"expiresAt"` Domain string `json:"domain"` Realm string `json:"realm"` } `json:"auth"` Account struct { UID string `json:"uid"` Nickname string `json:"nickname"` } `json:"account"` } if json.Unmarshal(raw, &n) != nil { return credential{}, false } if strings.TrimSpace(n.Auth.AccessToken) == "" { return credential{}, false } c = credential{ UID: n.Account.UID, Nickname: n.Account.Nickname, ExpiresAt: n.Auth.ExpiresAt, HasRefresh: strings.TrimSpace(n.Auth.RefreshToken) != "", Realm: n.Auth.Realm, Domain: n.Auth.Domain, } } else { var f struct { AccessToken string `json:"accessToken"` RefreshToken string `json:"refreshToken"` ExpiresAt int64 `json:"expiresAt"` Domain string `json:"domain"` Realm string `json:"realm"` UID string `json:"uid"` Nickname string `json:"nickname"` } if json.Unmarshal(raw, &f) != nil { return credential{}, false } if strings.TrimSpace(f.AccessToken) == "" { return credential{}, false } c = credential{ UID: f.UID, Nickname: f.Nickname, ExpiresAt: f.ExpiresAt, HasRefresh: strings.TrimSpace(f.RefreshToken) != "", Realm: f.Realm, Domain: f.Domain, } } c.Realm = resolveRealm(c.Realm, c.Domain) return c, true } // resolveRealm 归一化 realm:显式非空优先,否则按 domain 后缀推断,最后回落 "cn"。 func resolveRealm(explicit, domain string) string { if r := strings.TrimSpace(explicit); r != "" { return r } d := strings.ToLower(strings.TrimSpace(domain)) if d == "workbuddy.ai" || strings.HasSuffix(d, ".workbuddy.ai") { return "global" } return "cn" }