From c84612211caa882491da420892cab01ab2ecc78b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Tue, 7 Jul 2026 10:17:03 +0800 Subject: [PATCH] Add files via upload --- internal/config/config.go | 87 ++++++++++++++++++++++++++++------ internal/config/config_test.go | 73 ++++++++++++++++++++++++++++ 2 files changed, 146 insertions(+), 14 deletions(-) create mode 100644 internal/config/config_test.go diff --git a/internal/config/config.go b/internal/config/config.go index 750eab72..508052b5 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -455,15 +455,15 @@ type MultiAgentAPIUpdate struct { // RobotsConfig 机器人配置(企业微信、钉钉、飞书、微信 iLink、Telegram、Slack、Discord、QQ 等) type RobotsConfig struct { - Session RobotSessionConfig `yaml:"session,omitempty" json:"session,omitempty"` // 机器人会话隔离策略 - Wechat RobotWechatConfig `yaml:"wechat,omitempty" json:"wechat,omitempty"` // 微信(iLink 扫码绑定) - Wecom RobotWecomConfig `yaml:"wecom,omitempty" json:"wecom,omitempty"` // 企业微信 - Dingtalk RobotDingtalkConfig `yaml:"dingtalk,omitempty" json:"dingtalk,omitempty"` // 钉钉 - Lark RobotLarkConfig `yaml:"lark,omitempty" json:"lark,omitempty"` // 飞书 - Telegram RobotTelegramConfig `yaml:"telegram,omitempty" json:"telegram,omitempty"` // Telegram - Slack RobotSlackConfig `yaml:"slack,omitempty" json:"slack,omitempty"` // Slack - Discord RobotDiscordConfig `yaml:"discord,omitempty" json:"discord,omitempty"` // Discord - QQ RobotQQConfig `yaml:"qq,omitempty" json:"qq,omitempty"` // QQ 机器人 + Session RobotSessionConfig `yaml:"session,omitempty" json:"session,omitempty"` // 机器人会话隔离策略 + Wechat RobotWechatConfig `yaml:"wechat,omitempty" json:"wechat,omitempty"` // 微信(iLink 扫码绑定) + Wecom RobotWecomConfig `yaml:"wecom,omitempty" json:"wecom,omitempty"` // 企业微信 + Dingtalk RobotDingtalkConfig `yaml:"dingtalk,omitempty" json:"dingtalk,omitempty"` // 钉钉 + Lark RobotLarkConfig `yaml:"lark,omitempty" json:"lark,omitempty"` // 飞书 + Telegram RobotTelegramConfig `yaml:"telegram,omitempty" json:"telegram,omitempty"` // Telegram + Slack RobotSlackConfig `yaml:"slack,omitempty" json:"slack,omitempty"` // Slack + Discord RobotDiscordConfig `yaml:"discord,omitempty" json:"discord,omitempty"` // Discord + QQ RobotQQConfig `yaml:"qq,omitempty" json:"qq,omitempty"` // QQ 机器人 } // RobotWechatConfig 微信 iLink 机器人配置(个人微信 ClawBot / iLink 协议) @@ -540,9 +540,9 @@ type RobotTelegramConfig struct { // RobotSlackConfig Slack 机器人配置(Socket Mode,无需公网回调) type RobotSlackConfig struct { - Enabled bool `yaml:"enabled" json:"enabled"` - BotToken string `yaml:"bot_token" json:"bot_token"` // xoxb- - AppToken string `yaml:"app_token" json:"app_token"` // xapp-(connections:write) + Enabled bool `yaml:"enabled" json:"enabled"` + BotToken string `yaml:"bot_token" json:"bot_token"` // xoxb- + AppToken string `yaml:"app_token" json:"app_token"` // xapp-(connections:write) } // RobotDiscordConfig Discord 机器人配置(Gateway WebSocket) @@ -1074,11 +1074,11 @@ func PersistAuthPassword(path, password string) error { if strings.HasPrefix(strings.TrimSpace(line), "password:") { prefix := line[:len(line)-len(strings.TrimLeft(line, " "))] comment := "" - if idx := strings.Index(line, "#"); idx >= 0 { + if idx := yamlLineCommentIndex(line); idx >= 0 { comment = strings.TrimRight(line[idx:], " ") } - newLine := fmt.Sprintf("%spassword: %s", prefix, password) + newLine := fmt.Sprintf("%spassword: %s", prefix, quoteYAMLString(password)) if comment != "" { if !strings.HasPrefix(comment, " ") { newLine += " " @@ -1093,6 +1093,65 @@ func PersistAuthPassword(path, password string) error { return os.WriteFile(path, []byte(strings.Join(lines, "\n")), 0644) } +func quoteYAMLString(value string) string { + node := yaml.Node{ + Kind: yaml.ScalarNode, + Tag: "!!str", + Style: yaml.DoubleQuotedStyle, + Value: value, + } + data, err := yaml.Marshal(&node) + if err != nil { + return strconv.Quote(value) + } + return strings.TrimSuffix(string(data), "\n") +} + +func yamlLineCommentIndex(line string) int { + inSingleQuote := false + inDoubleQuote := false + escaped := false + + for i, r := range line { + if inDoubleQuote { + if escaped { + escaped = false + continue + } + if r == '\\' { + escaped = true + continue + } + if r == '"' { + inDoubleQuote = false + } + continue + } + if inSingleQuote { + if r == '\'' { + inSingleQuote = false + } + continue + } + + switch r { + case '"': + inDoubleQuote = true + case '\'': + inSingleQuote = true + case '#': + if i == 0 || isYAMLWhitespace(line[i-1]) { + return i + } + } + } + return -1 +} + +func isYAMLWhitespace(b byte) bool { + return b == ' ' || b == '\t' +} + func PrintGeneratedPasswordWarning(password string, persisted bool, persistErr string) { if strings.TrimSpace(password) == "" { return diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 00000000..e3b652d3 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,73 @@ +package config + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestPersistAuthPasswordQuotesYAMLSpecialCharacters(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + initial := strings.Join([]string{ + "server:", + " host: 0.0.0.0", + "auth:", + " password: old-password # Web 登录密码", + " session_duration_hours: 12", + "log:", + " level: info", + "", + }, "\n") + if err := os.WriteFile(path, []byte(initial), 0644); err != nil { + t.Fatalf("write config: %v", err) + } + + want := `@abc:def # still password` + if err := PersistAuthPassword(path, want); err != nil { + t.Fatalf("PersistAuthPassword: %v", err) + } + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read config: %v", err) + } + if !strings.Contains(string(data), `password: "@abc:def # still password" # Web 登录密码`) { + t.Fatalf("password was not safely quoted or comment was not preserved:\n%s", data) + } + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load after PersistAuthPassword: %v", err) + } + if cfg.Auth.Password != want { + t.Fatalf("Auth.Password = %q, want %q", cfg.Auth.Password, want) + } +} + +func TestPersistAuthPasswordDoesNotTreatQuotedHashAsComment(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "config.yaml") + initial := strings.Join([]string{ + "auth:", + ` password: "old#password"`, + " session_duration_hours: 12", + "", + }, "\n") + if err := os.WriteFile(path, []byte(initial), 0644); err != nil { + t.Fatalf("write config: %v", err) + } + + if err := PersistAuthPassword(path, "new-password"); err != nil { + t.Fatalf("PersistAuthPassword: %v", err) + } + + data, err := os.ReadFile(path) + if err != nil { + t.Fatalf("read config: %v", err) + } + if strings.Contains(string(data), "#password") { + t.Fatalf("old quoted password fragment was incorrectly preserved as a comment:\n%s", data) + } +}