mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-06-17 14:48:34 +00:00
30708ae79d
* feat(auth): support named chatgpt oauth providers - add provider-scoped ChatGPT OAuth routes and CLI support - persist refresh tokens per provider and reject provider-type collisions - wire provider OAuth setup flows in the dashboard and setup UI Refs #448 * feat(agent): add chatgpt oauth account routing - add agent other_config routing for manual and round-robin selection - reuse routed provider resolution across resolver and pending loaders - add router, parser, and agent advanced dialog coverage for multi-account use Refs #448 * docs(api): describe chatgpt oauth routing - document named-provider ChatGPT OAuth auth routes - describe agent-side account routing and round-robin behavior - update OpenAPI agent config schema and provider type enum Refs #448 * fix(store): add missing agent key context helpers * feat(ui): clarify chatgpt oauth account setup and routing * docs(providers): align chatgpt oauth alias examples * feat(agent): add codex pool activity dashboard * fix(providers): harden codex oauth alias setup * feat(codex-pool): improve routing dashboard UX - redesign the Codex/OpenAI pool page around saved-pool checkpoints and live evidence - add clearer selection, attention, and recent-proof states for pool members - make the lower panels fill the remaining desktop viewport while staying responsive * fix(store): resolve context helper merge duplication * feat(oauth): add codex pool quota and observation APIs - add quota inspection and observation endpoints for ChatGPT Subscription (OAuth) providers - teach codex routing to surface pool activity, observation metadata, and quota-aware readiness - extend tests and HTTP docs/OpenAPI for the new pool monitoring flows * feat(web): add codex pool quota monitor and controls - add provider quota fetching, readiness badges, and live routing evidence on the account pool page - redesign pool setup and activity panels for multi-account management with localized copy updates - keep the live monitor internally scrollable and compact the account cards for better viewport fit * fix(web): clarify pool routing labels - rename the recent request badge from Direct to Selected - restore compact quota bars in the live pool cards * feat(codex-pool): add runtime health dashboard - derive per-provider success and failure health from routed Codex traces - surface routing, quota, and recent request evidence in the pool UI - align provider alias guidance and owner access with the dashboard role model * docs(auth): document tenant scoping and key roles * fix(auth): harden tenant and codex pool access control * fix(providers): align codex pool runtime defaults * feat(ui): tighten codex pool responsive layout * feat(chatgpt-oauth): refine codex pool management UX * feat(chatgpt-oauth): surface quota bars on provider pages - add compact quota bars to Codex provider rows and provider detail - fetch quota only for ready visible provider rows and ready detail aliases - fix managed-member detail visibility and tighten provider locale copy
123 lines
2.9 KiB
Go
123 lines
2.9 KiB
Go
package http
|
|
|
|
import (
|
|
"strings"
|
|
)
|
|
|
|
var identityFieldPrefixes = []string{
|
|
"- **Name:**",
|
|
"- **Creature:**",
|
|
"- **Purpose:**",
|
|
"- **Vibe:**",
|
|
"- **Emoji:**",
|
|
"- **Avatar:**",
|
|
"Name:",
|
|
"Creature:",
|
|
"Purpose:",
|
|
"Vibe:",
|
|
"Emoji:",
|
|
"Avatar:",
|
|
}
|
|
|
|
// extractIdentityName extracts the Name field from IDENTITY.md content.
|
|
// Accepts only an inline Name value and ignores markdown field spillover.
|
|
func extractIdentityName(content string) string {
|
|
if content == "" {
|
|
return ""
|
|
}
|
|
|
|
for rawLine := range strings.SplitSeq(content, "\n") {
|
|
line := strings.TrimSpace(rawLine)
|
|
switch {
|
|
case strings.HasPrefix(line, "- **Name:**"):
|
|
return normalizeIdentityName(strings.TrimSpace(strings.TrimPrefix(line, "- **Name:**")))
|
|
case strings.HasPrefix(line, "Name:"):
|
|
return normalizeIdentityName(strings.TrimSpace(strings.TrimPrefix(line, "Name:")))
|
|
}
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func normalizeIdentityName(value string) string {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" || looksLikeIdentityField(value) {
|
|
return ""
|
|
}
|
|
|
|
for {
|
|
next := trimMarkdownWrapper(value)
|
|
if next == value {
|
|
return value
|
|
}
|
|
value = strings.TrimSpace(next)
|
|
if value == "" || looksLikeIdentityField(value) {
|
|
return ""
|
|
}
|
|
}
|
|
}
|
|
|
|
func looksLikeIdentityField(value string) bool {
|
|
for _, prefix := range identityFieldPrefixes {
|
|
if strings.HasPrefix(value, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func trimMarkdownWrapper(value string) string {
|
|
wrappers := [][2]string{
|
|
{"**", "**"},
|
|
{"__", "__"},
|
|
{"`", "`"},
|
|
{"*", "*"},
|
|
{"_", "_"},
|
|
}
|
|
for _, wrapper := range wrappers {
|
|
if strings.HasPrefix(value, wrapper[0]) && strings.HasSuffix(value, wrapper[1]) && len(value) > len(wrapper[0])+len(wrapper[1]) {
|
|
return strings.TrimSpace(strings.TrimSuffix(strings.TrimPrefix(value, wrapper[0]), wrapper[1]))
|
|
}
|
|
}
|
|
return value
|
|
}
|
|
|
|
// suffixString returns the last n runes of s.
|
|
func suffixString(s string, n int) string {
|
|
runes := []rune(s)
|
|
if len(runes) <= n {
|
|
return s
|
|
}
|
|
return string(runes[len(runes)-n:])
|
|
}
|
|
|
|
// truncateUTF8 truncates s to at most maxLen runes, appending "…" if truncated.
|
|
func truncateUTF8(s string, maxLen int) string {
|
|
runes := []rune(s)
|
|
if len(runes) <= maxLen {
|
|
return s
|
|
}
|
|
return string(runes[:maxLen]) + "…"
|
|
}
|
|
|
|
// parseFileResponse extracts file contents and frontmatter from XML-tagged LLM output.
|
|
// Frontmatter is stored under the special key "__frontmatter__".
|
|
func parseFileResponse(content string) map[string]string {
|
|
files := make(map[string]string)
|
|
matches := fileTagRe.FindAllStringSubmatch(content, -1)
|
|
for _, m := range matches {
|
|
name := strings.TrimSpace(m[1])
|
|
body := strings.TrimSpace(m[2])
|
|
if name != "" && body != "" {
|
|
files[name] = body
|
|
}
|
|
}
|
|
// Extract frontmatter tag if present
|
|
if fm := frontmatterTagRe.FindStringSubmatch(content); len(fm) > 1 {
|
|
if trimmed := strings.TrimSpace(fm[1]); trimmed != "" {
|
|
files[frontmatterKey] = trimmed
|
|
}
|
|
}
|
|
return files
|
|
}
|