Add project documentation and specs

This commit is contained in:
2026-03-21 10:13:45 +08:00
parent e76f0828b9
commit 3a7f4174ab
20 changed files with 11179 additions and 0 deletions

View File

@@ -0,0 +1,903 @@
# 注册界面 + 设置界面 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 实现用户注册界面和设置界面,支持多用户和用户级 LLM 配置
**Architecture:**
- 后端:扩展 User 模型,新建 settings router/service前端认证依赖现有 auth 机制
- 前端LoginView 添加注册 Tab新建 SettingsView 页面,复用现有 sci-fi 风格
- 数据User 表增加 JSON 字段存储 llm_config 和 scheduler_config
**Tech Stack:** FastAPI + SQLAlchemy + Vue 3 + axios + Pinia
---
## 文件总览
```
backend/
app/
models/
user.py # 修改:添加 llm_config, scheduler_config 字段
schemas/
settings.py # 新建Settings Pydantic schemas
routers/
settings.py # 新建settings API router
services/
settings_service.py # 新建:设置逻辑服务
frontend/
src/
api/
settings.ts # 新建settings API 客户端
views/
LoginView.vue # 修改:添加注册 Tab
SettingsView.vue # 新建:设置页面
router/
index.ts # 修改:添加 /settings 路由
components/
SidebarNav.vue # 修改:添加设置菜单
```
---
## Task 1: 后端 - User 模型扩展
**Files:**
- Modify: `backend/app/models/user.py`
- [ ] **Step 1: 添加 JSON 字段到 User 模型**
读取现有 User 模型,添加 llm_config 和 scheduler_config 字段:
```python
# 在 User 模型类中添加
from sqlalchemy import JSON
llm_config = Column(JSON, nullable=True) # 用户 LLM 配置
scheduler_config = Column(JSON, nullable=True) # 定时任务配置
```
- [ ] **Step 2: 设置默认值**
确保新用户创建时有默认配置(在 User 模型或 service 层处理)
- [ ] **Step 3: 提交**
```bash
git add backend/app/models/user.py
git commit -m "feat(settings): add llm_config and scheduler_config fields to User model"
```
---
## Task 2: 后端 - Settings Schema 定义
**Files:**
- Create: `backend/app/schemas/settings.py`
- [ ] **Step 1: 创建 settings schemas**
```python
from pydantic import BaseModel, Field
from typing import Optional
# LLM Provider 类型
LLMProviderType = Literal["openai", "claude", "ollama", "deepseek", "custom"]
LLMType = Literal["chat", "vlm", "embedding", "rerank"]
# 单个模型配置
class LLMModelConfig(BaseModel):
provider: LLMProviderType = "openai"
model: str = ""
base_url: str = ""
api_key: str = ""
# LLM 配置输入
class LLMConfigIn(BaseModel):
chat: Optional[LLMModelConfig] = None
vlm: Optional[LLMModelConfig] = None
embedding: Optional[LLMModelConfig] = None
rerank: Optional[LLMModelConfig] = None
# 定时任务配置
class SchedulerConfigIn(BaseModel):
daily_plan_time: Optional[str] = "08:00"
forum_scan_interval_minutes: Optional[int] = 30
todo_ai_generate_time: Optional[str] = "08:00"
enabled: Optional[bool] = True
# 用户资料更新
class ProfileUpdateIn(BaseModel):
full_name: Optional[str] = Field(None, min_length=2, max_length=50)
password: Optional[str] = Field(None, min_length=8)
current_password: Optional[str] = None # 修改密码时需要验证
# 完整设置输出
class SettingsOut(BaseModel):
profile: "UserOut" # 引用 auth.py 中的 UserOut
llm_config: Optional[dict] = None
scheduler_config: Optional[dict] = None
model_config = {"from_attributes": True}
# 测试 LLM 连接请求
class LLMTestIn(BaseModel):
type: LLMType
provider: LLMProviderType
model: str
base_url: str
api_key: str
```
- [ ] **Step 2: 提交**
```bash
git add backend/app/schemas/settings.py
git commit -m "feat(settings): add Pydantic schemas for settings API"
```
---
## Task 3: 后端 - Settings Service
**Files:**
- Create: `backend/app/services/settings_service.py`
- [ ] **Step 1: 创建设置服务**
主要功能:
1. `get_user_settings(user_id)` - 获取用户完整设置
2. `update_user_profile(user_id, data)` - 更新用户资料
3. `update_llm_config(user_id, config)` - 更新 LLM 配置
4. `update_scheduler_config(user_id, config)` - 更新定时任务配置
5. `test_llm_connection(data)` - 测试 LLM 连接
```python
import logging
from typing import Optional
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy import select
from app.models.user import User
from app.services.auth_service import verify_password, get_password_hash
from app.services.llm_service import get_llm
from langchain_core.messages import HumanMessage, SystemMessage
logger = logging.getLogger(__name__)
async def get_user_settings(user_id: str, db: AsyncSession) -> dict:
"""获取用户完整设置"""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
return None
return {
"profile": user,
"llm_config": user.llm_config or {},
"scheduler_config": user.scheduler_config or {}
}
async def update_user_profile(
user_id: str,
db: AsyncSession,
full_name: Optional[str] = None,
password: Optional[str] = None,
current_password: Optional[str] = None
) -> User:
"""更新用户资料"""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise ValueError("用户不存在")
if password:
if not current_password or not verify_password(current_password, user.hashed_password):
raise ValueError("当前密码错误")
user.hashed_password = get_password_hash(password)
if full_name:
user.full_name = full_name
await db.commit()
await db.refresh(user)
return user
async def update_llm_config(user_id: str, config: dict, db: AsyncSession) -> dict:
"""更新 LLM 配置"""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise ValueError("用户不存在")
current = user.llm_config or {}
# 合并配置
for key, value in config.items():
if value is not None:
current[key] = value
user.llm_config = current
await db.commit()
return current
async def update_scheduler_config(user_id: str, config: dict, db: AsyncSession) -> dict:
"""更新定时任务配置"""
result = await db.execute(select(User).where(User.id == user_id))
user = result.scalar_one_or_none()
if not user:
raise ValueError("用户不存在")
current = user.scheduler_config or {}
for key, value in config.items():
if value is not None:
current[key] = value
user.scheduler_config = current
await db.commit()
return current
async def test_llm_connection(
provider: str,
model: str,
base_url: str,
api_key: str
) -> dict:
"""测试 LLM 连接"""
try:
# 根据不同 provider 创建临时 LLM 实例并测试
if provider == "openai":
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
api_key=api_key,
model=model,
base_url=base_url or None,
timeout=30
)
elif provider == "claude":
from langchain_anthropic import ChatAnthropic
llm = ChatAnthropic(
api_key=api_key,
model=model,
timeout=30
)
elif provider == "ollama":
from langchain_ollama import ChatOllama
llm = ChatOllama(
base_url=base_url or "http://localhost:11434",
model=model,
timeout=30
)
elif provider == "deepseek":
from langchain_openai import ChatOpenAI
llm = ChatOpenAI(
api_key=api_key,
model=model,
base_url=base_url or "https://api.deepseek.com/v1",
timeout=30
)
else:
return {"success": False, "error": f"不支持的 provider: {provider}"}
# 简单测试调用
response = await llm.ainvoke([HumanMessage(content="Hi")])
return {"success": True, "message": f"连接成功,模型响应: {response.content[:50]}..."}
except Exception as e:
return {"success": False, "error": str(e)}
```
- [ ] **Step 2: 提交**
```bash
git add backend/app/services/settings_service.py
git commit -m "feat(settings): add settings service with LLM config management"
```
---
## Task 4: 后端 - Settings Router
**Files:**
- Create: `backend/app/routers/settings.py`
- [ ] **Step 1: 创建 settings router**
```python
from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from app.models.user import User
from app.routers.auth import get_current_user
from app.schemas.settings import (
SettingsOut, ProfileUpdateIn, LLMConfigIn, SchedulerConfigIn, LLMTestIn
)
from app.services.settings_service import (
get_user_settings, update_user_profile, update_llm_config,
update_scheduler_config, test_llm_connection
)
router = APIRouter(prefix="/api/settings", tags=["设置"])
@router.get("", response_model=SettingsOut)
async def get_settings(
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
settings = await get_user_settings(current_user.id, db)
if not settings:
raise HTTPException(status_code=404, detail="用户不存在")
return settings
@router.put("/profile")
async def update_profile(
data: ProfileUpdateIn,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
try:
user = await update_user_profile(
current_user.id, db,
full_name=data.full_name,
password=data.password,
current_password=data.current_password
)
return user
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.put("/llm")
async def update_llm(
data: LLMConfigIn,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
try:
config = await update_llm_config(current_user.id, data.model_dump(exclude_none=True), db)
return {"llm_config": config}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.post("/llm/test")
async def test_llm(
data: LLMTestIn,
current_user: User = Depends(get_current_user),
):
result = await test_llm_connection(
provider=data.provider,
model=data.model,
base_url=data.base_url,
api_key=data.api_key
)
return result
@router.put("/scheduler")
async def update_scheduler(
data: SchedulerConfigIn,
current_user: User = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
try:
config = await update_scheduler_config(
current_user.id,
data.model_dump(exclude_none=True),
db
)
return {"scheduler_config": config}
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
```
- [ ] **Step 2: 注册 router 到 main.py 和 routers/__init__.py**
`backend/app/routers/__init__.py` 添加:
```python
from app.routers.settings import router as settings_router
```
`backend/app/main.py` 添加:
```python
from app.routers.settings import router as settings_router
# ...
app.include_router(settings_router)
```
- [ ] **Step 3: 提交**
```bash
git add backend/app/routers/settings.py backend/app/routers/__init__.py backend/app/main.py
git commit -m "feat(settings): add settings router with profile, LLM and scheduler endpoints"
```
---
## Task 5: 前端 - Settings API 客户端
**Files:**
- Create: `frontend/src/api/settings.ts`
- [ ] **Step 1: 创建 settings API 客户端**
```typescript
import api from './index'
export type LLMProvider = 'openai' | 'claude' | 'ollama' | 'deepseek' | 'custom'
export type LLMType = 'chat' | 'vlm' | 'embedding' | 'rerank'
export interface LLMModelConfig {
provider: LLMProvider
model: string
base_url: string
api_key: string
}
export interface LLMConfig {
chat?: LLMModelConfig
vlm?: LLMModelConfig
embedding?: LLMModelConfig
rerank?: LLMModelConfig
}
export interface SchedulerConfig {
daily_plan_time?: string
forum_scan_interval_minutes?: number
todo_ai_generate_time?: string
enabled?: boolean
}
export interface ProfileUpdate {
full_name?: string
password?: string
current_password?: string
}
export interface SettingsResponse {
profile: {
id: string
email: string
full_name: string
created_at: string
}
llm_config: LLMConfig
scheduler_config: SchedulerConfig
}
export const settingsApi = {
// 获取设置
get() {
return api.get<SettingsResponse>('/api/settings')
},
// 更新资料
updateProfile(data: ProfileUpdate) {
return api.put('/api/settings/profile', data)
},
// 更新 LLM 配置
updateLLM(config: Partial<LLMConfig>) {
return api.put('/api/settings/llm', config)
},
// 测试 LLM 连接
testLLM(data: { type: LLMType } & LLMModelConfig) {
return api.post('/api/settings/llm/test', data)
},
// 更新定时任务配置
updateScheduler(config: Partial<SchedulerConfig>) {
return api.put('/api/settings/scheduler', config)
},
}
```
- [ ] **Step 2: 提交**
```bash
git add frontend/src/api/settings.ts
git commit -m "feat(settings): add settings API client"
```
---
## Task 6: 前端 - LoginView 注册功能
**Files:**
- Modify: `frontend/src/views/LoginView.vue`
- [ ] **Step 1: 添加注册 Tab 和表单**
在 script setup 中添加:
```typescript
const isLogin = ref(true)
const registerEmail = ref('')
const registerPassword = ref('')
const registerConfirmPassword = ref('')
const registerName = ref('')
const isRegistering = ref(false)
const registerError = ref('')
// 密码强度计算
function getPasswordStrength(pwd: string): { level: 'weak' | 'medium' | 'strong', text: string } {
if (pwd.length < 8) return { level: 'weak', text: '太短' }
let score = 0
if (pwd.length >= 8) score++
if (pwd.length >= 12) score++
if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) score++
if (/\d/.test(pwd)) score++
if (/[^a-zA-Z0-9]/.test(pwd)) score++
if (score <= 2) return { level: 'weak', text: '弱' }
if (score <= 3) return { level: 'medium', text: '中' }
return { level: 'strong', text: '强' }
}
const passwordStrength = computed(() => getPasswordStrength(registerPassword.value))
async function handleRegister() {
if (registerPassword.value !== registerConfirmPassword.value) {
registerError.value = '两次密码输入不一致'
return
}
if (registerPassword.value.length < 8) {
registerError.value = '密码至少需要8个字符'
return
}
try {
registerError.value = ''
isRegistering.value = true
await authApi.register({
email: registerEmail.value,
password: registerPassword.value,
full_name: registerName.value
})
// 注册成功后自动登录
await auth.login(registerEmail.value, registerPassword.value)
router.push('/chat')
} catch (e: unknown) {
registerError.value = (e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '注册失败'
} finally {
isRegistering.value = false
}
}
```
在 template 中添加注册表单(与登录表单并列,用 v-if 切换)
- [ ] **Step 2: 提交**
```bash
git add frontend/src/views/LoginView.vue
git commit -m "feat(auth): add registration form to LoginView"
```
---
## Task 7: 前端 - SettingsView 页面
**Files:**
- Create: `frontend/src/views/SettingsView.vue`
- [ ] **Step 1: 创建设置页面**
页面结构:
```vue
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { settingsApi, type LLMConfig, type SchedulerConfig, type LLMModelConfig } from '@/api/settings'
import { Save, RotateCcw, Eye, EyeOff, Play } from 'lucide-vue-next'
// 状态
const loading = ref(false)
const saving = ref(false)
const showApiKey = ref<Record<string, boolean>>({})
// 用户资料
const profile = ref({ email: '', full_name: '' })
const newPassword = ref('')
// LLM 配置
const llmConfig = ref<LLMConfig>({
chat: { provider: 'openai', model: 'gpt-4o', base_url: '', api_key: '' },
vlm: { provider: 'openai', model: 'gpt-4o', base_url: '', api_key: '' },
embedding: { provider: 'openai', model: 'text-embedding-3-small', base_url: '', api_key: '' },
rerank: { provider: 'openai', model: 'bge-reranker-v2', base_url: '', api_key: '' },
})
// 定时任务配置
const schedulerConfig = ref<SchedulerConfig>({
daily_plan_time: '08:00',
forum_scan_interval_minutes: 30,
todo_ai_generate_time: '08:00',
enabled: true,
})
// 加载设置
async function loadSettings() {
loading.value = true
try {
const res = await settingsApi.get()
profile.value = { ...res.data.profile }
llmConfig.value = { ...res.data.llm_config }
schedulerConfig.value = { ...res.data.scheduler_config }
} catch (e) {
console.error('加载设置失败', e)
} finally {
loading.value = false
}
}
// 保存资料
async function saveProfile() {
saving.value = true
try {
await settingsApi.updateProfile({ full_name: profile.value.full_name })
if (newPassword.value) {
const currentPwd = prompt('请输入当前密码以确认修改:')
if (!currentPwd) {
alert('密码修改已取消')
return
}
await settingsApi.updateProfile({
password: newPassword.value,
current_password: currentPwd
})
newPassword.value = ''
alert('密码修改成功')
}
} catch (e: unknown) {
alert((e as { response?: { data?: { detail?: string } } })?.response?.data?.detail || '保存失败')
} finally {
saving.value = false
}
}
// 保存 LLM 配置
async function saveLLM() {
saving.value = true
try {
await settingsApi.updateLLM(llmConfig.value)
} finally {
saving.value = false
}
}
// 测试 LLM 连接
async function testLLM(type: string, config: LLMModelConfig) {
try {
const res = await settingsApi.testLLM({ type, ...config })
alert(res.data.success ? `成功: ${res.data.message}` : `失败: ${res.data.error}`)
} catch (e) {
alert('测试连接失败')
}
}
// 保存定时任务配置
async function saveScheduler() {
saving.value = true
try {
await settingsApi.updateScheduler(schedulerConfig.value)
} finally {
saving.value = false
}
}
// Provider 默认值
function getDefaultBaseUrl(provider: string) {
switch (provider) {
case 'ollama': return 'http://localhost:11434'
case 'openai': return 'https://api.openai.com/v1'
case 'claude': return 'https://api.anthropic.com'
default: return ''
}
}
onMounted(loadSettings)
</script>
<template>
<div class="settings-view">
<!-- Header -->
<div class="view-header">
<span class="header-title">SETTINGS</span>
</div>
<!-- Content -->
<div class="settings-content">
<!-- Profile Section -->
<div class="settings-card">
<div class="card-title">PROFILE</div>
<div class="form-group">
<label>Email</label>
<input v-model="profile.email" type="email" disabled />
</div>
<div class="form-group">
<label>Name</label>
<input v-model="profile.full_name" type="text" />
</div>
<div class="form-group">
<label>New Password</label>
<input v-model="newPassword" type="password" placeholder="Leave empty to keep current" />
</div>
<button class="save-btn" @click="saveProfile" :disabled="saving">
{{ saving ? 'Saving...' : 'Save Profile' }}
</button>
</div>
<!-- LLM Config Section -->
<div v-for="(config, type) in llmConfig" :key="type" class="settings-card">
<div class="card-title">{{ type.toUpperCase() }}</div>
<div class="form-row">
<div class="form-group">
<label>Provider</label>
<select v-model="config.provider">
<option value="openai">OpenAI</option>
<option value="claude">Claude</option>
<option value="ollama">Ollama</option>
<option value="deepseek">DeepSeek</option>
<option value="custom">Custom</option>
</select>
</div>
<div class="form-group">
<label>Model</label>
<input v-model="config.model" type="text" />
</div>
</div>
<div class="form-group">
<label>Base URL</label>
<input v-model="config.base_url" type="text" :placeholder="getDefaultBaseUrl(config.provider)" />
</div>
<div class="form-group">
<label>API Key</label>
<div class="input-with-toggle">
<input v-model="config.api_key" :type="showApiKey[type] ? 'text' : 'password'" />
<button @click="showApiKey[type] = !showApiKey[type]" class="toggle-btn">
<Eye v-if="!showApiKey[type]" :size="14" />
<EyeOff v-else :size="14" />
</button>
</div>
</div>
<button class="test-btn" @click="testLLM(type, config)">
<Play :size="12" /> Test
</button>
</div>
<!-- Scheduler Section -->
<div class="settings-card">
<div class="card-title">SCHEDULER</div>
<div class="form-row">
<div class="form-group">
<label>Daily Plan Time</label>
<input v-model="schedulerConfig.daily_plan_time" type="time" />
</div>
<div class="form-group">
<label>Todo AI Generate Time</label>
<input v-model="schedulerConfig.todo_ai_generate_time" type="time" />
</div>
</div>
<div class="form-group">
<label>Forum Scan Interval (minutes)</label>
<input v-model.number="schedulerConfig.forum_scan_interval_minutes" type="number" min="5" max="1440" />
</div>
<div class="form-group toggle-group">
<label>Scheduler Enabled</label>
<button
class="toggle-btn"
:class="{ active: schedulerConfig.enabled }"
@click="schedulerConfig.enabled = !schedulerConfig.enabled"
>
<span class="toggle-knob" />
</button>
</div>
<button class="save-btn" @click="saveScheduler" :disabled="saving">
{{ saving ? 'Saving...' : 'Save Scheduler' }}
</button>
</div>
</div>
</div>
</template>
```
样式部分复用 AgentView 的 sci-fi 风格,保持一致。
- [ ] **Step 2: 提交**
```bash
git add frontend/src/views/SettingsView.vue
git commit -m "feat(settings): add SettingsView page with profile, LLM and scheduler config"
```
---
## Task 8: 前端 - 路由和侧边栏
**Files:**
- Modify: `frontend/src/router/index.ts`
- Modify: `frontend/src/components/SidebarNav.vue`
- [ ] **Step 1: 添加 /settings 路由**
在 children 数组中添加:
```typescript
{
path: 'settings',
name: 'settings',
component: () => import('@/views/SettingsView.vue'),
}
```
- [ ] **Step 2: 添加设置菜单项**
在 navItems 中添加:
```typescript
{ name: '设置', path: '/settings', icon: Settings },
```
导入 Settings 图标:
```typescript
import { Settings } from 'lucide-vue-next'
```
- [ ] **Step 3: 提交**
```bash
git add frontend/src/router/index.ts frontend/src/components/SidebarNav.vue
git commit -m "feat(settings): add /settings route and sidebar menu"
```
---
## Task 9: 数据库迁移
- [ ] **Step 1: 创建迁移 SQL**
由于使用 SQLAlchemy 的 `init_db()` 会在启动时自动创建表,但现有数据库不会自动添加新字段。需要:
1. 直接在数据库上执行 ALTER TABLE
```sql
ALTER TABLE users ADD COLUMN llm_config TEXT;
ALTER TABLE users ADD COLUMN scheduler_config TEXT;
```
2. 或通过 Python 脚本:
```python
import asyncio
from app.database import engine
async def migrate():
async with engine.begin() as conn:
await conn.execute(text('ALTER TABLE users ADD COLUMN llm_config TEXT'))
await conn.execute(text('ALTER TABLE users ADD COLUMN scheduler_config TEXT'))
print('Migration complete')
asyncio.run(migrate())
```
- [ ] **Step 2: 提交迁移脚本**
```bash
git add docs/superpowers/plans/2026-03-20-settings-migration.md
git commit -m "feat(settings): add database migration for user settings fields"
```
---
## 验证清单
完成所有 Task 后,验证以下内容:
1. **注册功能** - 可以通过注册页面创建新账号
2. **登录功能** - 新老用户都可以正常登录
3. **设置页面** - 可以访问 /settings 页面
4. **资料修改** - 用户名、密码可以修改
5. **LLM 配置** - 四种模型配置可以保存
6. **LLM 测试** - 测试连接功能正常
7. **定时任务** - 时间间隔可以修改
8. **配置持久化** - 重新登录后配置保留
9. **UI 风格** - 设置页面风格与其他页面一致
---
## 实现顺序建议
1. Task 1 → 2 → 3 → 4后端核心
2. Task 5前端 API
3. Task 6LoginView 注册功能)
4. Task 7SettingsView
5. Task 8路由和侧边栏
6. Task 9数据库迁移