mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-08-15 23:50:32 +02:00
Delete internal directory
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -1,207 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestEnsureLocalConfigCreatesFromExample(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
examplePath := filepath.Join(dir, "config.example.yaml")
|
||||
configPath := filepath.Join(dir, "config.yaml")
|
||||
|
||||
example := []byte(`auth:
|
||||
session_duration_hours: 12
|
||||
server:
|
||||
host: 127.0.0.1
|
||||
port: 8080
|
||||
`)
|
||||
if err := os.WriteFile(examplePath, example, 0644); err != nil {
|
||||
t.Fatalf("write example: %v", err)
|
||||
}
|
||||
|
||||
result, err := EnsureLocalConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureLocalConfig: %v", err)
|
||||
}
|
||||
if !result.Created {
|
||||
t.Fatal("Created = false, want true")
|
||||
}
|
||||
if result.ExamplePath != examplePath {
|
||||
t.Fatalf("ExamplePath = %q, want %q", result.ExamplePath, examplePath)
|
||||
}
|
||||
|
||||
cfg, err := Load(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("Load generated config: %v", err)
|
||||
}
|
||||
if cfg.Auth.SessionDurationHours != 12 {
|
||||
t.Fatalf("SessionDurationHours = %d, want 12", cfg.Auth.SessionDurationHours)
|
||||
}
|
||||
|
||||
second, err := EnsureLocalConfig(configPath)
|
||||
if err != nil {
|
||||
t.Fatalf("EnsureLocalConfig existing: %v", err)
|
||||
}
|
||||
if second.Created {
|
||||
t.Fatal("Created = true for existing config, want false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadIgnoresLegacyAuthPasswordField(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
initial := strings.Join([]string{
|
||||
"auth:",
|
||||
` password: "legacy-password"`,
|
||||
" session_duration_hours: 12",
|
||||
"server:",
|
||||
" host: 127.0.0.1",
|
||||
" port: 8080",
|
||||
"",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(path, []byte(initial), 0644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.Auth.SessionDurationHours != 12 {
|
||||
t.Fatalf("SessionDurationHours = %d, want 12", cfg.Auth.SessionDurationHours)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHitlAuditModelEffectiveFallsBackToMainConfig(t *testing.T) {
|
||||
main := OpenAIConfig{
|
||||
Provider: "openai",
|
||||
BaseURL: "https://api.example.com/v1",
|
||||
APIKey: "main-key",
|
||||
Model: "large-model",
|
||||
}
|
||||
|
||||
got := (HitlConfig{
|
||||
AuditModel: OpenAIConfig{Model: "small-reviewer"},
|
||||
}).AuditModelEffective(main)
|
||||
|
||||
if got.Provider != main.Provider || got.BaseURL != main.BaseURL || got.APIKey != main.APIKey {
|
||||
t.Fatalf("expected provider/base_url/api_key to inherit main config, got %+v", got)
|
||||
}
|
||||
if got.Model != "small-reviewer" {
|
||||
t.Fatalf("expected audit model override, got %q", got.Model)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadUsesAIDefaultChannelAsRuntimeOpenAI(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "config.yaml")
|
||||
initial := strings.Join([]string{
|
||||
"ai:",
|
||||
" default_channel: deepseek",
|
||||
" channels:",
|
||||
" qwen:",
|
||||
" name: Qwen",
|
||||
" provider: openai_compatible",
|
||||
" base_url: https://dashscope.example/v1",
|
||||
" api_key: qwen-key",
|
||||
" model: qwen-max",
|
||||
" deepseek:",
|
||||
" name: DeepSeek",
|
||||
" provider: openai_compatible",
|
||||
" base_url: https://deepseek.example/v1",
|
||||
" api_key: deepseek-key",
|
||||
" model: deepseek-chat",
|
||||
" max_total_tokens: 64000",
|
||||
"server:",
|
||||
" host: 127.0.0.1",
|
||||
" port: 8080",
|
||||
"",
|
||||
}, "\n")
|
||||
if err := os.WriteFile(path, []byte(initial), 0644); err != nil {
|
||||
t.Fatalf("write config: %v", err)
|
||||
}
|
||||
|
||||
cfg, err := Load(path)
|
||||
if err != nil {
|
||||
t.Fatalf("Load: %v", err)
|
||||
}
|
||||
if cfg.OpenAI.Model != "deepseek-chat" || cfg.OpenAI.APIKey != "deepseek-key" || cfg.OpenAI.MaxTotalTokens != 64000 {
|
||||
t.Fatalf("runtime OpenAI config did not follow ai.default_channel: %+v", cfg.OpenAI)
|
||||
}
|
||||
oa, id, ok := cfg.ResolveAIChannel("qwen")
|
||||
if !ok || id != "qwen" || oa.Model != "qwen-max" || oa.APIKey != "qwen-key" {
|
||||
t.Fatalf("ResolveAIChannel(qwen) = (%+v, %q, %v)", oa, id, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizationUserIntentLedgerRunesEffective(t *testing.T) {
|
||||
var zero MultiAgentEinoMiddlewareConfig
|
||||
if got := zero.SummarizationUserIntentLedgerMaxRunesEffective(); got != DefaultSummarizationUserIntentLedgerMaxRunes {
|
||||
t.Fatalf("default ledger max runes = %d, want %d", got, DefaultSummarizationUserIntentLedgerMaxRunes)
|
||||
}
|
||||
if got := zero.SummarizationUserIntentLedgerEntryMaxRunesEffective(); got != DefaultSummarizationUserIntentLedgerEntryMaxRunes {
|
||||
t.Fatalf("default ledger entry max runes = %d, want %d", got, DefaultSummarizationUserIntentLedgerEntryMaxRunes)
|
||||
}
|
||||
|
||||
custom := MultiAgentEinoMiddlewareConfig{
|
||||
SummarizationUserIntentLedgerMaxRunes: 12345,
|
||||
SummarizationUserIntentLedgerEntryMaxRunes: 2345,
|
||||
}
|
||||
if got := custom.SummarizationUserIntentLedgerMaxRunesEffective(); got != 12345 {
|
||||
t.Fatalf("custom ledger max runes = %d", got)
|
||||
}
|
||||
if got := custom.SummarizationUserIntentLedgerEntryMaxRunesEffective(); got != 2345 {
|
||||
t.Fatalf("custom ledger entry max runes = %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSummarizationOutputReserveTokensEffective(t *testing.T) {
|
||||
var zero MultiAgentEinoMiddlewareConfig
|
||||
if got := zero.SummarizationOutputReserveTokensEffective(); got != DefaultSummarizationOutputReserveTokens {
|
||||
t.Fatalf("default output reserve = %d, want %d", got, DefaultSummarizationOutputReserveTokens)
|
||||
}
|
||||
custom := MultiAgentEinoMiddlewareConfig{SummarizationOutputReserveTokens: 4096}
|
||||
if got := custom.SummarizationOutputReserveTokensEffective(); got != 4096 {
|
||||
t.Fatalf("custom output reserve = %d", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpenAIOutputLimitValidation(t *testing.T) {
|
||||
if got := (OpenAIConfig{}).MaxCompletionTokensEffective(); got != DefaultMaxCompletionTokens {
|
||||
t.Fatalf("max completion default=%d", got)
|
||||
}
|
||||
if err := validateOpenAIOutputLimits(OpenAIConfig{MaxCompletionTokens: -1}); err == nil {
|
||||
t.Fatal("negative completion limit must fail")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLatestUserMessageRunesEffective(t *testing.T) {
|
||||
var zero MultiAgentEinoMiddlewareConfig
|
||||
if got := zero.LatestUserMessageMaxRunesEffective(); got != DefaultLatestUserMessageMaxRunes {
|
||||
t.Fatalf("default latest user max runes = %d, want %d", got, DefaultLatestUserMessageMaxRunes)
|
||||
}
|
||||
if got := zero.LatestUserMessageHeadRunesEffective(); got != DefaultLatestUserMessageHeadRunes {
|
||||
t.Fatalf("default latest user head runes = %d, want %d", got, DefaultLatestUserMessageHeadRunes)
|
||||
}
|
||||
if got := zero.LatestUserMessageTailRunesEffective(); got != DefaultLatestUserMessageTailRunes {
|
||||
t.Fatalf("default latest user tail runes = %d, want %d", got, DefaultLatestUserMessageTailRunes)
|
||||
}
|
||||
|
||||
custom := MultiAgentEinoMiddlewareConfig{
|
||||
LatestUserMessageMaxRunes: 100,
|
||||
LatestUserMessageHeadRunes: 40,
|
||||
LatestUserMessageTailRunes: 60,
|
||||
}
|
||||
if got := custom.LatestUserMessageMaxRunesEffective(); got != 100 {
|
||||
t.Fatalf("custom latest user max runes = %d", got)
|
||||
}
|
||||
if got := custom.LatestUserMessageHeadRunesEffective(); got != 40 {
|
||||
t.Fatalf("custom latest user head runes = %d", got)
|
||||
}
|
||||
if got := custom.LatestUserMessageTailRunesEffective(); got != 60 {
|
||||
t.Fatalf("custom latest user tail runes = %d", got)
|
||||
}
|
||||
}
|
||||
@@ -1,66 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// expandEnvVar 展开字符串中的 ${VAR} 和 ${VAR:-default} 环境变量引用。
|
||||
// 与官方 MCP 配置格式一致(Claude Desktop / Cursor / VS Code 均支持此语法)。
|
||||
func expandEnvVar(s string) string {
|
||||
var b strings.Builder
|
||||
i := 0
|
||||
for i < len(s) {
|
||||
// 查找 ${
|
||||
idx := strings.Index(s[i:], "${")
|
||||
if idx < 0 {
|
||||
b.WriteString(s[i:])
|
||||
break
|
||||
}
|
||||
b.WriteString(s[i : i+idx])
|
||||
i += idx + 2 // skip ${
|
||||
|
||||
// 查找对应的 }
|
||||
end := strings.IndexByte(s[i:], '}')
|
||||
if end < 0 {
|
||||
// 没有 },原样保留
|
||||
b.WriteString("${")
|
||||
continue
|
||||
}
|
||||
expr := s[i : i+end]
|
||||
i += end + 1 // skip }
|
||||
|
||||
// 解析 VAR:-default
|
||||
varName := expr
|
||||
defaultVal := ""
|
||||
hasDefault := false
|
||||
if colonIdx := strings.Index(expr, ":-"); colonIdx >= 0 {
|
||||
varName = expr[:colonIdx]
|
||||
defaultVal = expr[colonIdx+2:]
|
||||
hasDefault = true
|
||||
}
|
||||
|
||||
val := os.Getenv(varName)
|
||||
if val == "" && hasDefault {
|
||||
val = defaultVal
|
||||
}
|
||||
b.WriteString(val)
|
||||
}
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// ExpandConfigEnv 展开 ExternalMCPServerConfig 中所有支持环境变量的字段。
|
||||
// 展开范围:Command、Args、Env values、URL、Headers values。
|
||||
func ExpandConfigEnv(cfg *ExternalMCPServerConfig) {
|
||||
cfg.Command = expandEnvVar(cfg.Command)
|
||||
for i, arg := range cfg.Args {
|
||||
cfg.Args[i] = expandEnvVar(arg)
|
||||
}
|
||||
for k, v := range cfg.Env {
|
||||
cfg.Env[k] = expandEnvVar(v)
|
||||
}
|
||||
cfg.URL = expandEnvVar(cfg.URL)
|
||||
for k, v := range cfg.Headers {
|
||||
cfg.Headers[k] = expandEnvVar(v)
|
||||
}
|
||||
}
|
||||
@@ -1,81 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestExpandEnvVar(t *testing.T) {
|
||||
os.Setenv("TEST_MCP_VAR", "hello")
|
||||
os.Setenv("TEST_MCP_PATH", "/usr/local/bin")
|
||||
defer os.Unsetenv("TEST_MCP_VAR")
|
||||
defer os.Unsetenv("TEST_MCP_PATH")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
expect string
|
||||
}{
|
||||
{"plain string", "no vars here", "no vars here"},
|
||||
{"empty string", "", ""},
|
||||
{"simple var", "${TEST_MCP_VAR}", "hello"},
|
||||
{"var in middle", "prefix-${TEST_MCP_VAR}-suffix", "prefix-hello-suffix"},
|
||||
{"multiple vars", "${TEST_MCP_PATH}/${TEST_MCP_VAR}", "/usr/local/bin/hello"},
|
||||
{"missing var empty", "${NONEXISTENT_MCP_VAR_XYZ}", ""},
|
||||
{"default value used", "${NONEXISTENT_MCP_VAR_XYZ:-fallback}", "fallback"},
|
||||
{"default not used", "${TEST_MCP_VAR:-unused}", "hello"},
|
||||
{"default with path", "${NONEXISTENT_MCP_VAR_XYZ:-/tmp/default}", "/tmp/default"},
|
||||
{"unclosed brace", "${UNCLOSED", "${UNCLOSED"},
|
||||
{"dollar without brace", "$PLAIN", "$PLAIN"},
|
||||
{"empty var name", "${}", ""},
|
||||
{"default empty var", "${:-default}", "default"},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := expandEnvVar(tt.input)
|
||||
if got != tt.expect {
|
||||
t.Errorf("expandEnvVar(%q) = %q, want %q", tt.input, got, tt.expect)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestExpandConfigEnv(t *testing.T) {
|
||||
os.Setenv("TEST_MCP_CMD", "python3")
|
||||
os.Setenv("TEST_MCP_TOKEN", "secret123")
|
||||
defer os.Unsetenv("TEST_MCP_CMD")
|
||||
defer os.Unsetenv("TEST_MCP_TOKEN")
|
||||
|
||||
cfg := &ExternalMCPServerConfig{
|
||||
Command: "${TEST_MCP_CMD}",
|
||||
Args: []string{"--token", "${TEST_MCP_TOKEN}", "${MISSING:-default_arg}"},
|
||||
Env: map[string]string{"API_KEY": "${TEST_MCP_TOKEN}", "LEVEL": "${MISSING:-INFO}"},
|
||||
URL: "https://${MISSING:-example.com}/mcp",
|
||||
Headers: map[string]string{"Authorization": "Bearer ${TEST_MCP_TOKEN}"},
|
||||
}
|
||||
|
||||
ExpandConfigEnv(cfg)
|
||||
|
||||
if cfg.Command != "python3" {
|
||||
t.Errorf("Command = %q, want %q", cfg.Command, "python3")
|
||||
}
|
||||
if cfg.Args[1] != "secret123" {
|
||||
t.Errorf("Args[1] = %q, want %q", cfg.Args[1], "secret123")
|
||||
}
|
||||
if cfg.Args[2] != "default_arg" {
|
||||
t.Errorf("Args[2] = %q, want %q", cfg.Args[2], "default_arg")
|
||||
}
|
||||
if cfg.Env["API_KEY"] != "secret123" {
|
||||
t.Errorf("Env[API_KEY] = %q, want %q", cfg.Env["API_KEY"], "secret123")
|
||||
}
|
||||
if cfg.Env["LEVEL"] != "INFO" {
|
||||
t.Errorf("Env[LEVEL] = %q, want %q", cfg.Env["LEVEL"], "INFO")
|
||||
}
|
||||
if cfg.URL != "https://example.com/mcp" {
|
||||
t.Errorf("URL = %q, want %q", cfg.URL, "https://example.com/mcp")
|
||||
}
|
||||
if cfg.Headers["Authorization"] != "Bearer secret123" {
|
||||
t.Errorf("Headers[Authorization] = %q, want %q", cfg.Headers["Authorization"], "Bearer secret123")
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultHitlAuditAgentPromptIncludesPrioritizedRules(t *testing.T) {
|
||||
prompt := DefaultHitlAuditAgentPrompt()
|
||||
for _, want := range []string{
|
||||
"如果同时命中 reject 和 approve,必须 reject",
|
||||
"修改/重置任意用户或管理员密码",
|
||||
"修改/创建/删除用户、角色、权限",
|
||||
"停止、禁用、重启业务服务",
|
||||
"命中规则:...",
|
||||
} {
|
||||
if !strings.Contains(prompt, want) {
|
||||
t.Fatalf("default approval prompt missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDefaultHitlAuditAgentPromptReviewEditKeepsEditedArguments(t *testing.T) {
|
||||
prompt := DefaultHitlAuditAgentPromptReviewEdit()
|
||||
if !strings.Contains(prompt, `"editedArguments":{...}`) {
|
||||
t.Fatal("review-edit prompt must preserve editedArguments output")
|
||||
}
|
||||
if !strings.Contains(prompt, "命中规则:...") {
|
||||
t.Fatal("review-edit prompt must require a matched rule")
|
||||
}
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestValidateWecomConfig(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg RobotWecomConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "disabled without token",
|
||||
cfg: RobotWecomConfig{Enabled: false, Token: ""},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "enabled with token",
|
||||
cfg: RobotWecomConfig{Enabled: true, Token: "secret"},
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
name: "enabled without token",
|
||||
cfg: RobotWecomConfig{Enabled: true, Token: ""},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "enabled with whitespace token",
|
||||
cfg: RobotWecomConfig{Enabled: true, Token: " "},
|
||||
wantErr: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tt := tt
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
err := ValidateWecomConfig(tt.cfg)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Fatalf("ValidateWecomConfig() error = %v, wantErr %v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateRobotAuthorization(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
cfg RobotAuthorizationConfig
|
||||
wantErr bool
|
||||
}{
|
||||
{name: "default user binding", cfg: RobotAuthorizationConfig{}, wantErr: false},
|
||||
{name: "explicit user binding", cfg: RobotAuthorizationConfig{Mode: RobotAuthModeUserBinding}, wantErr: false},
|
||||
{name: "service account", cfg: RobotAuthorizationConfig{Mode: RobotAuthModeServiceAccount, ServiceUserID: "svc-1", AllowedExternalUsers: []string{"t:x|u:y"}}, wantErr: false},
|
||||
{name: "missing service user", cfg: RobotAuthorizationConfig{Mode: RobotAuthModeServiceAccount, AllowedExternalUsers: []string{"t:x|u:y"}}, wantErr: true},
|
||||
{name: "admin allowed with exact sender", cfg: RobotAuthorizationConfig{Mode: RobotAuthModeServiceAccount, ServiceUserID: "admin", AllowedExternalUsers: []string{"t:x|u:y"}}, wantErr: false},
|
||||
{name: "allowlist required", cfg: RobotAuthorizationConfig{Mode: RobotAuthModeServiceAccount, ServiceUserID: "svc-1"}, wantErr: true},
|
||||
{name: "wildcard forbidden", cfg: RobotAuthorizationConfig{Mode: RobotAuthModeServiceAccount, ServiceUserID: "svc-1", AllowedExternalUsers: []string{"*"}}, wantErr: true},
|
||||
{name: "unknown mode", cfg: RobotAuthorizationConfig{Mode: "open"}, wantErr: true},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if err := ValidateRobotAuthorization(tt.cfg, "robots.lark"); (err != nil) != tt.wantErr {
|
||||
t.Fatalf("ValidateRobotAuthorization() error=%v wantErr=%v", err, tt.wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,60 +0,0 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
// MainWebUIUsesHTTPS 判断主 Web UI 是否以 HTTPS 监听(与 internal/app.prepareMainServerTLS 前置条件一致)。
|
||||
func MainWebUIUsesHTTPS(s *ServerConfig) bool {
|
||||
if s == nil {
|
||||
return false
|
||||
}
|
||||
if s.TLSEnabled {
|
||||
return true
|
||||
}
|
||||
if s.TLSAutoSelfSign {
|
||||
return true
|
||||
}
|
||||
cert := strings.TrimSpace(s.TLSCertPath)
|
||||
key := strings.TrimSpace(s.TLSKeyPath)
|
||||
return cert != "" && key != ""
|
||||
}
|
||||
|
||||
// ServerHTTPRedirectEnabled 是否在主站启用 HTTPS 时把明文 HTTP 请求重定向到 HTTPS(默认开启)。
|
||||
func ServerHTTPRedirectEnabled(s *ServerConfig) bool {
|
||||
if s == nil || !MainWebUIUsesHTTPS(s) {
|
||||
return false
|
||||
}
|
||||
if s.TLSHTTPRedirect == nil {
|
||||
return true
|
||||
}
|
||||
return *s.TLSHTTPRedirect
|
||||
}
|
||||
|
||||
// ApplyDevHTTPSBootstrap 供 --https / 一键脚本使用:强制开启主站 TLS。
|
||||
// 若已配置 tls_cert_path 与 tls_key_path 则仅用 PEM,不开启自签;否则启用 tls_auto_self_sign(内存证书,仅本地测试)。
|
||||
func ApplyDevHTTPSBootstrap(cfg *Config) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
cfg.Server.TLSEnabled = true
|
||||
cert := strings.TrimSpace(cfg.Server.TLSCertPath)
|
||||
key := strings.TrimSpace(cfg.Server.TLSKeyPath)
|
||||
if cert != "" && key != "" {
|
||||
cfg.Server.TLSAutoSelfSign = false
|
||||
return
|
||||
}
|
||||
cfg.Server.TLSAutoSelfSign = true
|
||||
}
|
||||
|
||||
// ApplyPlainHTTPBootstrap 供 --http / 一键脚本使用:强制主站使用明文 HTTP。
|
||||
// 它会覆盖配置文件中的 TLS 开关、自签证书以及证书路径,避免 --http 仍被配置中的 HTTPS 选项重新启用。
|
||||
func ApplyPlainHTTPBootstrap(cfg *Config) {
|
||||
if cfg == nil {
|
||||
return
|
||||
}
|
||||
cfg.Server.TLSEnabled = false
|
||||
cfg.Server.TLSAutoSelfSign = false
|
||||
cfg.Server.TLSCertPath = ""
|
||||
cfg.Server.TLSKeyPath = ""
|
||||
disabled := false
|
||||
cfg.Server.TLSHTTPRedirect = &disabled
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestApplyPlainHTTPBootstrapDisablesConfiguredTLS(t *testing.T) {
|
||||
enabled := true
|
||||
cfg := &Config{
|
||||
Server: ServerConfig{
|
||||
TLSEnabled: true,
|
||||
TLSAutoSelfSign: true,
|
||||
TLSCertPath: "/tmp/server.crt",
|
||||
TLSKeyPath: "/tmp/server.key",
|
||||
TLSHTTPRedirect: &enabled,
|
||||
},
|
||||
}
|
||||
|
||||
ApplyPlainHTTPBootstrap(cfg)
|
||||
|
||||
if MainWebUIUsesHTTPS(&cfg.Server) {
|
||||
t.Fatal("expected --http bootstrap to disable main web UI HTTPS")
|
||||
}
|
||||
if ServerHTTPRedirectEnabled(&cfg.Server) {
|
||||
t.Fatal("expected --http bootstrap to disable HTTP to HTTPS redirect")
|
||||
}
|
||||
if cfg.Server.TLSCertPath != "" || cfg.Server.TLSKeyPath != "" {
|
||||
t.Fatalf("expected TLS cert paths to be cleared, got cert=%q key=%q", cfg.Server.TLSCertPath, cfg.Server.TLSKeyPath)
|
||||
}
|
||||
if cfg.Server.TLSHTTPRedirect == nil || *cfg.Server.TLSHTTPRedirect {
|
||||
t.Fatal("expected TLSHTTPRedirect to be explicitly disabled")
|
||||
}
|
||||
}
|
||||
@@ -1,111 +0,0 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReloadSecurityToolsFromDir(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
toolsDir := filepath.Join(root, "tools")
|
||||
if err := os.MkdirAll(toolsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
configPath := filepath.Join(root, "config.yaml")
|
||||
if err := os.WriteFile(configPath, []byte(`security:
|
||||
tools_dir: tools
|
||||
tools:
|
||||
- name: inline-only
|
||||
command: inline-cmd
|
||||
enabled: true
|
||||
description: inline tool
|
||||
`), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
writeTool := func(name, command string) {
|
||||
t.Helper()
|
||||
content := "name: " + name + "\ncommand: " + command + "\nenabled: true\ndescription: test\n"
|
||||
if err := os.WriteFile(filepath.Join(toolsDir, name+".yaml"), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
writeTool("alpha", "alpha-cmd")
|
||||
|
||||
cfg := &Config{
|
||||
Security: SecurityConfig{
|
||||
ToolsDir: "tools",
|
||||
Tools: []ToolConfig{
|
||||
{Name: "stale", Command: "stale-cmd", Enabled: true, Description: "should be removed"},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if err := ReloadSecurityToolsFromDir(cfg, configPath); err != nil {
|
||||
t.Fatalf("reload: %v", err)
|
||||
}
|
||||
if len(cfg.Security.Tools) != 2 {
|
||||
t.Fatalf("expected 2 tools, got %d", len(cfg.Security.Tools))
|
||||
}
|
||||
|
||||
names := map[string]string{}
|
||||
for _, tool := range cfg.Security.Tools {
|
||||
names[tool.Name] = tool.Command
|
||||
}
|
||||
if names["alpha"] != "alpha-cmd" {
|
||||
t.Fatalf("alpha tool missing or wrong command: %#v", names)
|
||||
}
|
||||
if names["inline-only"] != "inline-cmd" {
|
||||
t.Fatalf("inline-only tool missing: %#v", names)
|
||||
}
|
||||
if _, ok := names["stale"]; ok {
|
||||
t.Fatal("stale in-memory tool should not survive reload")
|
||||
}
|
||||
|
||||
writeTool("beta", "beta-cmd")
|
||||
if err := ReloadSecurityToolsFromDir(cfg, configPath); err != nil {
|
||||
t.Fatalf("second reload: %v", err)
|
||||
}
|
||||
if len(cfg.Security.Tools) != 3 {
|
||||
t.Fatalf("expected 3 tools after add, got %d", len(cfg.Security.Tools))
|
||||
}
|
||||
foundBeta := false
|
||||
for _, tool := range cfg.Security.Tools {
|
||||
if tool.Name == "beta" {
|
||||
foundBeta = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !foundBeta {
|
||||
t.Fatal("beta tool not found after second reload")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMergeToolsFromDir_DirOverridesInline(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
toolsDir := filepath.Join(root, "tools")
|
||||
if err := os.MkdirAll(toolsDir, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
content := "name: shared\ncommand: dir-cmd\nenabled: true\ndescription: from dir\n"
|
||||
if err := os.WriteFile(filepath.Join(toolsDir, "shared.yaml"), []byte(content), 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
inline := []ToolConfig{
|
||||
{Name: "shared", Command: "inline-cmd", Enabled: true, Description: "from inline"},
|
||||
}
|
||||
merged, err := MergeToolsFromDir(toolsDir, inline)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(merged) != 1 {
|
||||
t.Fatalf("expected 1 tool, got %d", len(merged))
|
||||
}
|
||||
if merged[0].Command != "dir-cmd" {
|
||||
t.Fatalf("dir tool should win, got command %q", merged[0].Command)
|
||||
}
|
||||
}
|
||||
@@ -1,97 +0,0 @@
|
||||
package config
|
||||
|
||||
import "strings"
|
||||
|
||||
// VisionConfig 独立视觉模型与 analyze_image 工具参数;enabled 时注册 MCP 工具 analyze_image。
|
||||
type VisionConfig struct {
|
||||
Enabled bool `yaml:"enabled" json:"enabled"`
|
||||
APIKey string `yaml:"api_key,omitempty" json:"api_key,omitempty"`
|
||||
BaseURL string `yaml:"base_url,omitempty" json:"base_url,omitempty"`
|
||||
Model string `yaml:"model,omitempty" json:"model,omitempty"`
|
||||
Provider string `yaml:"provider,omitempty" json:"provider,omitempty"`
|
||||
TimeoutSeconds int `yaml:"timeout_seconds,omitempty" json:"timeout_seconds,omitempty"`
|
||||
MaxImageBytes int64 `yaml:"max_image_bytes,omitempty" json:"max_image_bytes,omitempty"`
|
||||
MaxDimension int `yaml:"max_dimension,omitempty" json:"max_dimension,omitempty"`
|
||||
JPEGQuality int `yaml:"jpeg_quality,omitempty" json:"jpeg_quality,omitempty"`
|
||||
MaxPayloadBytes int64 `yaml:"max_payload_bytes,omitempty" json:"max_payload_bytes,omitempty"`
|
||||
SkipPreprocessBelowBytes int64 `yaml:"skip_preprocess_below_bytes,omitempty" json:"skip_preprocess_below_bytes,omitempty"` // 0=始终压缩;默认 2MB 且长边已<=max_dimension 时原图直传
|
||||
Detail string `yaml:"detail,omitempty" json:"detail,omitempty"` // low | high | auto
|
||||
}
|
||||
|
||||
func (v VisionConfig) TimeoutSecondsEffective() int {
|
||||
if v.TimeoutSeconds <= 0 {
|
||||
return 60
|
||||
}
|
||||
return v.TimeoutSeconds
|
||||
}
|
||||
|
||||
func (v VisionConfig) MaxImageBytesEffective() int64 {
|
||||
if v.MaxImageBytes <= 0 {
|
||||
return 5 * 1024 * 1024
|
||||
}
|
||||
return v.MaxImageBytes
|
||||
}
|
||||
|
||||
func (v VisionConfig) MaxDimensionEffective() int {
|
||||
if v.MaxDimension <= 0 {
|
||||
return 2048
|
||||
}
|
||||
return v.MaxDimension
|
||||
}
|
||||
|
||||
func (v VisionConfig) JPEGQualityEffective() int {
|
||||
if v.JPEGQuality <= 0 || v.JPEGQuality > 100 {
|
||||
return 82
|
||||
}
|
||||
return v.JPEGQuality
|
||||
}
|
||||
|
||||
func (v VisionConfig) MaxPayloadBytesEffective() int64 {
|
||||
if v.MaxPayloadBytes <= 0 {
|
||||
return 512 * 1024
|
||||
}
|
||||
return v.MaxPayloadBytes
|
||||
}
|
||||
|
||||
// SkipPreprocessBelowBytesEffective 低于该字节数且长边<=max_dimension、且<=max_payload 时可原图直传;0 表示始终压缩。
|
||||
func (v VisionConfig) SkipPreprocessBelowBytesEffective() int64 {
|
||||
if v.SkipPreprocessBelowBytes < 0 {
|
||||
return 0
|
||||
}
|
||||
return v.SkipPreprocessBelowBytes
|
||||
}
|
||||
|
||||
func (v VisionConfig) DetailEffective() string {
|
||||
d := strings.ToLower(strings.TrimSpace(v.Detail))
|
||||
switch d {
|
||||
case "high", "low", "auto":
|
||||
return d
|
||||
default:
|
||||
return "low"
|
||||
}
|
||||
}
|
||||
|
||||
// OpenAICfgEffective 合并主 openai 配置与 vision 覆盖项,供 VL ChatModel 使用。
|
||||
// vision.api_key / base_url / provider 留空或省略时,沿用 main(openai)对应字段;vision.model 必填(由 Ready 校验)。
|
||||
func (v VisionConfig) OpenAICfgEffective(main OpenAIConfig) OpenAIConfig {
|
||||
out := main
|
||||
if k := strings.TrimSpace(v.APIKey); k != "" {
|
||||
out.APIKey = k
|
||||
}
|
||||
if u := strings.TrimSpace(v.BaseURL); u != "" {
|
||||
out.BaseURL = u
|
||||
}
|
||||
if m := strings.TrimSpace(v.Model); m != "" {
|
||||
out.Model = m
|
||||
}
|
||||
if p := strings.TrimSpace(v.Provider); p != "" {
|
||||
out.Provider = p
|
||||
}
|
||||
out.Reasoning.Mode = "off"
|
||||
return out
|
||||
}
|
||||
|
||||
// Ready 表示已启用且模型名非空。
|
||||
func (v VisionConfig) Ready() bool {
|
||||
return v.Enabled && strings.TrimSpace(v.Model) != ""
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
package config
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestVisionConfig_OpenAICfgEffective_fallbackToMain(t *testing.T) {
|
||||
main := OpenAIConfig{
|
||||
APIKey: "main-key",
|
||||
BaseURL: "https://main.example/v1",
|
||||
Model: "main-model",
|
||||
Provider: "openai",
|
||||
}
|
||||
v := VisionConfig{Model: "qwen-vl-max"}
|
||||
out := v.OpenAICfgEffective(main)
|
||||
if out.APIKey != main.APIKey || out.BaseURL != main.BaseURL || out.Provider != main.Provider {
|
||||
t.Fatalf("expected openai fallback, got key=%q url=%q provider=%q", out.APIKey, out.BaseURL, out.Provider)
|
||||
}
|
||||
if out.Model != "qwen-vl-max" {
|
||||
t.Fatalf("model: %s", out.Model)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisionConfig_OpenAICfgEffective(t *testing.T) {
|
||||
main := OpenAIConfig{
|
||||
APIKey: "main-key",
|
||||
BaseURL: "https://main.example/v1",
|
||||
Model: "main-model",
|
||||
Provider: "openai",
|
||||
Reasoning: OpenAIReasoningConfig{Mode: "on"},
|
||||
}
|
||||
v := VisionConfig{
|
||||
Model: "vl-model",
|
||||
APIKey: "vl-key",
|
||||
BaseURL: "https://vl.example/v1",
|
||||
Provider: "claude",
|
||||
}
|
||||
out := v.OpenAICfgEffective(main)
|
||||
if out.APIKey != "vl-key" || out.BaseURL != "https://vl.example/v1" || out.Model != "vl-model" {
|
||||
t.Fatalf("unexpected merge: %+v", out)
|
||||
}
|
||||
if out.Provider != "claude" {
|
||||
t.Fatalf("provider: %s", out.Provider)
|
||||
}
|
||||
if out.Reasoning.Mode != "off" {
|
||||
t.Fatalf("reasoning should be off for vision, got %s", out.Reasoning.Mode)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVisionConfig_Ready(t *testing.T) {
|
||||
if (VisionConfig{Enabled: true, Model: "x"}).Ready() != true {
|
||||
t.Fatal("expected ready")
|
||||
}
|
||||
if (VisionConfig{Enabled: true}).Ready() != false {
|
||||
t.Fatal("expected not ready without model")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user