mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-15 23:22:59 +08:00
fix: match Codex session imports by chatgpt_user_id before shared account id
Members of the same ChatGPT team share chatgpt_account_id, so matching imports by the account key first could overwrite another member's account credentials. Identity keys are now ordered by strength (user > email > access > account), and an account-key hit is rejected when both sides carry different chatgpt_user_id values. - Keep the account-key fallback when either side lacks a user id, so legacy accounts without chatgpt_user_id are updated and backfilled instead of duplicated - Index all candidate accounts per shared key so a teammate's row can no longer shadow a legacy account depending on row order - Apply the same conflict check to in-batch dedup and emit a warning when a legacy account is claimed via the shared account key - Scope a 120s timeout to the Codex session import request instead of raising the global client timeout
This commit is contained in:
@@ -106,7 +106,7 @@ type codexJWTOpenAIClaims struct {
|
||||
}
|
||||
|
||||
type codexAccountIndex struct {
|
||||
accountsByKey map[string]service.Account
|
||||
accountsByKey map[string][]service.Account
|
||||
}
|
||||
|
||||
func (h *AccountHandler) ImportCodexSession(c *gin.Context) {
|
||||
@@ -178,7 +178,7 @@ func (h *AccountHandler) importCodexSessions(ctx context.Context, req CodexSessi
|
||||
}
|
||||
skipMixedChannelCheck := req.ConfirmMixedChannelRisk != nil && *req.ConfirmMixedChannelRisk
|
||||
|
||||
seenIdentity := map[string]int{}
|
||||
seenIdentity := map[string]codexSeenIdentity{}
|
||||
for _, entry := range entries {
|
||||
item, err := normalizeCodexImportEntry(entry)
|
||||
if err != nil {
|
||||
@@ -225,7 +225,7 @@ func (h *AccountHandler) importCodexSessions(ctx context.Context, req CodexSessi
|
||||
})
|
||||
}
|
||||
|
||||
if duplicateIndex, ok := firstSeenCodexIdentity(seenIdentity, item.IdentityKeys); ok {
|
||||
if duplicateIndex, ok := firstSeenCodexIdentity(seenIdentity, item.IdentityKeys, item.UserID); ok {
|
||||
message := fmt.Sprintf("与第 %d 条导入项重复,已跳过", duplicateIndex)
|
||||
result.Skipped++
|
||||
result.Items = append(result.Items, CodexSessionImportItem{
|
||||
@@ -241,9 +241,18 @@ func (h *AccountHandler) importCodexSessions(ctx context.Context, req CodexSessi
|
||||
})
|
||||
continue
|
||||
}
|
||||
markCodexIdentitySeen(seenIdentity, item.IdentityKeys, entry.Index)
|
||||
markCodexIdentitySeen(seenIdentity, item.IdentityKeys, entry.Index, item.UserID)
|
||||
|
||||
if existing := index.Find(item.IdentityKeys); existing != nil && updateExisting {
|
||||
existing, matchedKey := index.Find(item.IdentityKeys, item.UserID)
|
||||
if existing != nil && updateExisting {
|
||||
if strings.HasPrefix(matchedKey, "account:") && item.UserID != "" &&
|
||||
codexCredentialString(existing.Credentials, "chatgpt_user_id") == "" {
|
||||
result.Warnings = append(result.Warnings, CodexSessionImportMessage{
|
||||
Index: entry.Index,
|
||||
Name: accountName,
|
||||
Message: "已有账号未记录 chatgpt_user_id,已按共享的 chatgpt_account_id 匹配并回填,请确认两者属于同一用户",
|
||||
})
|
||||
}
|
||||
mergedCredentials := mergeCodexImportCredentials(existing.Credentials, credentials, item)
|
||||
mergedExtra := mergeCodexImportMap(existing.Extra, extra)
|
||||
updateInput := &service.UpdateAccountInput{
|
||||
@@ -806,13 +815,13 @@ func sanitizeCodexImportCredentialExtras(input map[string]any) map[string]any {
|
||||
return out
|
||||
}
|
||||
|
||||
// buildCodexIdentityKeys 按身份强度排序生成匹配键:chatgpt_account_id 在同一
|
||||
// ChatGPT 团队内是共享的,因此 account: 键排在最后,且命中时还需通过
|
||||
// codexIdentityConflicts 的跨用户校验才生效。
|
||||
func buildCodexIdentityKeys(accountID, userID, email, accessToken string) []string {
|
||||
keys := make([]string, 0, 4)
|
||||
keys := make([]string, 0, 3)
|
||||
accountID = strings.TrimSpace(accountID)
|
||||
userID = strings.TrimSpace(userID)
|
||||
if accountID != "" {
|
||||
keys = append(keys, "account:"+accountID)
|
||||
}
|
||||
if userID != "" {
|
||||
keys = append(keys, "user:"+userID)
|
||||
}
|
||||
@@ -824,11 +833,14 @@ func buildCodexIdentityKeys(accountID, userID, email, accessToken string) []stri
|
||||
if accessToken = strings.TrimSpace(accessToken); accessToken != "" {
|
||||
keys = append(keys, "access:"+codexTokenFingerprint(accessToken))
|
||||
}
|
||||
if accountID != "" {
|
||||
keys = append(keys, "account:"+accountID)
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
func buildCodexAccountIndex(accounts []service.Account) *codexAccountIndex {
|
||||
index := &codexAccountIndex{accountsByKey: map[string]service.Account{}}
|
||||
index := &codexAccountIndex{accountsByKey: map[string][]service.Account{}}
|
||||
for _, account := range accounts {
|
||||
index.Add(account)
|
||||
}
|
||||
@@ -840,7 +852,7 @@ func (i *codexAccountIndex) Add(account service.Account) {
|
||||
return
|
||||
}
|
||||
if i.accountsByKey == nil {
|
||||
i.accountsByKey = map[string]service.Account{}
|
||||
i.accountsByKey = map[string][]service.Account{}
|
||||
}
|
||||
keys := buildCodexIdentityKeys(
|
||||
codexCredentialString(account.Credentials, "chatgpt_account_id"),
|
||||
@@ -849,34 +861,73 @@ func (i *codexAccountIndex) Add(account service.Account) {
|
||||
codexCredentialString(account.Credentials, "access_token"),
|
||||
)
|
||||
for _, key := range keys {
|
||||
i.accountsByKey[key] = account
|
||||
i.accountsByKey[key] = upsertCodexAccount(i.accountsByKey[key], account)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *codexAccountIndex) Find(keys []string) *service.Account {
|
||||
// upsertCodexAccount 保留同一键下的全部候选账号(共享的 account: 键可对应
|
||||
// 团队内多个账号),同一账号重复 Add 时原位替换为最新状态。
|
||||
func upsertCodexAccount(accounts []service.Account, account service.Account) []service.Account {
|
||||
for idx := range accounts {
|
||||
if accounts[idx].ID == account.ID {
|
||||
accounts[idx] = account
|
||||
return accounts
|
||||
}
|
||||
}
|
||||
return append(accounts, account)
|
||||
}
|
||||
|
||||
// Find 返回第一个通过跨用户校验的候选账号及其命中的匹配键。
|
||||
func (i *codexAccountIndex) Find(keys []string, userID string) (*service.Account, string) {
|
||||
if i == nil {
|
||||
return nil
|
||||
return nil, ""
|
||||
}
|
||||
for _, key := range keys {
|
||||
if account, ok := i.accountsByKey[key]; ok {
|
||||
return &account
|
||||
for _, account := range i.accountsByKey[key] {
|
||||
if codexIdentityConflicts(key, userID, codexCredentialString(account.Credentials, "chatgpt_user_id")) {
|
||||
continue
|
||||
}
|
||||
return &account, key
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return nil, ""
|
||||
}
|
||||
|
||||
func firstSeenCodexIdentity(seen map[string]int, keys []string) (int, bool) {
|
||||
// codexIdentityConflicts 判断 account: 键的命中是否把同一 ChatGPT 团队的两个
|
||||
// 不同成员误连到一起:双方都携带 user id 且不相等时视为冲突。任一侧缺少
|
||||
// user id 时保留匹配,使早期未记录 chatgpt_user_id 的存量账号仍能被更新
|
||||
// (并借助凭据合并回填 user id),而不是产生重复账号。
|
||||
func codexIdentityConflicts(key, userID, storedUserID string) bool {
|
||||
if !strings.HasPrefix(key, "account:") {
|
||||
return false
|
||||
}
|
||||
userID = strings.TrimSpace(userID)
|
||||
storedUserID = strings.TrimSpace(storedUserID)
|
||||
return userID != "" && storedUserID != "" && userID != storedUserID
|
||||
}
|
||||
|
||||
type codexSeenIdentity struct {
|
||||
index int
|
||||
userID string
|
||||
}
|
||||
|
||||
func firstSeenCodexIdentity(seen map[string]codexSeenIdentity, keys []string, userID string) (int, bool) {
|
||||
for _, key := range keys {
|
||||
if index, ok := seen[key]; ok {
|
||||
return index, true
|
||||
entry, ok := seen[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if codexIdentityConflicts(key, userID, entry.userID) {
|
||||
continue
|
||||
}
|
||||
return entry.index, true
|
||||
}
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func markCodexIdentitySeen(seen map[string]int, keys []string, index int) {
|
||||
func markCodexIdentitySeen(seen map[string]codexSeenIdentity, keys []string, index int, userID string) {
|
||||
for _, key := range keys {
|
||||
seen[key] = index
|
||||
seen[key] = codexSeenIdentity{index: index, userID: userID}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user