1461 lines
48 KiB
Markdown
Executable File
1461 lines
48 KiB
Markdown
Executable File
好的喵~ 让我们正式开始!以下是一份完整的、可立即执行的开发框架。
|
||
|
||
---
|
||
|
||
# 🚀 昔涟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 1:MVP 开发 (文字对话 + 昔涟人格)
|
||
|
||
```
|
||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||
│ 🎯 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编排器的完整代码、前端聊天组件的完整实现、或者昔涟人格文档的更详细版本? |