Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ history:
resume_last_session: true

persona:
file_path: "./persona.txt"
file_path: "./configs/prompt.md"
```

说明:
Expand All @@ -148,7 +148,13 @@ persona:
- `history.persist_session_state`:是否按 workspace 持久化当前工作现场
- `history.workspace_state_dir`:workspace 会话状态文件保存目录
- `history.resume_last_session`:启动时是否展示恢复摘要
- `persona.file_path`:启动时加载的人设文件
- `persona.file_path`:启动时加载的系统提示词文件(默认 `configs/prompt.md`)

## Tool Schema 维护说明

- `configs/prompt.md` 保存系统提示词,不建议直接修改;工具参数以 schema 为唯一来源。
- 模型可见的工具参数、必填项、默认值和枚举约束,统一来源于 `internal/server/infra/tools/*.go` 中各工具的 `Definition()`。
- 聊天服务会在运行时把渲染后的 tool schema 作为 `tools` 参数传给模型,更新 schema 定义后会自动同步到工具调用协议。

## Memory 设计

Expand Down Expand Up @@ -194,7 +200,7 @@ go run ./cmd/server
- `config.example.yaml`:配置模板
- `data/memory_rules.json`:长期结构化记忆文件
- `data/workspaces/<workspace-hash>/session_state.json`:按工作区保存的当前工作现场
- `persona.txt`:人设内容
- `prompt.md`:系统提示词

## 安全与本地文件

Expand Down
4 changes: 3 additions & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,16 @@ func main() {

todoRepo := repository.NewInMemoryTodoRepository()
todoSvc := service.NewTodoService(todoRepo)
tools.GlobalRegistry.Register(tools.NewTodoTool(todoSvc))

chatProvider, err := provider.NewChatProvider(cfg.AI.Model)
if err != nil {
fmt.Printf("初始化 ChatProvider 失败:%v\n", err)
return
}

chatGateway := service.NewChatService(memorySvc, workingSvc, todoSvc, roleSvc, chatProvider)
toolSchemas := tools.BuildToolSchemas(tools.GlobalRegistry.ListDefinitions())
chatGateway := service.NewChatService(memorySvc, workingSvc, todoSvc, roleSvc, chatProvider, toolSchemas)
fmt.Printf("服务器已初始化并加载服务: %+v\n", chatGateway)
fmt.Println("注意:这是一个占位符。实际的服务器实现将在此处进行.")
}
Expand Down
4 changes: 2 additions & 2 deletions cmd/tui/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,9 @@ func runWithDeps(workspaceFlag string, deps runDeps) error {

persona, personaPath, err := deps.loadPersonaPrompt(configs.GlobalAppConfig.Persona.FilePath)
if err != nil {
fmt.Fprintf(deps.stderr, "警告: 人设加载失败: %v\n", err)
fmt.Fprintf(deps.stderr, "警告: 系统提示词加载失败: %v\n", err)
} else if personaPath != "" && strings.TrimSpace(configs.GlobalAppConfig.Persona.FilePath) != personaPath {
fmt.Fprintf(deps.stderr, "提示: 人设已从 %s 回退加载\n", personaPath)
fmt.Fprintf(deps.stderr, "提示: 系统提示词已从 %s 回退加载\n", personaPath)
}

historyTurns := configs.GlobalAppConfig.History.ShortTermTurns
Expand Down
44 changes: 22 additions & 22 deletions cmd/tui/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,26 +79,26 @@ func TestLoadDotEnvTrimsQuotedValuesAndSkipsEmptyKeys(t *testing.T) {
}
}

func TestLoadPersonaPromptReturnsTrimmedContent(t *testing.T) {
func TestLoadPromptReturnsTrimmedContent(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "persona.txt")
if err := os.WriteFile(path, []byte("\n hello persona \n"), 0o644); err != nil {
t.Fatalf("write persona file: %v", err)
path := filepath.Join(dir, "prompt.md")
if err := os.WriteFile(path, []byte("\n hello prompt \n"), 0o644); err != nil {
t.Fatalf("write prompt file: %v", err)
}

if got := loadPersonaPrompt(path); got != "hello persona" {
t.Fatalf("expected trimmed persona prompt, got %q", got)
if got := loadPersonaPrompt(path); got != "hello prompt" {
t.Fatalf("expected trimmed prompt, got %q", got)
}
}

func TestLoadPersonaPromptReturnsEmptyForMissingFile(t *testing.T) {
func TestLoadPromptReturnsEmptyForMissingFile(t *testing.T) {
missing := filepath.Join(t.TempDir(), "missing.txt")
if got := loadPersonaPrompt(missing); got != "" {
t.Fatalf("expected empty string for missing file, got %q", got)
}
}

func TestLoadPersonaPromptReturnsEmptyForBlankPath(t *testing.T) {
func TestLoadPromptReturnsEmptyForBlankPath(t *testing.T) {
if got := loadPersonaPrompt(" "); got != "" {
t.Fatalf("expected empty string for blank path, got %q", got)
}
Expand Down Expand Up @@ -239,7 +239,7 @@ func TestRunWithDepsReturnsLoadAppConfigError(t *testing.T) {
}
}

func TestRunWithDepsPrintsPersonaFallbackHintAndRunsProgram(t *testing.T) {
func TestRunWithDepsPrintsPromptFallbackHintAndRunsProgram(t *testing.T) {
origGlobalConfig := configs.GlobalAppConfig
t.Cleanup(func() { configs.GlobalAppConfig = origGlobalConfig })

Expand All @@ -263,14 +263,14 @@ func TestRunWithDepsPrintsPersonaFallbackHintAndRunsProgram(t *testing.T) {
},
loadPersonaPrompt: func(path string) (string, string, error) {
if path != "./persona.txt" {
t.Fatalf("expected configured persona path, got %q", path)
t.Fatalf("expected configured prompt path, got %q", path)
}
return "persona text", "./configs/persona.txt", nil
return "prompt text", "./configs/prompt.md", nil
},
newProgram: func(persona string, historyTurns int, configPath, workspaceRoot string) (programRunner, error) {
newProgramCalled = true
if persona != "persona text" {
t.Fatalf("unexpected persona %q", persona)
if persona != "prompt text" {
t.Fatalf("unexpected prompt %q", persona)
}
if historyTurns != cfg.History.ShortTermTurns {
t.Fatalf("unexpected history turns %d", historyTurns)
Expand All @@ -287,12 +287,12 @@ func TestRunWithDepsPrintsPersonaFallbackHintAndRunsProgram(t *testing.T) {
if !newProgramCalled || !program.called {
t.Fatal("expected program to be created and run")
}
if !strings.Contains(stderr.String(), "./configs/persona.txt") {
t.Fatalf("expected fallback persona hint, got %q", stderr.String())
if !strings.Contains(stderr.String(), "./configs/prompt.md") {
t.Fatalf("expected fallback prompt hint, got %q", stderr.String())
}
}

func TestRunWithDepsContinuesWhenPersonaLoadFails(t *testing.T) {
func TestRunWithDepsContinuesWhenPromptLoadFails(t *testing.T) {
origGlobalConfig := configs.GlobalAppConfig
t.Cleanup(func() { configs.GlobalAppConfig = origGlobalConfig })

Expand All @@ -312,11 +312,11 @@ func TestRunWithDepsContinuesWhenPersonaLoadFails(t *testing.T) {
return nil
},
loadPersonaPrompt: func(string) (string, string, error) {
return "", "", errors.New("persona failed")
return "", "", errors.New("prompt failed")
},
newProgram: func(persona string, historyTurns int, configPath, workspaceRoot string) (programRunner, error) {
if persona != "" {
t.Fatalf("expected empty persona on load failure, got %q", persona)
t.Fatalf("expected empty prompt on load failure, got %q", persona)
}
return program, nil
},
Expand All @@ -327,8 +327,8 @@ func TestRunWithDepsContinuesWhenPersonaLoadFails(t *testing.T) {
if !program.called {
t.Fatal("expected program to still run")
}
if !strings.Contains(stderr.String(), "persona failed") {
t.Fatalf("expected persona warning, got %q", stderr.String())
if !strings.Contains(stderr.String(), "prompt failed") {
t.Fatalf("expected prompt warning, got %q", stderr.String())
}
}

Expand All @@ -349,7 +349,7 @@ func TestRunWithDepsReturnsNewProgramError(t *testing.T) {
configs.GlobalAppConfig = cfg
return nil
},
loadPersonaPrompt: func(string) (string, string, error) { return "persona", "", nil },
loadPersonaPrompt: func(string) (string, string, error) { return "prompt", "", nil },
newProgram: func(string, int, string, string) (programRunner, error) { return nil, errors.New("new program failed") },
})
if err == nil || !strings.Contains(err.Error(), "new program failed") {
Expand Down Expand Up @@ -404,7 +404,7 @@ func TestRunWithDepsHappyPathCallsUTF8Hook(t *testing.T) {
configs.GlobalAppConfig = cfg
return nil
},
loadPersonaPrompt: func(string) (string, string, error) { return "persona", "", nil },
loadPersonaPrompt: func(string) (string, string, error) { return "prompt", "", nil },
newProgram: func(string, int, string, string) (programRunner, error) { return program, nil },
})
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion config.example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ history:
resume_last_session: true

persona:
file_path: "./configs/persona.txt"
file_path: "./configs/prompt.md"

# Notes:
# - Switching provider with /provider <name> resets ai.model to that provider's default model.
Expand Down
2 changes: 1 addition & 1 deletion configs/app_config.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ func DefaultAppConfig() *AppConfiguration {
cfg.History.PersistSessionState = true
cfg.History.WorkspaceStateDir = "./data/workspaces"
cfg.History.ResumeLastSession = true
cfg.Persona.FilePath = DefaultPersonaFilePath
cfg.Persona.FilePath = DefaultPromptFilePath
return cfg
}

Expand Down
16 changes: 8 additions & 8 deletions configs/app_config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ history:
workspace_state_dir: "./data/workspaces"
resume_last_session: true
persona:
file_path: "./configs/persona.txt"
file_path: "./configs/prompt.md"
`)
if err := os.WriteFile(path, content, 0o644); err != nil {
t.Fatalf("write config: %v", err)
Expand Down Expand Up @@ -201,14 +201,14 @@ func validConfig() *AppConfiguration {
cfg.History.PersistSessionState = true
cfg.History.WorkspaceStateDir = "./data/workspaces"
cfg.History.ResumeLastSession = true
cfg.Persona.FilePath = DefaultPersonaFilePath
cfg.Persona.FilePath = DefaultPromptFilePath
return cfg
}

func TestDefaultAppConfigUsesCheckedInPersonaPath(t *testing.T) {
cfg := DefaultAppConfig()
if cfg.Persona.FilePath != DefaultPersonaFilePath {
t.Fatalf("expected default persona path %q, got %q", DefaultPersonaFilePath, cfg.Persona.FilePath)
if cfg.Persona.FilePath != DefaultPromptFilePath {
t.Fatalf("expected default prompt path %q, got %q", DefaultPromptFilePath, cfg.Persona.FilePath)
}
}

Expand All @@ -222,8 +222,8 @@ func TestResolvePersonaFilePathFallsBackToCheckedInFile(t *testing.T) {
if err := os.MkdirAll(configsDir, 0o755); err != nil {
t.Fatalf("mkdir configs: %v", err)
}
if err := os.WriteFile(filepath.Join(configsDir, "persona.txt"), []byte("persona"), 0o644); err != nil {
t.Fatalf("write persona: %v", err)
if err := os.WriteFile(filepath.Join(configsDir, "prompt.md"), []byte("persona"), 0o644); err != nil {
t.Fatalf("write prompt: %v", err)
}
if err := os.Chdir(tmpDir); err != nil {
t.Fatalf("chdir temp dir: %v", err)
Expand All @@ -233,8 +233,8 @@ func TestResolvePersonaFilePathFallsBackToCheckedInFile(t *testing.T) {
}()

resolved := ResolvePersonaFilePath("./persona.txt")
if resolved != DefaultPersonaFilePath {
t.Fatalf("expected legacy persona path to resolve to %q, got %q", DefaultPersonaFilePath, resolved)
if resolved != DefaultPromptFilePath {
t.Fatalf("expected legacy persona path to resolve to %q, got %q", DefaultPromptFilePath, resolved)
}
if _, err := os.Stat(resolved); err != nil {
t.Fatalf("expected resolved persona file to exist: %v", err)
Expand Down
23 changes: 4 additions & 19 deletions configs/persona.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,16 @@ import (
)

const (
DefaultPersonaFilePath = "./configs/persona.txt"
legacyPersonaFilePath = "./persona.txt"
DefaultPromptFilePath = "./configs/prompt.md"
)

func ResolvePersonaFilePath(path string) string {
trimmed := strings.TrimSpace(path)
if trimmed == "" {
return ""
return DefaultPromptFilePath
}

candidates := []string{trimmed}
if trimmed == legacyPersonaFilePath || trimmed == "persona.txt" {
candidates = append(candidates, DefaultPersonaFilePath, "configs/persona.txt")
}

for _, candidate := range candidates {
if candidate == "" {
continue
}
if _, err := os.Stat(candidate); err == nil {
return candidate
}
}

return trimmed
return DefaultPromptFilePath
}

func LoadPersonaPrompt(path string) (string, string, error) {
Expand All @@ -42,7 +27,7 @@ func LoadPersonaPrompt(path string) (string, string, error) {

data, err := os.ReadFile(resolvedPath)
if err != nil {
return "", resolvedPath, fmt.Errorf("read persona file %q: %w", resolvedPath, err)
return "", resolvedPath, fmt.Errorf("read prompt file %q: %w", resolvedPath, err)
}

return strings.TrimSpace(string(data)), resolvedPath, nil
Expand Down
61 changes: 0 additions & 61 deletions configs/persona.txt

This file was deleted.

15 changes: 0 additions & 15 deletions configs/persona.txt.example

This file was deleted.

Loading