904 lines
25 KiB
Markdown
904 lines
25 KiB
Markdown
# 注册界面 + 设置界面 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 6(LoginView 注册功能)
|
||
4. Task 7(SettingsView)
|
||
5. Task 8(路由和侧边栏)
|
||
6. Task 9(数据库迁移)
|