# 交互广场重新设计实现计划 > **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:** 将论坛重构为三个AI驱动的智能板块:AI学习、AI建议、AI交互 **Architecture:** 前端三板块布局,后端三个Service处理业务逻辑,数据库新增三张表存储学习记录、建议和交互主题 **Tech Stack:** Vue 3 + TypeScript + FastAPI + SQLAlchemy + LLM --- ## 文件结构 ``` backend/app/ ├── models/ │ ├── learning_record.py # 新建 - 学习记录模型 │ ├── suggestion.py # 新建 - 建议模型 │ └── interactive_topic.py # 新建 - 交互主题模型 ├── schemas/ │ ├── learning.py # 新建 - LearningRecord Pydantic schemas │ ├── suggestion.py # 新建 - Suggestion Pydantic schemas │ └── interactive.py # 新建 - InteractiveTopic Pydantic schemas ├── services/ │ ├── learning_service.py # 新建 - AI学习服务 │ ├── suggestion_service.py # 新建 - AI建议服务 │ └── interactive_service.py # 新建 - AI交互服务 └── routers/ └── forum.py # 修改 - 添加新接口 frontend/src/ ├── api/ │ └── forum.ts # 修改 - 添加新API方法 ├── views/ │ └── ForumView.vue # 修改 - 重写为三板块布局 └── components/forum/ ├── LearningSection.vue # 新建 - AI学习板块 │ ├── LearningSummaryCard.vue # 新建 - 今日摘要卡片 │ ├── LearningTimeline.vue # 新建 - 学习历史时间线 │ └── LearningStats.vue # 新建 - 图谱更新统计 ├── SuggestionSection.vue # 新建 - AI建议板块 │ └── SuggestionCard.vue # 新建 - 建议卡片 └── InteractiveSection.vue # 新建 - AI交互板块 └── LearningInput.vue # 新建 - 学习主题输入框 ``` --- ## Task 1: 创建 LearningRecord 模型 **Files:** - Create: `backend/app/models/learning_record.py` - [ ] **Step 1: 创建 learning_record.py** ```python from sqlalchemy import Column, String, Text, ForeignKey, JSON from app.models.base import BaseModel class LearningRecord(BaseModel): __tablename__ = "learning_records" user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) learning_type = Column(String(50), nullable=False) # concept, technology, workflow topic = Column(String(500), nullable=False) # 学习主题 summary = Column(Text, nullable=False) # AI生成的学习摘要 source = Column(String(50), nullable=False) # conversation, kanban, knowledge source_ids = Column(JSON, nullable=True) # 来源ID列表 kg_nodes_created = Column(JSON, nullable=True) # 创建的KGNode ID列表 ``` - [ ] **Step 2: 在 models/__init__.py 中导出** ```python from app.models.learning_record import LearningRecord ``` --- ## Task 2: 创建 Suggestion 模型 **Files:** - Create: `backend/app/models/suggestion.py` - [ ] **Step 1: 创建 suggestion.py** ```python from sqlalchemy import Column, String, Text, ForeignKey, JSON, Boolean from app.models.base import BaseModel class Suggestion(BaseModel): __tablename__ = "suggestions" user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) suggestion_type = Column(String(50), nullable=False) # knowledge, efficiency, skill title = Column(String(500), nullable=False) # 建议标题 content = Column(Text, nullable=False) # 建议内容 source_data = Column(JSON, nullable=True) # 分析依据 is_read = Column(Boolean, default=False) # 是否已读 is_dismissed = Column(Boolean, default=False) # 是否忽略 ``` - [ ] **Step 2: 在 models/__init__.py 中导出** ```python from app.models.suggestion import Suggestion ``` --- ## Task 3: 创建 InteractiveTopic 模型 **Files:** - Create: `backend/app/models/interactive_topic.py` - [ ] **Step 1: 创建 interactive_topic.py** ```python from sqlalchemy import Column, String, Text, ForeignKey, JSON, DateTime from sqlalchemy.orm import relationship from app.models.base import BaseModel class InteractiveTopic(BaseModel): __tablename__ = "interactive_topics" user_id = Column(String(36), ForeignKey("users.id"), nullable=False, index=True) topic = Column(String(500), nullable=False) # 学习主题 status = Column(String(50), nullable=False) # pending, learning, completed, failed result = Column(Text, nullable=True) # 学习结果/报告 kg_nodes_created = Column(JSON, nullable=True) # 创建的KGNode ID列表 source = Column(String(50), nullable=False) # user_initiated, ai_proactive completed_at = Column(DateTime, nullable=True) ``` - [ ] **Step 2: 在 models/__init__.py 中导出** ```python from app.models.interactive_topic import InteractiveTopic ``` --- ## Task 4: 创建 Pydantic Schemas **Files:** - Create: `backend/app/schemas/learning.py` - Create: `backend/app/schemas/suggestion.py` - Create: `backend/app/schemas/interactive.py` - [ ] **Step 1: 创建 learning.py** ```python from pydantic import BaseModel from typing import Optional class LearningRecordOut(BaseModel): id: str learning_type: str topic: str summary: str source: str source_ids: Optional[dict] = None kg_nodes_created: Optional[list[str]] = None created_at: str model_config = {"from_attributes": True} class LearningSummaryOut(BaseModel): summary: str records: list[LearningRecordOut] stats: dict class LearningHistoryOut(BaseModel): records: list[LearningRecordOut] total: int ``` - [ ] **Step 2: 创建 suggestion.py** ```python from pydantic import BaseModel from typing import Optional class SuggestionOut(BaseModel): id: str suggestion_type: str title: str content: str source_data: Optional[dict] = None is_read: bool is_dismissed: bool created_at: str model_config = {"from_attributes": True} class SuggestionListOut(BaseModel): suggestions: list[SuggestionOut] ``` - [ ] **Step 3: 创建 interactive.py** ```python from pydantic import BaseModel from typing import Optional class InteractiveTopicOut(BaseModel): id: str topic: str status: str result: Optional[str] = None kg_nodes_created: Optional[list[str]] = None source: str created_at: str completed_at: Optional[str] = None model_config = {"from_attributes": True} class InteractiveTopicsOut(BaseModel): user_initiated: list[InteractiveTopicOut] ai_proactive: list[InteractiveTopicOut] class LearnRequest(BaseModel): topic: str class LearnResponse(BaseModel): topic_id: str status: str ``` --- ## Task 5: 创建 LearningService **Files:** - Create: `backend/app/services/learning_service.py` - [ ] **Step 1: 创建 learning_service.py** ```python from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from app.models.learning_record import LearningRecord from app.models.conversation import Message from app.models.task import Task from app.models.knowledge_graph import KGNode from app.core.llm import get_llm_client from datetime import datetime, timedelta from typing import Optional class LearningService: def __init__(self, db: AsyncSession): self.db = db self.llm = get_llm_client() async def get_summary(self, user_id: str) -> dict: """获取今日学习摘要""" today = datetime.utcnow().date() today_start = datetime.combine(today, datetime.min.time()) result = await self.db.execute( select(LearningRecord) .where( LearningRecord.user_id == user_id, LearningRecord.created_at >= today_start ) .order_by(desc(LearningRecord.created_at)) ) records = result.scalars().all() if not records: return { "summary": "今日暂无学习记录", "records": [], "stats": {"nodes_created": 0, "edges_created": 0} } # 生成摘要 topics = [r.topic for r in records] summary = f"今日学习了 {len(topics)} 个主题:{', '.join(topics[:3])}" # 统计 nodes_count = sum(len(r.kg_nodes_created or []) for r in records) return { "summary": summary, "records": [self._record_to_dict(r) for r in records], "stats": {"nodes_created": nodes_count, "edges_created": 0} } async def get_history(self, user_id: str, page: int = 1, limit: int = 20) -> dict: """获取学习历史""" offset = (page - 1) * limit result = await self.db.execute( select(LearningRecord) .where(LearningRecord.user_id == user_id) .order_by(desc(LearningRecord.created_at)) .limit(limit) .offset(offset) ) records = result.scalars().all() count_result = await self.db.execute( select(LearningRecord) .where(LearningRecord.user_id == user_id) ) total = len(count_result.scalars().all()) return { "records": [self._record_to_dict(r) for r in records], "total": total } def _record_to_dict(self, record: LearningRecord) -> dict: return { "id": record.id, "learning_type": record.learning_type, "topic": record.topic, "summary": record.summary, "source": record.source, "source_ids": record.source_ids, "kg_nodes_created": record.kg_nodes_created, "created_at": record.created_at.isoformat() if record.created_at else None } ``` --- ## Task 6: 创建 SuggestionService **Files:** - Create: `backend/app/services/suggestion_service.py` - [ ] **Step 1: 创建 suggestion_service.py** ```python from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select from app.models.suggestion import Suggestion from app.models.knowledge_graph import KGNode from app.core.llm import get_llm_client from datetime import datetime class SuggestionService: def __init__(self, db: AsyncSession): self.db = db self.llm = get_llm_client() async def get_suggestions(self, user_id: str) -> list[dict]: """获取用户的所有建议""" result = await self.db.execute( select(Suggestion) .where( Suggestion.user_id == user_id, Suggestion.is_dismissed == False ) .order_by(Suggestion.created_at.desc()) ) suggestions = result.scalars().all() return [self._suggestion_to_dict(s) for s in suggestions] async def get_suggestion(self, suggestion_id: str, user_id: str) -> dict: """获取单个建议""" result = await self.db.execute( select(Suggestion).where( Suggestion.id == suggestion_id, Suggestion.user_id == user_id ) ) suggestion = result.scalar_one_or_none() if not suggestion: return None return self._suggestion_to_dict(suggestion) async def mark_read(self, suggestion_id: str, user_id: str) -> bool: """标记建议为已读""" result = await self.db.execute( select(Suggestion).where( Suggestion.id == suggestion_id, Suggestion.user_id == user_id ) ) suggestion = result.scalar_one_or_none() if not suggestion: return False suggestion.is_read = True await self.db.commit() return True async def dismiss(self, suggestion_id: str, user_id: str) -> bool: """忽略建议""" result = await self.db.execute( select(Suggestion).where( Suggestion.id == suggestion_id, Suggestion.user_id == user_id ) ) suggestion = result.scalar_one_or_none() if not suggestion: return False suggestion.is_dismissed = True await self.db.commit() return True def _suggestion_to_dict(self, suggestion: Suggestion) -> dict: return { "id": suggestion.id, "suggestion_type": suggestion.suggestion_type, "title": suggestion.title, "content": suggestion.content, "source_data": suggestion.source_data, "is_read": suggestion.is_read, "is_dismissed": suggestion.is_dismissed, "created_at": suggestion.created_at.isoformat() if suggestion.created_at else None } ``` --- ## Task 7: 创建 InteractiveService **Files:** - Create: `backend/app/services/interactive_service.py` - [ ] **Step 1: 创建 interactive_service.py** ```python from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, desc from app.models.interactive_topic import InteractiveTopic from app.core.llm import get_llm_client from datetime import datetime class InteractiveService: def __init__(self, db: AsyncSession): self.db = db self.llm = get_llm_client() async def get_topics(self, user_id: str) -> dict: """获取交互主题列表""" result = await self.db.execute( select(InteractiveTopic) .where(InteractiveTopic.user_id == user_id) .order_by(desc(InteractiveTopic.created_at)) ) topics = result.scalars().all() user_initiated = [t for t in topics if t.source == "user_initiated"] ai_proactive = [t for t in topics if t.source == "ai_proactive"] return { "user_initiated": [self._topic_to_dict(t) for t in user_initiated], "ai_proactive": [self._topic_to_dict(t) for t in ai_proactive] } async def initiate_learning(self, user_id: str, topic: str) -> dict: """用户发起学习""" interactive_topic = InteractiveTopic( user_id=user_id, topic=topic, status="pending", source="user_initiated" ) self.db.add(interactive_topic) await self.db.commit() await self.db.refresh(interactive_topic) # 触发异步学习(实际实现中可能用后台任务) # 这里简化为直接返回 return self._topic_to_dict(interactive_topic) async def get_topic_detail(self, topic_id: str, user_id: str) -> dict: """获取主题详情""" result = await self.db.execute( select(InteractiveTopic).where( InteractiveTopic.id == topic_id, InteractiveTopic.user_id == user_id ) ) topic = result.scalar_one_or_none() if not topic: return None return self._topic_to_dict(topic) def _topic_to_dict(self, topic: InteractiveTopic) -> dict: return { "id": topic.id, "topic": topic.topic, "status": topic.status, "result": topic.result, "kg_nodes_created": topic.kg_nodes_created, "source": topic.source, "created_at": topic.created_at.isoformat() if topic.created_at else None, "completed_at": topic.completed_at.isoformat() if topic.completed_at else None } ``` --- ## Task 8: 修改 Forum Router **Files:** - Modify: `backend/app/routers/forum.py` - [ ] **Step 1: 添加新接口** 在文件开头添加导入: ```python from app.schemas.learning import LearningSummaryOut, LearningHistoryOut from app.schemas.suggestion import SuggestionOut, SuggestionListOut from app.schemas.interactive import InteractiveTopicOut, InteractiveTopicsOut, LearnRequest, LearnResponse from app.services.learning_service import LearningService from app.services.suggestion_service import SuggestionService from app.services.interactive_service import InteractiveService ``` - [ ] **Step 2: 添加 Learning 接口** ```python @router.get("/learning/summary", response_model=LearningSummaryOut) async def get_learning_summary( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = LearningService(db) return await service.get_summary(current_user.id) @router.get("/learning/history", response_model=LearningHistoryOut) async def get_learning_history( page: int = 1, limit: int = 20, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = LearningService(db) return await service.get_history(current_user.id, page, limit) ``` - [ ] **Step 3: 添加 Suggestion 接口** ```python @router.get("/suggestions", response_model= dict) async def list_suggestions( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = SuggestionService(db) suggestions = await service.get_suggestions(current_user.id) return {"suggestions": suggestions} @router.get("/suggestions/{suggestion_id}", response_model=SuggestionOut) async def get_suggestion( suggestion_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = SuggestionService(db) suggestion = await service.get_suggestion(suggestion_id, current_user.id) if not suggestion: raise HTTPException(status_code=404, detail="建议不存在") return suggestion @router.patch("/suggestions/{suggestion_id}/read") async def mark_suggestion_read( suggestion_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = SuggestionService(db) success = await service.mark_read(suggestion_id, current_user.id) if not success: raise HTTPException(status_code=404, detail="建议不存在") return {"status": "ok"} @router.delete("/suggestions/{suggestion_id}/dismiss") async def dismiss_suggestion( suggestion_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = SuggestionService(db) success = await service.dismiss(suggestion_id, current_user.id) if not success: raise HTTPException(status_code=404, detail="建议不存在") return {"status": "ok"} ``` - [ ] **Step 4: 添加 Interactive 接口** ```python @router.get("/interactive/topics", response_model=InteractiveTopicsOut) async def get_interactive_topics( current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = InteractiveService(db) return await service.get_topics(current_user.id) @router.post("/interactive/learn", response_model=InteractiveTopicOut) async def initiate_learning( data: LearnRequest, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = InteractiveService(db) result = await service.initiate_learning(current_user.id, data.topic) return result @router.get("/interactive/topics/{topic_id}", response_model=InteractiveTopicOut) async def get_interactive_topic( topic_id: str, current_user: User = Depends(get_current_user), db: AsyncSession = Depends(get_db), ): service = InteractiveService(db) topic = await service.get_topic_detail(topic_id, current_user.id) if not topic: raise HTTPException(status_code=404, detail="主题不存在") return topic ``` --- ## Task 9: 更新前端 API **Files:** - Modify: `frontend/src/api/forum.ts` - [ ] **Step 1: 添加新类型和API方法** ```typescript // Types export interface LearningRecord { id: string learning_type: 'concept' | 'technology' | 'workflow' topic: string summary: string source: string source_ids?: { conversation_ids?: string[]; task_ids?: string[] } kg_nodes_created?: string[] created_at: string } export interface LearningSummary { summary: string records: LearningRecord[] stats: { nodes_created: number edges_created: number } } export interface Suggestion { id: string suggestion_type: 'knowledge' | 'efficiency' | 'skill' title: string content: string source_data?: Record is_read: boolean is_dismissed: boolean created_at: string } export interface InteractiveTopic { id: string topic: string status: 'pending' | 'learning' | 'completed' | 'failed' result?: string kg_nodes_created?: string[] source: 'user_initiated' | 'ai_proactive' created_at: string completed_at?: string } // API methods export const forumApi = { // ... existing methods ... // Learning fetchLearningSummary() { return api.get('/api/forum/learning/summary') }, fetchLearningHistory(params: { page: number, limit: number }) { return api.get<{ records: LearningRecord[], total: number }>('/api/forum/learning/history', { params }) }, // Suggestions fetchSuggestions() { return api.get<{ suggestions: Suggestion[] }>('/api/forum/suggestions') }, getSuggestion(id: string) { return api.get(`/api/forum/suggestions/${id}`) }, markSuggestionRead(id: string) { return api.patch(`/api/forum/suggestions/${id}/read`) }, dismissSuggestion(id: string) { return api.delete(`/api/forum/suggestions/${id}/dismiss`) }, // Interactive fetchInteractiveTopics() { return api.get<{ user_initiated: InteractiveTopic[], ai_proactive: InteractiveTopic[] }>('/api/forum/interactive/topics') }, initiateLearning(topic: string) { return api.post('/api/forum/interactive/learn', { topic }) }, getTopicDetail(id: string) { return api.get(`/api/forum/interactive/topics/${id}`) }, } ``` --- ## Task 10: 创建 LearningSection 组件 **Files:** - Create: `frontend/src/components/forum/LearningSection.vue` - Create: `frontend/src/components/forum/LearningSummaryCard.vue` - Create: `frontend/src/components/forum/LearningTimeline.vue` - Create: `frontend/src/components/forum/LearningStats.vue` - [ ] **Step 1: 创建 LearningSummaryCard.vue** ```vue ``` - [ ] **Step 2: 创建 LearningTimeline.vue** ```vue ``` - [ ] **Step 3: 创建 LearningStats.vue** ```vue ``` - [ ] **Step 4: 创建 LearningSection.vue** ```vue ``` --- ## Task 11: 创建 SuggestionSection 组件 **Files:** - Create: `frontend/src/components/forum/SuggestionSection.vue` - Create: `frontend/src/components/forum/SuggestionCard.vue` - [ ] **Step 1: 创建 SuggestionCard.vue** ```vue ``` - [ ] **Step 2: 创建 SuggestionSection.vue** ```vue ``` --- ## Task 12: 创建 InteractiveSection 组件 **Files:** - Create: `frontend/src/components/forum/InteractiveSection.vue` - Create: `frontend/src/components/forum/LearningInput.vue` - [ ] **Step 1: 创建 LearningInput.vue** ```vue ``` - [ ] **Step 2: 创建 InteractiveSection.vue** ```vue ``` --- ## Task 13: 重写 ForumView.vue **Files:** - Modify: `frontend/src/views/ForumView.vue` - [ ] **Step 1: 重写 ForumView.vue** ```vue ``` --- ## Task 14: 验证和测试 - [ ] **Step 1: 后端语法检查** ```bash cd backend && python -m py_compile app/models/learning_record.py app/models/suggestion.py app/models/interactive_topic.py app/services/learning_service.py app/services/suggestion_service.py app/services/interactive_service.py app/routers/forum.py ``` - [ ] **Step 2: 前端 TypeScript 检查** ```bash cd frontend && npx vue-tsc --noEmit ``` - [ ] **Step 3: 启动服务测试** ```bash # 后端 cd backend && python -m uvicorn app.main:app --reload # 前端 cd frontend && npm run dev ``` --- ## 执行选项 **1. Subagent-Driven (推荐)** - 我为每个任务派遣独立的子代理,任务间进行审查,快速迭代 **2. Inline Execution** - 在当前会话中按批次执行任务 选择哪种方式?