Add project documentation and specs
This commit is contained in:
903
docs/superpowers/plans/2026-03-20-settings-register-plan.md
Normal file
903
docs/superpowers/plans/2026-03-20-settings-register-plan.md
Normal 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 6(LoginView 注册功能)
|
||||
4. Task 7(SettingsView)
|
||||
5. Task 8(路由和侧边栏)
|
||||
6. Task 9(数据库迁移)
|
||||
Reference in New Issue
Block a user