mirror of
https://github.com/router-for-me/CLIProxyAPIPlus.git
synced 2026-03-26 22:02:23 +00:00
fix(api): propagate note to Gemini virtual auths and align priority parsing
- Read note from Attributes (consistent with priority) in buildAuthFileEntry, fixing missing note on Gemini multi-project virtual auth cards. - Propagate note from primary to virtual auths in SynthesizeGeminiVirtualAuths, mirroring existing priority propagation. - Sync note/priority writes to both Metadata and Attributes in PatchAuthFileFields, with refactored nil-check to reduce duplication (review feedback). - Validate priority type in fallback disk-read path instead of coercing all values to 0 via gjson.Int(), aligning with the auth-manager code path. - Add regression tests for note synthesis, virtual-auth note propagation, and end-to-end multi-project Gemini note inheritance. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -333,10 +333,19 @@ func (h *Handler) listAuthFilesFromDisk(c *gin.Context) {
|
||||
fileData["type"] = typeValue
|
||||
fileData["email"] = emailValue
|
||||
if pv := gjson.GetBytes(data, "priority"); pv.Exists() {
|
||||
fileData["priority"] = int(pv.Int())
|
||||
switch pv.Type {
|
||||
case gjson.Number:
|
||||
fileData["priority"] = int(pv.Int())
|
||||
case gjson.String:
|
||||
if parsed, errAtoi := strconv.Atoi(strings.TrimSpace(pv.String())); errAtoi == nil {
|
||||
fileData["priority"] = parsed
|
||||
}
|
||||
}
|
||||
}
|
||||
if nv := gjson.GetBytes(data, "note"); nv.Exists() && strings.TrimSpace(nv.String()) != "" {
|
||||
fileData["note"] = strings.TrimSpace(nv.String())
|
||||
if nv := gjson.GetBytes(data, "note"); nv.Exists() {
|
||||
if trimmed := strings.TrimSpace(nv.String()); trimmed != "" {
|
||||
fileData["note"] = trimmed
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -427,11 +436,9 @@ func (h *Handler) buildAuthFileEntry(auth *coreauth.Auth) gin.H {
|
||||
entry["priority"] = parsed
|
||||
}
|
||||
}
|
||||
// Expose note from Metadata.
|
||||
if note, ok := auth.Metadata["note"].(string); ok {
|
||||
if trimmed := strings.TrimSpace(note); trimmed != "" {
|
||||
entry["note"] = trimmed
|
||||
}
|
||||
// Expose note from Attributes (set by synthesizer from JSON "note" field).
|
||||
if note := strings.TrimSpace(authAttribute(auth, "note")); note != "" {
|
||||
entry["note"] = note
|
||||
}
|
||||
return entry
|
||||
}
|
||||
@@ -912,26 +919,32 @@ func (h *Handler) PatchAuthFileFields(c *gin.Context) {
|
||||
targetAuth.ProxyURL = *req.ProxyURL
|
||||
changed = true
|
||||
}
|
||||
if req.Priority != nil {
|
||||
if req.Priority != nil || req.Note != nil {
|
||||
if targetAuth.Metadata == nil {
|
||||
targetAuth.Metadata = make(map[string]any)
|
||||
}
|
||||
if *req.Priority == 0 {
|
||||
delete(targetAuth.Metadata, "priority")
|
||||
} else {
|
||||
targetAuth.Metadata["priority"] = *req.Priority
|
||||
if targetAuth.Attributes == nil {
|
||||
targetAuth.Attributes = make(map[string]string)
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
if req.Note != nil {
|
||||
if targetAuth.Metadata == nil {
|
||||
targetAuth.Metadata = make(map[string]any)
|
||||
|
||||
if req.Priority != nil {
|
||||
if *req.Priority == 0 {
|
||||
delete(targetAuth.Metadata, "priority")
|
||||
delete(targetAuth.Attributes, "priority")
|
||||
} else {
|
||||
targetAuth.Metadata["priority"] = *req.Priority
|
||||
targetAuth.Attributes["priority"] = strconv.Itoa(*req.Priority)
|
||||
}
|
||||
}
|
||||
trimmedNote := strings.TrimSpace(*req.Note)
|
||||
if trimmedNote == "" {
|
||||
delete(targetAuth.Metadata, "note")
|
||||
} else {
|
||||
targetAuth.Metadata["note"] = trimmedNote
|
||||
if req.Note != nil {
|
||||
trimmedNote := strings.TrimSpace(*req.Note)
|
||||
if trimmedNote == "" {
|
||||
delete(targetAuth.Metadata, "note")
|
||||
delete(targetAuth.Attributes, "note")
|
||||
} else {
|
||||
targetAuth.Metadata["note"] = trimmedNote
|
||||
targetAuth.Attributes["note"] = trimmedNote
|
||||
}
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
|
||||
@@ -229,6 +229,10 @@ func SynthesizeGeminiVirtualAuths(primary *coreauth.Auth, metadata map[string]an
|
||||
if priorityVal, hasPriority := primary.Attributes["priority"]; hasPriority && priorityVal != "" {
|
||||
attrs["priority"] = priorityVal
|
||||
}
|
||||
// Propagate note from primary auth to virtual auths
|
||||
if noteVal, hasNote := primary.Attributes["note"]; hasNote && noteVal != "" {
|
||||
attrs["note"] = noteVal
|
||||
}
|
||||
metadataCopy := map[string]any{
|
||||
"email": email,
|
||||
"project_id": projectID,
|
||||
|
||||
@@ -744,3 +744,200 @@ func TestBuildGeminiVirtualID(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizeGeminiVirtualAuths_NotePropagated(t *testing.T) {
|
||||
now := time.Now()
|
||||
primary := &coreauth.Auth{
|
||||
ID: "primary-id",
|
||||
Provider: "gemini-cli",
|
||||
Label: "test@example.com",
|
||||
Attributes: map[string]string{
|
||||
"source": "test-source",
|
||||
"path": "/path/to/auth",
|
||||
"priority": "5",
|
||||
"note": "my test note",
|
||||
},
|
||||
}
|
||||
metadata := map[string]any{
|
||||
"project_id": "proj-a, proj-b",
|
||||
"email": "test@example.com",
|
||||
"type": "gemini",
|
||||
}
|
||||
|
||||
virtuals := SynthesizeGeminiVirtualAuths(primary, metadata, now)
|
||||
|
||||
if len(virtuals) != 2 {
|
||||
t.Fatalf("expected 2 virtuals, got %d", len(virtuals))
|
||||
}
|
||||
|
||||
for i, v := range virtuals {
|
||||
if got := v.Attributes["note"]; got != "my test note" {
|
||||
t.Errorf("virtual %d: expected note %q, got %q", i, "my test note", got)
|
||||
}
|
||||
if got := v.Attributes["priority"]; got != "5" {
|
||||
t.Errorf("virtual %d: expected priority %q, got %q", i, "5", got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSynthesizeGeminiVirtualAuths_NoteAbsentWhenEmpty(t *testing.T) {
|
||||
now := time.Now()
|
||||
primary := &coreauth.Auth{
|
||||
ID: "primary-id",
|
||||
Provider: "gemini-cli",
|
||||
Label: "test@example.com",
|
||||
Attributes: map[string]string{
|
||||
"source": "test-source",
|
||||
"path": "/path/to/auth",
|
||||
},
|
||||
}
|
||||
metadata := map[string]any{
|
||||
"project_id": "proj-a, proj-b",
|
||||
"email": "test@example.com",
|
||||
"type": "gemini",
|
||||
}
|
||||
|
||||
virtuals := SynthesizeGeminiVirtualAuths(primary, metadata, now)
|
||||
|
||||
if len(virtuals) != 2 {
|
||||
t.Fatalf("expected 2 virtuals, got %d", len(virtuals))
|
||||
}
|
||||
|
||||
for i, v := range virtuals {
|
||||
if _, hasNote := v.Attributes["note"]; hasNote {
|
||||
t.Errorf("virtual %d: expected no note attribute when primary has no note", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_NoteParsing(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
note any
|
||||
want string
|
||||
hasValue bool
|
||||
}{
|
||||
{
|
||||
name: "valid string note",
|
||||
note: "hello world",
|
||||
want: "hello world",
|
||||
hasValue: true,
|
||||
},
|
||||
{
|
||||
name: "string note with whitespace",
|
||||
note: " trimmed note ",
|
||||
want: "trimmed note",
|
||||
hasValue: true,
|
||||
},
|
||||
{
|
||||
name: "empty string note",
|
||||
note: "",
|
||||
hasValue: false,
|
||||
},
|
||||
{
|
||||
name: "whitespace only note",
|
||||
note: " ",
|
||||
hasValue: false,
|
||||
},
|
||||
{
|
||||
name: "non-string note ignored",
|
||||
note: 12345,
|
||||
hasValue: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
authData := map[string]any{
|
||||
"type": "claude",
|
||||
"note": tt.note,
|
||||
}
|
||||
data, _ := json.Marshal(authData)
|
||||
errWriteFile := os.WriteFile(filepath.Join(tempDir, "auth.json"), data, 0644)
|
||||
if errWriteFile != nil {
|
||||
t.Fatalf("failed to write auth file: %v", errWriteFile)
|
||||
}
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, errSynthesize := synth.Synthesize(ctx)
|
||||
if errSynthesize != nil {
|
||||
t.Fatalf("unexpected error: %v", errSynthesize)
|
||||
}
|
||||
if len(auths) != 1 {
|
||||
t.Fatalf("expected 1 auth, got %d", len(auths))
|
||||
}
|
||||
|
||||
value, ok := auths[0].Attributes["note"]
|
||||
if tt.hasValue {
|
||||
if !ok {
|
||||
t.Fatal("expected note attribute to be set")
|
||||
}
|
||||
if value != tt.want {
|
||||
t.Fatalf("expected note %q, got %q", tt.want, value)
|
||||
}
|
||||
return
|
||||
}
|
||||
if ok {
|
||||
t.Fatalf("expected note attribute to be absent, got %q", value)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFileSynthesizer_Synthesize_MultiProjectGeminiWithNote(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
|
||||
authData := map[string]any{
|
||||
"type": "gemini",
|
||||
"email": "multi@example.com",
|
||||
"project_id": "project-a, project-b",
|
||||
"priority": 5,
|
||||
"note": "production keys",
|
||||
}
|
||||
data, _ := json.Marshal(authData)
|
||||
err := os.WriteFile(filepath.Join(tempDir, "gemini-multi.json"), data, 0644)
|
||||
if err != nil {
|
||||
t.Fatalf("failed to write auth file: %v", err)
|
||||
}
|
||||
|
||||
synth := NewFileSynthesizer()
|
||||
ctx := &SynthesisContext{
|
||||
Config: &config.Config{},
|
||||
AuthDir: tempDir,
|
||||
Now: time.Now(),
|
||||
IDGenerator: NewStableIDGenerator(),
|
||||
}
|
||||
|
||||
auths, err := synth.Synthesize(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
// Should have 3 auths: 1 primary (disabled) + 2 virtuals
|
||||
if len(auths) != 3 {
|
||||
t.Fatalf("expected 3 auths (1 primary + 2 virtuals), got %d", len(auths))
|
||||
}
|
||||
|
||||
primary := auths[0]
|
||||
if gotNote := primary.Attributes["note"]; gotNote != "production keys" {
|
||||
t.Errorf("expected primary note %q, got %q", "production keys", gotNote)
|
||||
}
|
||||
|
||||
// Verify virtuals inherit note
|
||||
for i := 1; i < len(auths); i++ {
|
||||
v := auths[i]
|
||||
if gotNote := v.Attributes["note"]; gotNote != "production keys" {
|
||||
t.Errorf("expected virtual %d note %q, got %q", i, "production keys", gotNote)
|
||||
}
|
||||
if gotPriority := v.Attributes["priority"]; gotPriority != "5" {
|
||||
t.Errorf("expected virtual %d priority %q, got %q", i, "5", gotPriority)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user