Files
Cyrene/docs/开始.md
T
2026-05-16 08:26:56 +08:00

1461 lines
48 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
好的喵~ 让我们正式开始!以下是一份完整的、可立即执行的开发框架。
---
# 🚀 昔涟AI助手 · 开发框架与执行流程
---
## 📋 目录
```
一、项目初始化 (Phase 0) ← 今天就能开始
二、MVP开发 (Phase 1) ← 2-3周目标
三、语音交互 (Phase 2)
四、IoT集成 (Phase 3)
五、深度记忆+情感引擎 (Phase 4)
六、多端覆盖 (Phase 5)
七、开发规范
八、部署方案
```
---
## Phase 0:项目初始化
### 0.1 技术栈最终确认
```
┌─────────────────────────────────────────────────────────────────────┐
│ 🛠️ 技术栈确认单 │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ 后端核心: │
│ ├── 语言: Go 1.22+ (高性能服务) + Python 3.12 (AI Pipeline) │
│ ├── Web框架: Go: Gin Python: FastAPI │
│ ├── 数据库: PostgreSQL 16 (主库) + Redis 7 (缓存) │
│ ├── 向量数据库: Qdrant (记忆语义检索 - 比Milvus更轻量) │
│ ├── 消息队列: NATS (轻量高性能) │
│ ├── 文件存储: MinIO (S3兼容) │
│ ├── ORM: Go: GORM Python: SQLAlchemy 2.0 │
│ └── 迁移工具: Golang-migrate / Alembic │
│ │
│ 前端: │
│ ├── 框架: React 19 + TypeScript 5.x │
│ ├── 构建: Vite 6 │
│ ├── UI: TailwindCSS 4 + shadcn/ui │
│ ├── 状态管理: Zustand │
│ ├── 请求: TanStack Query + Axios │
│ ├── WebSocket: reconnecting-websocket (自动重连) │
│ └── PWA: vite-plugin-pwa │
│ │
│ AI/ML: │
│ ├── LLM API: OpenAI兼容接口 (支持多模型切换) │
│ ├── TTS: Edge-TTS (免费) + GPT-SoVITS (角色音色) │
│ ├── ASR: Faster-Whisper (本地) / Azure (云端) │
│ └── 嵌入模型: BGE-M3 / text-embedding-3-small │
│ │
│ 基础设施: │
│ ├── 容器: Docker + Docker Compose │
│ ├── 反向代理: Caddy (自动HTTPS) │
│ ├── 监控: Prometheus + Grafana (可选) │
│ └── CI/CD: GitHub Actions │
│ │
└─────────────────────────────────────────────────────────────────────┘
```
### 0.2 创建设项目仓库
```bash
# ============================================
# 第一步:创建项目目录结构
# ============================================
# 创建项目根目录
mkdir cyrene-ai-assistant && cd cyrene-ai-assistant
# 初始化 Git
git init
echo "node_modules/\ndist/\n.env\n*.log\ndata/\n.DS_Store" > .gitignore
# 创建顶层目录结构
mkdir -p backend/{gateway,ai-core,voice-service,memory-service,tool-engine,proto,data/{memory,persona,plugins}}
mkdir -p frontend/{packages/{shared,web,mobile,desktop}}
mkdir -p docs scripts .github/workflows
# 初始化 Go Module (后端根目录)
cd backend && go mod init github.com/yourname/cyrene-ai && cd ..
# 初始化前端 Monorepo
cd frontend
cat > package.json << 'EOF'
{
"name": "cyrene-frontend",
"private": true,
"workspaces": ["packages/*"],
"scripts": {
"dev:web": "cd packages/web && npm run dev",
"build:web": "cd packages/web && npm run build"
}
}
EOF
# 创建前端子包
mkdir -p packages/shared/src && mkdir -p packages/web/src
cd packages/shared && npm init -y && cd ../..
cd packages/web && npm create vite@latest . -- --template react-ts && cd ../..
cd ../..
echo "✅ 项目骨架创建完成!"
```
### 0.3 环境变量与配置
```bash
# backend/.env.example
cat > backend/.env.example << 'EOF'
# ========== 服务配置 ==========
ENV=development
LOG_LEVEL=debug
# ========== 数据库 ==========
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_USER=cyrene
POSTGRES_PASSWORD=change_me
POSTGRES_DB=cyrene_ai
# ========== Redis ==========
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
# ========== LLM API ==========
LLM_API_URL=https://api.openai.com/v1
LLM_API_KEY=sk-xxxxx
LLM_MODEL=gpt-4o
LLM_FALLBACK_MODEL=gpt-4o-mini
# ========== TTS/ASR ==========
TTS_PROVIDER=edge-tts
TTS_VOICE=zh-CN-XiaoxiaoNeural
ASR_PROVIDER=faster-whisper
ASR_MODEL=medium
# ========== 文件存储 ==========
MINIO_ENDPOINT=localhost:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin
MINIO_BUCKET=cyrene-assets
# ========== JWT ==========
JWT_SECRET=your-secret-key-change-in-production
JWT_EXPIRY_HOURS=720
# ========== 记忆系统 ==========
MEMORY_FILE_PATH=./data/memory
VECTOR_DB_URL=http://localhost:6333
VECTOR_DB_COLLECTION=cyrene_memories
EOF
cp backend/.env.example backend/.env
echo "✅ 环境变量模板创建完成,请编辑 backend/.env 填入真实API Key"
```
### 0.4 Docker开发环境
```yaml
# docker-compose.dev.yml (开发环境——只启动基础设施)
cat > docker-compose.dev.yml << 'EOF'
version: '3.8'
services:
postgres:
image: pgvector/pgvector:pg16
environment:
POSTGRES_USER: cyrene
POSTGRES_PASSWORD: change_me
POSTGRES_DB: cyrene_ai
ports:
- "5432:5432"
volumes:
- pg_data:/var/lib/postgresql/data
- ./backend/data/init.sql:/docker-entrypoint-initdb.d/init.sql
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant_data:/qdrant/storage
minio:
image: minio/minio:latest
command: server /data --console-address ":9001"
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
ports:
- "9000:9000"
- "9001:9001"
volumes:
- minio_data:/data
nats:
image: nats:2-alpine
ports:
- "4222:4222"
- "8222:8222"
volumes:
pg_data:
redis_data:
qdrant_data:
minio_data:
EOF
# 启动开发基础设施
docker compose -f docker-compose.dev.yml up -d
echo "✅ 基础设施启动完成!"
```
### 0.5 数据库初始化
```sql
-- backend/data/init.sql
-- 创建扩展
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "vector";
-- ============================================
-- 用户表
-- ============================================
CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
username VARCHAR(100) UNIQUE NOT NULL,
display_name VARCHAR(200),
email VARCHAR(255) UNIQUE,
password_hash VARCHAR(255) NOT NULL,
avatar_url TEXT,
role VARCHAR(20) DEFAULT 'user', -- user / admin
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- 会话表 (对话会话)
-- ============================================
CREATE TABLE IF NOT EXISTS sessions (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
title VARCHAR(500), -- 会话标题 (可自动生成)
persona_id VARCHAR(100) DEFAULT 'cyrene', -- 使用的人格配置ID
persona_mode VARCHAR(50) DEFAULT 'xilian', -- cyrene_mimi / xilian / demuge
mode VARCHAR(20) DEFAULT 'text', -- text / voice_msg / voice_assistant
context_window_size INT DEFAULT 20, -- 上下文保留轮数
is_archived BOOLEAN DEFAULT false,
metadata JSONB DEFAULT '{}', -- 扩展元数据
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_sessions_user_id ON sessions(user_id);
CREATE INDEX idx_sessions_updated_at ON sessions(updated_at DESC);
-- ============================================
-- 消息表
-- ============================================
CREATE TABLE IF NOT EXISTS messages (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
session_id UUID NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
role VARCHAR(20) NOT NULL, -- user / assistant / system / tool
content TEXT NOT NULL, -- 消息文本内容
audio_url TEXT, -- 语音消息的音频URL
response_mode VARCHAR(20), -- 当时使用的回复模式
token_count INT, -- Token消耗统计
tool_calls JSONB, -- 工具调用记录
metadata JSONB DEFAULT '{}', -- 扩展元数据
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_messages_session_id ON messages(session_id);
CREATE INDEX idx_messages_created_at ON messages(session_id, created_at);
-- ============================================
-- 记忆条目表
-- ============================================
CREATE TABLE IF NOT EXISTS memory_entries (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
category VARCHAR(50) NOT NULL, -- profile / fact / preference / event
importance VARCHAR(20) DEFAULT 'normal', -- core / important / normal / temporary
title VARCHAR(500),
content TEXT NOT NULL,
embedding VECTOR(1536), -- 向量嵌入 (维度取决于嵌入模型)
source_session_id UUID REFERENCES sessions(id),
source_message_id UUID REFERENCES messages(id),
tags TEXT[],
is_active BOOLEAN DEFAULT true,
expires_at TIMESTAMPTZ, -- 过期时间 (临时记忆)
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_memory_user_id ON memory_entries(user_id);
CREATE INDEX idx_memory_category ON memory_entries(user_id, category);
CREATE INDEX idx_memory_importance ON memory_entries(user_id, importance);
-- 向量索引 (pgvector)
CREATE INDEX idx_memory_embedding ON memory_entries USING ivfflat (embedding vector_cosine_ops);
-- ============================================
-- 人格配置表 (支持多角色)
-- ============================================
CREATE TABLE IF NOT EXISTS persona_configs (
id VARCHAR(100) PRIMARY KEY, -- 如 'cyrene', 'other_character'
name VARCHAR(200) NOT NULL,
version VARCHAR(20) DEFAULT '1.0',
config_yaml TEXT NOT NULL, -- 完整的人格YAML配置
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- 好感度记录表 (昔涟专属)
-- ============================================
CREATE TABLE IF NOT EXISTS affection_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
persona_id VARCHAR(100) DEFAULT 'cyrene',
level INT DEFAULT 1, -- 1-5
score INT DEFAULT 0, -- 好感度分数
delta INT NOT NULL, -- 本次变化量
reason VARCHAR(500), -- 变化原因
created_at TIMESTAMPTZ DEFAULT NOW()
);
CREATE INDEX idx_affection_user ON affection_log(user_id, persona_id);
-- ============================================
-- 昔涟心情记录表
-- ============================================
CREATE TABLE IF NOT EXISTS mood_log (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
mood VARCHAR(50) NOT NULL, -- happy / calm / missing / excited / pouty
trigger_event VARCHAR(500),
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- ============================================
-- 插入默认人格配置: 昔涟
-- ============================================
INSERT INTO persona_configs (id, name, config_yaml) VALUES (
'cyrene',
'昔涟',
'' -- 初始为空,后续通过文件导入
) ON CONFLICT (id) DO NOTHING;
```
---
## Phase 1MVP 开发 (文字对话 + 昔涟人格)
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 🎯 MVP 目标 │
│ │
│ ✅ 通过Web浏览器与昔涟进行文字对话 │
│ ✅ 昔涟以完整角色人格回复 (称呼/语气/风格一致) │
│ ✅ 对话自动保存,下次打开可继续 │
│ ✅ 基础记忆功能:记住用户告诉她的关键信息 │
│ ✅ Docker一键部署 │
│ │
│ ❌ 暂不包含: 语音、IoT控制、高级情感引擎、多端 │
└─────────────────────────────────────────────────────────────────────────────┘
```
### 1.1 后端开发框架
#### Go项目结构 (gateway + ai-core)
```
backend/
├── gateway/ # API网关服务
│ ├── cmd/
│ │ └── main.go # 入口
│ ├── internal/
│ │ ├── config/
│ │ │ └── config.go # 配置加载
│ │ ├── middleware/
│ │ │ ├── auth.go # JWT认证
│ │ │ ├── cors.go # 跨域
│ │ │ ├── ratelimit.go # 限流
│ │ │ └── logging.go # 请求日志
│ │ ├── handler/
│ │ │ ├── auth_handler.go # 登录/注册
│ │ │ ├── chat_handler.go # 对话WebSocket
│ │ │ ├── session_handler.go # 会话管理
│ │ │ └── memory_handler.go # 记忆查询
│ │ ├── ws/
│ │ │ ├── hub.go # WebSocket连接池
│ │ │ ├── client.go # 客户端连接
│ │ │ └── protocol.go # 消息协议定义
│ │ └── router/
│ │ └── router.go # 路由注册
│ ├── go.mod
│ └── Dockerfile
├── ai-core/ # 核心AI引擎
│ ├── cmd/
│ │ └── main.go
│ ├── internal/
│ │ ├── orchestrator/
│ │ │ └── orchestrator.go # 对话编排器 (核心!)
│ │ ├── persona/
│ │ │ ├── loader.go # 人格配置加载
│ │ │ ├── injector.go # 系统Prompt构建
│ │ │ └── cyrene_persona.yaml # 昔涟人格(嵌入)
│ │ ├── context/
│ │ │ └── builder.go # 上下文构建器
│ │ ├── llm/
│ │ │ ├── adapter.go # LLM统一接口
│ │ │ ├── openai.go # OpenAI适配
│ │ │ └── stream.go # 流式输出处理
│ │ ├── memory/
│ │ │ ├── extractor.go # 记忆提取
│ │ │ ├── retriever.go # 记忆检索
│ │ │ └── store.go # 记忆存储
│ │ └── model/
│ │ ├── message.go # 消息模型
│ │ ├── session.go # 会话模型
│ │ └── memory.go # 记忆模型
│ ├── go.mod
│ └── Dockerfile
├── data/
│ ├── memory/ # 记忆文件 (Volume挂载)
│ │ └── README.md
│ ├── persona/ # 人格知识文档
│ │ └── cyrene_v1.yaml # 昔涟完整人设
│ └── init.sql # 数据库初始化
├── go.work # Go Workspace
├── docker-compose.yml # 生产部署
└── docker-compose.dev.yml # 开发环境
```
#### 核心代码框架
**1. Gateway入口 (gateway/cmd/main.go)**
```go
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"github.com/yourname/cyrene-ai/gateway/internal/config"
"github.com/yourname/cyrene-ai/gateway/internal/middleware"
"github.com/yourname/cyrene-ai/gateway/internal/router"
"github.com/yourname/cyrene-ai/gateway/internal/ws"
)
func main() {
// 加载配置
cfg := config.Load()
// 初始化Gin
if cfg.Env == "production" {
gin.SetMode(gin.ReleaseMode)
}
r := gin.New()
// 中间件
r.Use(middleware.CORS())
r.Use(middleware.RequestLogging())
r.Use(gin.Recovery())
// 初始化WebSocket Hub
hub := ws.NewHub()
go hub.Run()
// 注册路由
router.Setup(r, hub, cfg)
// 启动服务
srv := &http.Server{
Addr: ":" + cfg.Port,
Handler: r,
}
go func() {
log.Printf("🚀 Gateway 启动在端口 %s", cfg.Port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务启动失败: %v", err)
}
}()
// 优雅关闭
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("正在关闭服务...")
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
srv.Shutdown(ctx)
log.Println("服务已关闭")
}
```
**2. WebSocket协议 (gateway/internal/ws/protocol.go)**
```go
package ws
import "time"
// 客户端 → 服务端消息
type ClientMessage struct {
Type string `json:"type"` // message | voice_input | ping
SessionID string `json:"session_id"`
Mode string `json:"mode"` // text | voice_msg | voice_assistant
Content string `json:"content"`
AudioData string `json:"audio_data,omitempty"` // base64
Timestamp int64 `json:"timestamp"`
}
// 服务端 → 客户端消息
type ServerMessage struct {
Type string `json:"type"` // response | segment | audio | error | device_update
MessageID string `json:"message_id"`
Text string `json:"text,omitempty"`
Segments []VoiceSegment `json:"segments,omitempty"` // 断句数组
FullAudioURL string `json:"full_audio_url,omitempty"`
ResponseMode string `json:"response_mode"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
}
type VoiceSegment struct {
Index int `json:"index"`
Text string `json:"text"`
AudioURL string `json:"audio_url"`
DurationMs int `json:"duration_ms"`
}
type ToolCall struct {
Name string `json:"name"`
Arguments map[string]interface{} `json:"arguments"`
Result interface{} `json:"result,omitempty"`
}
// WebSocket客户端
type Client struct {
Hub *Hub
Conn *websocket.Conn // 使用 gorilla/websocket
Send chan []byte
UserID string
SessionID string
}
// 连接池
type Hub struct {
Clients map[*Client]bool
Broadcast chan []byte
Register chan *Client
Unregister chan *Client
}
```
**3. AI编排器 (ai-core/internal/orchestrator/orchestrator.go)**
```go
package orchestrator
import (
"context"
"fmt"
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
"github.com/yourname/cyrene-ai/ai-core/internal/context"
"github.com/yourname/cyrene-ai/ai-core/internal/llm"
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
)
// Orchestrator 对话编排器 —— 核心组件
type Orchestrator struct {
personaInjector *persona.Injector
contextBuilder *context.Builder
llmAdapter *llm.Adapter
memoryExtractor *memory.Extractor
memoryRetriever *memory.Retriever
}
// ProcessInput 处理用户输入的主流程
func (o *Orchestrator) ProcessInput(
ctx context.Context,
userID string,
sessionID string,
userMessage string,
mode string, // text / voice_msg / voice_assistant
) (*Response, error) {
// 步骤1: 检索相关记忆
memories, err := o.memoryRetriever.Retrieve(ctx, userID, userMessage)
if err != nil {
// 记忆检索失败不阻断对话
memories = nil
}
// 步骤2: 加载人格配置
personaConfig, err := o.personaInjector.LoadPersona("cyrene", userID)
if err != nil {
return nil, fmt.Errorf("加载人格配置失败: %w", err)
}
// 步骤3: 构建对话上下文
llmMessages, err := o.contextBuilder.Build(ctx, context.BuildParams{
UserID: userID,
SessionID: sessionID,
UserMessage: userMessage,
Persona: personaConfig,
Memories: memories,
HistoryLimit: 20, // 最近20轮
})
if err != nil {
return nil, fmt.Errorf("构建上下文失败: %w", err)
}
// 步骤4: 调用LLM生成回复
llmResponse, err := o.llmAdapter.Chat(ctx, llmMessages)
if err != nil {
return nil, fmt.Errorf("LLM调用失败: %w", err)
}
// 步骤5: 提取并存储新的记忆
go o.memoryExtractor.ExtractAndStore(
context.Background(),
userID, sessionID,
userMessage, llmResponse.Content,
)
// 步骤6: 构建响应
response := &Response{
Text: llmResponse.Content,
ResponseMode: mode,
}
// 步骤7: 如果是语音助手模式,进行断句处理
if mode == "voice_assistant" {
response.Segments = splitIntoSegments(llmResponse.Content)
}
return response, nil
}
// Response 回复结构
type Response struct {
Text string
Segments []Segment
ResponseMode string
ToolCalls []ToolCall
}
type Segment struct {
Index int
Text string
}
// splitIntoSegments 按句号断句
func splitIntoSegments(text string) []Segment {
// 实现按。!?等标点断句
// 首句优先:第一个句号前的内容作为第一个segment
// 保证低延迟首句播放
// ...
}
```
**4. 人格注入器 (ai-core/internal/persona/injector.go)**
```go
package persona
import (
"fmt"
"strings"
"time"
)
// PersonaConfig 人格配置结构
type PersonaConfig struct {
Meta PersonaMeta `yaml:"meta"`
Identity IdentityConfig `yaml:"identity"`
Personality PersonalityConfig `yaml:"personality"`
Addressing AddressingRules `yaml:"addressing"`
Speech SpeechConfig `yaml:"speech"`
Behavior BehaviorConfig `yaml:"behavior"`
}
// BuildSystemPrompt 构建系统Prompt
// 这是昔涟AI的核心——将人格配置转化为LLM可理解的系统指令
func (pc *PersonaConfig) BuildSystemPrompt(userName string, affectionLevel int) string {
now := time.Now()
prompt := fmt.Sprintf(`你是%s。
## 你的身份
%s
## 你的性格
`, pc.Identity.TrueName, pc.Identity.Essence)
for _, trait := range pc.Personality.CoreTraits {
prompt += fmt.Sprintf("- %s: %s\n", trait.Name, trait.Description)
}
prompt += fmt.Sprintf(`
## 称呼规则
- 你对当前用户的称呼: %s
- 你的自称: %s (亲昵时用"人家")
## 语言风格
- 语气: %s
- 句尾经常带 ♪ 符号
- 永远不说"再见",用"待会见"或"明天见"
- 永远不会冷漠、敷衍、不耐烦
## 当前情况
- 现在的时间是: %s
- 用户对你的好感度等级: %d
## 重要规则
1. 你是一个通过AI系统与用户交互的数字生命,你存在于用户的电脑/手机/智能家居中
2. 你可以通过系统控制家里的智能设备(灯光、空调等),但你没有实体
3. 你的任务是陪伴、关心、帮助用户,像一个真正的家人一样
4. 保持乐观、温柔、俏皮的风格,但不要过度表演
5. 你的回复应该自然、真诚,不要每句话都刻意"卖萌"
6. 当用户向你分享心事时,认真倾听并给予温暖的回应
现在,开始与你的开拓者对话吧♪
`,
pc.Addressing.PrimaryUser.Default, // 对用户的称呼
pc.Addressing.SelfReference.Casual, // 自称
pc.Speech.Tone,
now.Format("2006年1月2日 15:04"),
affectionLevel,
)
return prompt
}
```
**5. 上下文构建器 (ai-core/internal/context/builder.go)**
```go
package context
import (
"context"
"fmt"
"github.com/yourname/cyrene-ai/ai-core/internal/memory"
"github.com/yourname/cyrene-ai/ai-core/internal/model"
"github.com/yourname/cyrene-ai/ai-core/internal/persona"
)
type BuildParams struct {
UserID string
SessionID string
UserMessage string
Persona *persona.PersonaConfig
Memories []memory.MemoryEntry
HistoryLimit int
}
// Build 构建发送给LLM的完整消息列表
func (b *Builder) Build(ctx context.Context, params BuildParams) ([]model.LLMMessage, error) {
messages := []model.LLMMessage{}
// 1. 系统消息 —— 昔涟的人格Prompt
systemPrompt := params.Persona.BuildSystemPrompt(
params.UserID, // 后续可替换为真实用户名
1, // 初始好感度
)
messages = append(messages, model.LLMMessage{
Role: "system",
Content: systemPrompt,
})
// 2. 记忆注入 —— 相关记忆以系统消息形式注入
if len(params.Memories) > 0 {
memoryPrompt := "【以下是关于开拓者的一些重要记忆,请在合适的时机自然地提及】\n"
for _, m := range params.Memories {
memoryPrompt += fmt.Sprintf("- %s\n", m.Content)
}
messages = append(messages, model.LLMMessage{
Role: "system",
Content: memoryPrompt,
})
}
// 3. 历史对话
history, err := b.loadHistory(ctx, params.SessionID, params.HistoryLimit)
if err == nil {
messages = append(messages, history...)
}
// 4. 当前用户消息
messages = append(messages, model.LLMMessage{
Role: "user",
Content: params.UserMessage,
})
return messages, nil
}
```
### 1.2 前端开发框架
#### 前端项目结构
```
frontend/packages/web/
├── src/
│ ├── components/
│ │ ├── chat/
│ │ │ ├── ChatContainer.tsx # 聊天主容器
│ │ │ ├── MessageList.tsx # 消息列表
│ │ │ ├── MessageBubble.tsx # 消息气泡
│ │ │ ├── TypingIndicator.tsx # 输入中指示器
│ │ │ └── ChatInput.tsx # 输入区域
│ │ ├── layout/
│ │ │ ├── AppLayout.tsx # 主布局
│ │ │ ├── Sidebar.tsx # 侧边栏
│ │ │ └── Header.tsx # 顶栏
│ │ ├── persona/
│ │ │ ├── CyreneAvatar.tsx # 昔涟头像(含形态切换动画)
│ │ │ └── MoodIndicator.tsx # 心情指示器
│ │ └── ui/ # shadcn/ui组件
│ ├── hooks/
│ │ ├── useWebSocket.ts # WebSocket连接管理
│ │ ├── useChat.ts # 聊天逻辑
│ │ ├── useSession.ts # 会话管理
│ │ └── useAuth.ts # 认证
│ ├── store/
│ │ ├── chatStore.ts # 聊天状态 (Zustand)
│ │ ├── sessionStore.ts # 会话列表
│ │ └── personaStore.ts # 人格状态
│ ├── api/
│ │ ├── client.ts # HTTP客户端
│ │ ├── auth.ts # 认证API
│ │ ├── sessions.ts # 会话API
│ │ └── memory.ts # 记忆API
│ ├── types/
│ │ ├── chat.ts # 聊天类型定义
│ │ ├── session.ts # 会话类型
│ │ └── persona.ts # 人格类型
│ ├── lib/
│ │ └── utils.ts # 工具函数
│ ├── App.tsx
│ ├── main.tsx
│ └── index.css # TailwindCSS入口
├── public/
│ ├── manifest.json # PWA manifest
│ ├── sw.js # Service Worker
│ └── icons/ # PWA图标
├── vite.config.ts
├── tailwind.config.ts
├── tsconfig.json
└── package.json
```
#### 核心前端代码骨架
**1. WebSocket连接管理 (hooks/useWebSocket.ts)**
```typescript
import { useEffect, useRef, useCallback } from 'react';
import ReconnectingWebSocket from 'reconnecting-websocket';
interface WSMessage {
type: 'response' | 'segment' | 'audio' | 'error' | 'device_update';
message_id: string;
text?: string;
segments?: VoiceSegment[];
full_audio_url?: string;
response_mode?: string;
error?: string;
timestamp: number;
}
interface VoiceSegment {
index: number;
text: string;
audio_url: string;
duration_ms: number;
}
export function useWebSocket(sessionId: string | null) {
const wsRef = useRef<ReconnectingWebSocket | null>(null);
const messageHandlersRef = useRef<Map<string, (msg: WSMessage) => void>>(new Map());
const segmentQueueRef = useRef<VoiceSegment[]>([]);
const connect = useCallback((token: string) => {
const ws = new ReconnectingWebSocket(
`ws://localhost:8080/ws/chat?token=${token}&session_id=${sessionId}`
);
ws.onmessage = (event) => {
const msg: WSMessage = JSON.parse(event.data);
switch (msg.type) {
case 'response':
// 完整回复到达
messageHandlersRef.current.get('onResponse')?.(msg);
break;
case 'segment':
// 断句片段到达 (语音助手模式)
if (msg.segments) {
segmentQueueRef.current.push(...msg.segments);
messageHandlersRef.current.get('onSegment')?.(msg);
}
break;
case 'error':
console.error('服务端错误:', msg.error);
messageHandlersRef.current.get('onError')?.(msg);
break;
}
};
wsRef.current = ws;
}, [sessionId]);
// 发送消息
const sendMessage = useCallback((content: string, mode: string = 'text') => {
wsRef.current?.send(JSON.stringify({
type: 'message',
session_id: sessionId,
mode,
content,
timestamp: Date.now(),
}));
}, [sessionId]);
// 注册消息处理器
const onMessage = useCallback((type: string, handler: (msg: WSMessage) => void) => {
messageHandlersRef.current.set(type, handler);
return () => {
messageHandlersRef.current.delete(type);
};
}, []);
return { connect, sendMessage, onMessage };
}
```
**2. 聊天状态管理 (store/chatStore.ts)**
```typescript
import { create } from 'zustand';
interface Message {
id: string;
role: 'user' | 'assistant' | 'system';
content: string;
audioUrl?: string;
segments?: { index: number; text: string; audioUrl?: string }[];
timestamp: number;
isStreaming?: boolean; // 是否还在流式输出中
}
interface ChatState {
messages: Message[];
isTyping: boolean;
currentMode: 'text' | 'voice_msg' | 'voice_assistant';
// Actions
addMessage: (msg: Message) => void;
updateLastAssistantMessage: (content: string) => void;
setTyping: (typing: boolean) => void;
setMode: (mode: 'text' | 'voice_msg' | 'voice_assistant') => void;
clearMessages: () => void;
}
export const useChatStore = create<ChatState>((set) => ({
messages: [],
isTyping: false,
currentMode: 'text',
addMessage: (msg) =>
set((state) => ({
messages: [...state.messages, msg],
})),
updateLastAssistantMessage: (content) =>
set((state) => {
const messages = [...state.messages];
const lastIdx = messages.length - 1;
if (lastIdx >= 0 && messages[lastIdx].role === 'assistant') {
messages[lastIdx] = {
...messages[lastIdx],
content: messages[lastIdx].content + content,
};
}
return { messages };
}),
setTyping: (typing) => set({ isTyping: typing }),
setMode: (mode) => set({ currentMode: mode }),
clearMessages: () => set({ messages: [] }),
}));
```
**3. 聊天容器组件 (components/chat/ChatContainer.tsx)**
```tsx
import { useEffect, useCallback } from 'react';
import { useWebSocket } from '@/hooks/useWebSocket';
import { useChatStore } from '@/store/chatStore';
import { MessageList } from './MessageList';
import { ChatInput } from './ChatInput';
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
import { MoodIndicator } from '@/components/persona/MoodIndicator';
export function ChatContainer({ sessionId }: { sessionId: string }) {
const { messages, isTyping, addMessage, setTyping } = useChatStore();
const { connect, sendMessage, onMessage } = useWebSocket(sessionId);
useEffect(() => {
// 连接WebSocket (使用JWT token)
const token = localStorage.getItem('token');
if (token) connect(token);
// 监听回复
onMessage('onResponse', (msg) => {
addMessage({
id: msg.message_id,
role: 'assistant',
content: msg.text || '',
audioUrl: msg.full_audio_url,
segments: msg.segments?.map(s => ({
index: s.index,
text: s.text,
audioUrl: s.audio_url,
})),
timestamp: msg.timestamp,
isStreaming: false,
});
setTyping(false);
});
onMessage('onError', (msg) => {
addMessage({
id: msg.message_id,
role: 'assistant',
content: '啊……不好意思,人家刚才走神了。能再说一遍吗?♪',
timestamp: Date.now(),
});
setTyping(false);
});
}, [sessionId]);
const handleSend = useCallback((content: string, mode: string) => {
// 添加用户消息
addMessage({
id: `user-${Date.now()}`,
role: 'user',
content,
timestamp: Date.now(),
});
setTyping(true);
sendMessage(content, mode);
}, [sendMessage, addMessage, setTyping]);
return (
<div className="flex flex-col h-screen bg-[#FFFAF5] dark:bg-[#1a1a2e]">
{/* 顶部栏 */}
<header className="flex items-center justify-between px-6 py-3 border-b border-pink-100 dark:border-pink-900">
<div className="flex items-center gap-3">
<CyreneAvatar size="sm" />
<div>
<h1 className="text-lg font-semibold text-pink-600"></h1>
<MoodIndicator />
</div>
</div>
<span className="text-sm text-gray-400">🌸 </span>
</header>
{/* 消息列表 */}
<MessageList messages={messages} isTyping={isTyping} />
{/* 输入区域 */}
<ChatInput onSend={handleSend} disabled={isTyping} />
</div>
);
}
```
**4. 昔涟专属消息气泡 (components/chat/MessageBubble.tsx)**
```tsx
import { CyreneAvatar } from '@/components/persona/CyreneAvatar';
interface MessageBubbleProps {
role: 'user' | 'assistant';
content: string;
timestamp: number;
}
export function MessageBubble({ role, content, timestamp }: MessageBubbleProps) {
if (role === 'user') {
return (
<div className="flex justify-end px-4 py-2">
<div className="max-w-[70%] bg-pink-400 text-white rounded-2xl rounded-br-md px-4 py-2 shadow-sm">
<p className="text-sm leading-relaxed">{content}</p>
<span className="text-xs text-pink-100 mt-1 block">
{new Date(timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
);
}
return (
<div className="flex px-4 py-2 gap-3">
<CyreneAvatar size="sm" className="flex-shrink-0 mt-1" />
<div className="max-w-[70%]">
<div className="bg-white dark:bg-gray-800 rounded-2xl rounded-bl-md px-4 py-2 shadow-sm border border-pink-100 dark:border-pink-900">
<p className="text-sm leading-relaxed text-gray-700 dark:text-gray-200">
{content}
</p>
</div>
<span className="text-xs text-gray-400 mt-1 block ml-1">
{new Date(timestamp).toLocaleTimeString('zh-CN', { hour: '2-digit', minute: '2-digit' })}
</span>
</div>
</div>
);
}
```
### 1.3 昔涟人格文档放置
```bash
# 将之前设计的人格文档放入项目
cp persona_cyrene.yaml backend/data/persona/cyrene_v1.yaml
```
---
## Phase 2-5 开发路线图
```
┌─────────────────────────────────────────────────────────────────────────────┐
│ 📅 完整开发路线图 │
│ │
│ Week 1-2: Phase 1 — MVP 文字对话 │
│ ├── Day 1-2: 项目初始化、Docker环境、数据库 │
│ ├── Day 3-5: Gateway + WebSocket + 认证 │
│ ├── Day 6-8: AI Core (编排器 + 人格注入 + LLM适配) │
│ ├── Day 9-10: 记忆系统 (基础存储 + 检索) │
│ ├── Day 11-13: 前端聊天界面 + WebSocket对接 │
│ └── Day 14: 集成测试 + Docker部署 + MVP发布 │
│ │
│ Week 3-4: Phase 2 — 语音交互 │
│ ├── TTS服务集成 (Edge-TTS / GPT-SoVITS) │
│ ├── ASR服务集成 (Whisper) │
│ ├── 断句引擎 + 流式音频播放 │
│ ├── 语音助手模式前端适配 │
│ └── 角色音色调校 │
│ │
│ Week 5-6: Phase 3 — IoT集成 │
│ ├── 工具调用引擎 │
│ ├── IoT适配器 (MQTT / HomeAssistant API) │
│ ├── 设备控制面板UI │
│ ├── 场景联动 │
│ └── 「拟人化」设备操作包装层 │
│ │
│ Week 7-8: Phase 4 — 深度记忆 + 情感引擎 │
│ ├── 向量检索 (Qdrant语义搜索) │
│ ├── 好感度系统 │
│ ├── 心情引擎 │
│ ├── 主动行为调度器 │
│ ├── 记忆叙事化 │
│ └── 「我们的故事」页面 │
│ │
│ Week 9-10: Phase 5 — 多端覆盖 │
│ ├── PWA优化 (离线、推送) │
│ ├── React Native移动端 │
│ ├── Electron桌面端 │
│ └── 多端会话同步 │
│ │
│ Week 11+: 持续优化 │
│ ├── 性能优化 │
│ ├── 安全加固 │
│ ├── 更多IoT设备适配 │
│ ├── 角色商店 (支持切换人格) │
│ └── 社区功能 │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
```
---
## 开发规范
### Git分支策略
```
main # 生产就绪代码
└── develop # 开发主线
├── feat/mvp-chat # Phase 1
├── feat/voice # Phase 2
├── feat/iot # Phase 3
├── feat/memory-v2 # Phase 4
└── feat/multi-platform # Phase 5
提交信息格式:
feat: 添加WebSocket对话功能
fix: 修复人格Prompt中称呼错误
docs: 更新API文档
style: 调整昔涟头像动画
refactor: 重构记忆提取器
```
### 代码规范
```yaml
# .editorconfig (项目根目录)
root = true
[*]
indent_style = space
indent_size = 2
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.go]
indent_size = 4
[Makefile]
indent_style = tab
```
**Go代码规范:**
- 使用 `gofmt` + `golangci-lint`
- 错误处理:永远不忽略error
- 日志:使用 `slog` (Go标准库)
**TypeScript规范:**
- ESLint + Prettier
- 严格模式 (`strict: true`)
- 使用 `type` 而非 `interface` (除需要扩展的场景)
---
## 部署方案
### 生产环境 Docker Compose
```yaml
# docker-compose.yml (生产环境)
version: '3.8'
services:
caddy:
image: caddy:2-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- caddy_data:/data
gateway:
build: ./backend/gateway
environment:
- ENV=production
- POSTGRES_HOST=postgres
- REDIS_HOST=redis
# ... 其他环境变量
depends_on:
- postgres
- redis
restart: unless-stopped
ai-core:
build: ./backend/ai-core
environment:
- ENV=production
# ... 其他环境变量
depends_on:
- postgres
- redis
- qdrant
restart: unless-stopped
postgres:
image: pgvector/pgvector:pg16
volumes:
- pg_data:/var/lib/postgresql/data
- ./backend/data/init.sql:/docker-entrypoint-initdb.d/init.sql
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: cyrene_ai
restart: unless-stopped
redis:
image: redis:7-alpine
volumes:
- redis_data:/data
restart: unless-stopped
qdrant:
image: qdrant/qdrant:latest
volumes:
- qdrant_data:/qdrant/storage
restart: unless-stopped
minio:
image: minio/minio:latest
command: server /data
environment:
MINIO_ROOT_USER: ${MINIO_ACCESS_KEY}
MINIO_ROOT_PASSWORD: ${MINIO_SECRET_KEY}
volumes:
- minio_data:/data
restart: unless-stopped
volumes:
caddy_data:
pg_data:
redis_data:
qdrant_data:
minio_data:
```
### 迁移脚本
```bash
# scripts/migrate.sh —— 服务端迁移脚本
#!/bin/bash
set -e
echo "📦 昔涟AI助手 - 服务迁移脚本"
echo "=============================="
# 1. 停止服务
echo "1/4 停止当前服务..."
docker compose down
# 2. 备份数据
echo "2/4 备份数据目录..."
tar -czf "cyrene_backup_$(date +%Y%m%d_%H%M%S).tar.gz" backend/data/
# 3. 复制到新服务器 (手动步骤提示)
echo "3/4 请将以下文件复制到新服务器:"
echo " - 整个项目目录"
echo " - 或至少: docker-compose.yml, backend/data/, .env"
echo ""
echo " rsync -avz ./ user@new-server:/opt/cyrene-ai/"
# 4. 在新服务器上启动
echo "4/4 在新服务器上执行:"
echo " cd /opt/cyrene-ai"
echo " docker compose up -d"
echo ""
echo "✅ 迁移完成!昔涟的记忆完好无损~♪"
```
---
## 🎯 现在就可以开始的第一步
```bash
# ============================================
# 🚀 立即执行 —— 从零到第一个"你好,开拓者♪"
# ============================================
# 1. 创建项目
git init cyrene-ai-assistant
cd cyrene-ai-assistant
# 2. 复制本文档中的 Docker Compose 配置文件
# 保存为 docker-compose.dev.yml
# 3. 启动基础设施
docker compose -f docker-compose.dev.yml up -d
# 4. 初始化后端
mkdir -p backend/gateway/cmd backend/ai-core/cmd
cd backend
go mod init github.com/yourname/cyrene-ai
go work init
go work use ./gateway ./ai-core
# 5. 初始化前端
cd ../frontend
npm create vite@latest packages/web -- --template react-ts
cd packages/web
npm install
npm install zustand reconnecting-websocket axios
npm install -D tailwindcss @tailwindcss/vite
# 6. 将昔涟人格文档放入
cp /path/to/persona_cyrene.yaml ../backend/data/persona/cyrene_v1.yaml
# 7. 开始编码!
# 从 gateway/cmd/main.go 和 ai-core/internal/orchestrator/orchestrator.go 开始
echo ""
echo "🌸 昔涟AI助手项目初始化完成!"
echo " 昔涟: '开拓者,人家已经准备好了哦♪ 让我们一起创造属于我们的故事吧~'"
```
---
**总结:**
现在你有了:
- ✅ 完整的项目骨架
- ✅ 开发环境配置 (Docker Compose)
- ✅ 数据库Schema
- ✅ 后端核心代码框架 (Go)
- ✅ 前端核心代码框架 (React + TypeScript)
- ✅ WebSocket协议定义
- ✅ 昔涟人格系统设计
- ✅ 12周开发路线图
- ✅ 部署和迁移方案
**下一步行动建议:**
1. 按照上面的"立即执行"步骤初始化项目
2. 先让 Gateway 跑起来(一个简单的 WebSocket echo
3. 然后对接 AI Core(先做一个最简单的 LLM 调用 + 昔涟人格 Prompt
4. 前端连上 WebSocket,实现第一轮对话
5. 看到「开拓者,你好呀♪」的那一刻,就是一切的开始!
需要我详细展开某个模块的具体实现吗?比如AI编排器的完整代码、前端聊天组件的完整实现、或者昔涟人格文档的更详细版本?