From fd8e37c52226dd092c173c4d53296871ee742c20 Mon Sep 17 00:00:00 2001 From: Boyuan-Zheng Date: Tue, 17 Mar 2026 17:07:10 +0800 Subject: [PATCH 1/4] =?UTF-8?q?=EF=BB=BFfeat(profile-helper):=20add=20prof?= =?UTF-8?q?ile=20view=20page=20migration=20and=20vite=20proxy=20config?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Migrate profile helper module: new blocks (ActionsBlock, ChartBlock, ChoiceBlock, RatingBlock, TextInputBlock, CopyableBlock, BlockRenderer), profile components (ScientistCard, ScientistMatchSection, ScientistScatter, profile sections), types.ts Update ChatWindow, ProfilePanel, ProfilePage, ScaleTestPage, scales data, scoring utils, and profile-helper.css Add vite proxy rule for /api/auth routing to localhost:8001 (auth service) Add DEPLOY_ARCH.md and full docs suite: product overview, product design, implementation plan, execution plan, profile page migration plan Made-with: Cursor --- DEPLOY_ARCH.md | 140 ++ docs/getting-started/architecture.md | 812 +++++++++++ ...66\346\236\204\346\200\273\350\247\210.md" | 1234 +++++++++++++++++ ...346\231\257\346\242\263\347\220\206_v1.md" | 356 +++++ ...346\241\214\350\256\250\350\256\272_v1.md" | 781 +++++++++++ ...345\244\247\351\227\255\347\216\257_v1.md" | 1221 ++++++++++++++++ ...346\217\220\347\244\272\350\257\215_v1.md" | 641 +++++++++ ...346\244\215\346\226\271\346\241\210_v1.md" | 973 +++++++++++++ .../src/modules/profile-helper/ChatWindow.tsx | 418 ++++-- .../modules/profile-helper/ProfilePanel.tsx | 12 +- .../profile-helper/blocks/ActionsBlock.tsx | 38 + .../profile-helper/blocks/BlockRenderer.tsx | 107 ++ .../profile-helper/blocks/ChartBlock.tsx | 99 ++ .../profile-helper/blocks/ChoiceBlock.tsx | 95 ++ .../profile-helper/blocks/CopyableBlock.tsx | 28 + .../profile-helper/blocks/RatingBlock.tsx | 47 + .../profile-helper/blocks/TextInputBlock.tsx | 68 + .../components/ScientistCard.tsx | 26 + .../components/ScientistMatchSection.tsx | 86 ++ .../components/ScientistScatter.tsx | 72 + .../components/profile/CapabilitySection.tsx | 100 ++ .../profile/CognitiveStyleSection.tsx | 59 + .../components/profile/DataSourceBadge.tsx | 14 + .../profile/InterpretationSection.tsx | 44 + .../components/profile/MotivationSection.tsx | 58 + .../components/profile/NeedsSection.tsx | 43 + .../components/profile/PersonalitySection.tsx | 104 ++ .../components/profile/ProfileHeader.tsx | 40 + .../src/modules/profile-helper/data/scales.ts | 202 ++- .../profile-helper/pages/ProfilePage.tsx | 164 ++- .../profile-helper/pages/ScaleTestPage.tsx | 133 +- .../modules/profile-helper/profile-helper.css | 701 +++++++++- .../profile-helper/profileHelperApi.ts | 77 +- frontend/src/modules/profile-helper/types.ts | 141 ++ .../modules/profile-helper/utils/scoring.ts | 82 +- frontend/vite.config.ts | 8 + 36 files changed, 8916 insertions(+), 308 deletions(-) create mode 100644 DEPLOY_ARCH.md create mode 100644 docs/getting-started/architecture.md create mode 100644 "docs/getting-started/history/\346\212\200\346\234\257\346\236\266\346\236\204\346\200\273\350\247\210.md" create mode 100644 "docs/\344\272\247\345\223\201\345\205\250\346\231\257\346\242\263\347\220\206_v1.md" create mode 100644 "docs/\344\272\247\345\223\201\350\256\276\350\256\241_\346\225\260\345\255\227\345\210\206\350\272\253\344\270\216\345\234\206\346\241\214\350\256\250\350\256\272_v1.md" create mode 100644 "docs/\345\267\245\347\250\213\345\256\236\346\226\275\350\256\241\345\210\222_\346\225\260\345\255\227\345\210\206\350\272\253\344\270\216\345\234\206\346\241\214\345\244\247\351\227\255\347\216\257_v1.md" create mode 100644 "docs/\346\211\247\350\241\214\350\256\241\345\210\222_\350\247\222\350\211\262\345\210\206\345\267\245\344\270\216\346\217\220\347\244\272\350\257\215_v1.md" create mode 100644 "docs/\347\224\273\345\203\217\346\237\245\347\234\213\351\241\265\347\247\273\346\244\215\346\226\271\346\241\210_v1.md" create mode 100644 frontend/src/modules/profile-helper/blocks/ActionsBlock.tsx create mode 100644 frontend/src/modules/profile-helper/blocks/BlockRenderer.tsx create mode 100644 frontend/src/modules/profile-helper/blocks/ChartBlock.tsx create mode 100644 frontend/src/modules/profile-helper/blocks/ChoiceBlock.tsx create mode 100644 frontend/src/modules/profile-helper/blocks/CopyableBlock.tsx create mode 100644 frontend/src/modules/profile-helper/blocks/RatingBlock.tsx create mode 100644 frontend/src/modules/profile-helper/blocks/TextInputBlock.tsx create mode 100644 frontend/src/modules/profile-helper/components/ScientistCard.tsx create mode 100644 frontend/src/modules/profile-helper/components/ScientistMatchSection.tsx create mode 100644 frontend/src/modules/profile-helper/components/ScientistScatter.tsx create mode 100644 frontend/src/modules/profile-helper/components/profile/CapabilitySection.tsx create mode 100644 frontend/src/modules/profile-helper/components/profile/CognitiveStyleSection.tsx create mode 100644 frontend/src/modules/profile-helper/components/profile/DataSourceBadge.tsx create mode 100644 frontend/src/modules/profile-helper/components/profile/InterpretationSection.tsx create mode 100644 frontend/src/modules/profile-helper/components/profile/MotivationSection.tsx create mode 100644 frontend/src/modules/profile-helper/components/profile/NeedsSection.tsx create mode 100644 frontend/src/modules/profile-helper/components/profile/PersonalitySection.tsx create mode 100644 frontend/src/modules/profile-helper/components/profile/ProfileHeader.tsx create mode 100644 frontend/src/modules/profile-helper/types.ts diff --git a/DEPLOY_ARCH.md b/DEPLOY_ARCH.md new file mode 100644 index 0000000..7929675 --- /dev/null +++ b/DEPLOY_ARCH.md @@ -0,0 +1,140 @@ +# Tashan-TopicLab · 部署架构文档 + +> **关联关系**:implements → 他山大项目整体部署架构 +> **最后更新**:2026-03-16 +> **部署状态**:✅ 已部署(生产环境) +> **生产地址**:https://tashan.chat/topic-lab/ +> **对应仓库**:https://github.com/TashanGKD/Tashan-TopicLab + +--- + +## 部署架构图 + +``` +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 用户浏览器 │ +│ https://tashan.chat/topic-lab/ │ +│ (唯一一套前端,一个 React SPA) │ +└────────────────────────────────┬─────────────────────────────────────────────┘ + │ HTTPS + ▼ +┌──────────────────────────────────────────────────────────────────────────────┐ +│ 🖥️ ECS 应用服务器 101.200.234.115(阿里云北京) │ +│ │ +│ ┌─────────────────────────────────────────────────────────────────────────┐ │ +│ │ Host nginx(/etc/nginx/sites-enabled/) │ │ +│ │ tashan.chat/topic-lab/ → 转发到 127.0.0.1:3000(前端容器) │ │ +│ └─────────────────────────┬───────────────────────────────────────────────┘ │ +│ │ │ +│ ┌──────────────────────────▼──────────────────────────────────────────────┐ │ +│ │ 🐳 Docker 容器 1:frontend(端口 3000:80) │ │ +│ │ │ │ +│ │ ① nginx 静态文件服务 │ │ +│ │ /usr/share/nginx/html/ ← React 构建产物(HTML/JS/CSS/图片) │ │ +│ │ │ │ +│ │ ② nginx 反向代理(按路径分流到不同后端): │ │ +│ │ /topic-lab/api/auth/ → topiclab-backend:8000(容器内网) │ │ +│ │ /topic-lab/api/source-feed/ → topiclab-backend:8000 │ │ +│ │ /topic-lab/api/api/v1/ → topiclab-backend:8000 │ │ +│ │ /topic-lab/api/* → backend:8000(Resonnet,支持 SSE) │ │ +│ │ /topic-lab/* → 静态文件(React SPA) │ │ +│ └───────────┬──────────────────────────────────┬──────────────────────────┘ │ +│ │ 容器内网 Docker network │ 容器内网 │ +│ ▼ ▼ │ +│ ┌───────────────────────────┐ ┌──────────────────────────────────────────┐ │ +│ │ 🐳 容器 2:backend:8000 │ │ 🐳 容器 3:topiclab-backend:8000 │ │ +│ │ (Resonnet, 对外 8000) │ │ (业务主服务, 对外 8001) │ │ +│ │ │ │ │ │ +│ │ Python FastAPI │ │ Python FastAPI │ │ +│ │ • /experts │ │ • /auth 登录/注册/JWT │ │ +│ │ • /skills │ │ • /topics 话题 CRUD │ │ +│ │ • /mcp │ │ • /posts 帖子/回复/点赞 │ │ +│ │ • /moderator-modes │ │ • /source-feed 外部信源文章 │ │ +│ │ • /profile-helper(SSE) │ │ • /literature 学术论文 │ │ +│ │ • /agent-links(SSE) │ │ • /aminer 学者/机构检索 │ │ +│ │ • /executor │◄───│ • /api/v1/me/ 收藏夹 │ │ +│ │ discussion execution │ │ (AI任务时代理给 Resonnet) │ │ +│ │ expert_reply execution │ │ │ │ +│ │ │ │ ──→ PostgreSQL(容器外,见下方) │ │ +│ │ 文件系统(bind mount): │ └─────────────────────────┬────────────────┘ │ +│ │ /app/workspace ◄─────────┼──────────────────────────────┘(共享挂载) │ +│ │ /app/libs ◄─────────┼── ./backend/libs(代码目录内) │ +│ └───────────────────────────┘ │ +│ │ +│ 📁 宿主机文件(bind mount 到容器内) │ +│ /var/www/github-actions/repos/Tashan-TopicLab/ │ +│ ├── workspace/ ← 讨论产物(turns/*.md)、会话文件(两个容器共用) │ +│ └── backend/libs/ ← 专家定义、技能、MCP配置(代码仓库的一部分) │ +│ │ +└──────────────────────────────────────────────────────────────────────────────┘ + │ │ + ▼ ▼ +┌───────────────────────────────┐ ┌──────────────────────────────────────────┐ +│ 🗄️ 数据库服务器(独立) │ │ 🌐 外部服务(各有独立服务器) │ +│ PostgreSQL │ │ │ +│ (DATABASE_URL 指向的 host) │ │ ic.nexus.tashan.ac.cn │ +│ │ │ → 信源文章爬取/存储服务 │ +│ 表: │ │ │ +│ ├── users 用户账号 │ │ coding.dashscope.aliyuncs.com │ +│ ├── topics 话题 │ │ → 阿里云 LLM API(qwen3.5) │ +│ ├── posts 帖子 │ │ NPC决策、讨论、专家回复 │ +│ ├── discussion_turns │ │ │ +│ │ 每轮讨论快照 │ │ GitHub (TashanGKD/Tashan-TopicLab) │ +│ ├── topic_generated_images │ │ → CI/CD 代码部署触发源 │ +│ │ 讨论图片(webp) │ │ │ +│ ├── openclaw_api_keys │ │ GitHub Actions Runner │ +│ │ OpenClaw绑定密钥 │ │ → 执行 deploy.yml 脚本 │ +│ ├── source_articles 信源 │ │ → SSH 到 ECS,拉代码重部署 │ +│ ├── favorite_categories │ └──────────────────────────────────────────┘ +│ └── ...(收藏/点赞/分享) │ +└───────────────────────────────┘ +``` + +--- + +## 文件与数据分布 + +| 文件/数据 | 存在哪里 | 说明 | +|-----------|---------|------| +| React 静态文件(JS/CSS/HTML) | ECS → frontend 容器内 | 每次部署重新 build | +| 代码(Python/TS/配置)| ECS 宿主机 `/var/www/...` | git pull 更新 | +| `backend/libs/`(专家/技能定义)| ECS 宿主机,挂载入 Resonnet 容器 | git 版本管理 | +| `workspace/`(讨论产物、会话)| ECS 宿主机,**两个后端容器共享挂载** | 重部署不消失(bind mount)| +| 用户账号、话题、帖子、讨论结果 | **独立 PostgreSQL 服务器** | 主业务数据 | +| 信源文章原文 | **ic.nexus.tashan.ac.cn**(独立)| 外部信息采集服务 | +| LLM 对话(AI 计算)| **阿里云 Dashscope API**(无状态)| 每次调用不持久化 | + +--- + +## 容器端口映射 + +| 容器 | 对外端口 | 容器内端口 | 说明 | +|------|---------|-----------|------| +| frontend | 3000 | 80 | nginx,静态文件 + 反向代理 | +| backend (Resonnet) | 8000 | 8000 | Python FastAPI,AI 执行引擎 | +| topiclab-backend | 8001 | 8000 | Python FastAPI,业务主服务 | + +--- + +## 部署流程 + +``` +开发者 git push → GitHub TashanGKD/Tashan-TopicLab (main) + → GitHub Actions deploy.yml 触发 + → SSH 登录 101.200.234.115 + → git pull + git submodule update (拉取 Resonnet) + → echo "$DEPLOY_ENV" > .env + → docker compose build --no-cache + → docker compose down && docker compose up -d + → 更新 /etc/nginx/snippets/world-tashan-chat.conf + → nginx -s reload + → 部署完成 +``` + +--- + +## 变更记录 + +| 日期 | 版本 | 内容 | +|------|------|------| +| 2026-03-16 | v1.0 | 初建,记录已部署生产架构 | diff --git a/docs/getting-started/architecture.md b/docs/getting-started/architecture.md new file mode 100644 index 0000000..0b55d50 --- /dev/null +++ b/docs/getting-started/architecture.md @@ -0,0 +1,812 @@ +# Tashan-TopicLab 完整技术架构文档 + +> 基于 2026-03-17 代码实际阅读,严格按代码现状梳理,可凭此文档复现整个项目。 + +--- + +## 一、项目概览 + +**Tashan-TopicLab**(代码内部名:**Resonnet**)是一个多智能体圆桌讨论平台,支持: + +1. **自动化圆桌讨论**:用户发布话题,系统调度多个 AI 专家 Agent 轮流发言,生成结构化讨论记录。 +2. **专家 @提及**:用户在话题评论区 @某个专家,触发单次 AI 专家回复。 +3. **档案助手(Profile Helper)**:基于 Agent Link 的科研数字分身采集工具。 +4. **资料库管理**:管理专家库、主持风格库、可分配技能库、MCP 服务器库。 + +--- + +## 二、系统整体架构 + +``` + ┌─────────────────────────────────────────────┐ + │ Nginx 反向代理 (80 端口) │ + │ / → frontend:80 │ + │ /api/ → frontend:80/api/ │ + └──────────────────┬──────────────────────────┘ + │ + ┌────────────────────────┴────────────────────────┐ + │ │ + ┌──────────▼──────────┐ ┌─────────────▼──────────────┐ + │ frontend │ │ topiclab-backend │ + │ React + Vite │ │ FastAPI (Python 3.11) │ + │ nginx:alpine │ │ Port 8001 │ + │ Port 80 │ │ 用户认证、信源、收藏、 │ + └─────────────────────┘ │ 文献、互动、数字分身同步 │ + └─────────────────────────────┘ + ↑ + ┌────────────────────────────────────────────┘ + │ AUTH_SERVICE_BASE_URL=http://topiclab-backend:8000 + │ + ┌──────────▼──────────────────────────────────────────────┐ + │ backend (Resonnet) │ + │ FastAPI (Python 3.11) │ + │ Port 8000 │ + │ │ + │ topics | discussion | posts | experts | moderator │ + │ profile-helper | agent-links | skills | mcp | libs │ + └───────────────────────────────┬──────────────────────────┘ + │ + ┌────────────────────┼──────────────────────────┐ + │ │ │ + ┌──────────▼──────┐ ┌────────▼────────┐ ┌─────────────▼────────────┐ + │ Database │ │ Workspace (FS) │ │ AI 服务 │ + │ SQLite / │ │ ./workspace/ │ │ ANTHROPIC_* (讨论 SDK) │ + │ PostgreSQL │ │ (Volume 挂载) │ │ AI_GENERATION_* (生成) │ + └─────────────────┘ └─────────────────┘ └──────────────────────────┘ +``` + +--- + +## 三、仓库目录结构 + +``` +Tashan-TopicLab/ ← 主仓库(宿主) +├── .env / .env.example ← 统一环境变量(前后端共用) +├── .env.deploy.example ← 生产部署环境变量模板 +├── .gitmodules ← 子模块配置 +├── docker-compose.yml ← 3个服务编排 +├── AGENTS.md ← AI 规范文档 +├── .github/workflows/ +│ ├── ci.yml ← CI 检查 +│ ├── deploy.yml ← main 分支自动部署(SSH → docker compose) +│ └── deploy-branch.yml ← 分支手动部署 +│ +├── backend/ ← Git 子模块 (Resonnet 仓库) +│ ├── main.py ← FastAPI 应用入口 +│ ├── Dockerfile ← python:3.11-slim 基础镜像 +│ ├── alembic.ini ← 数据库迁移配置 +│ ├── migrations/ ← Alembic 迁移脚本 +│ ├── libs/ ← 资料库(Volume 挂载,运行时可扩展) +│ │ ├── experts/ ← 专家技能库 +│ │ │ ├── meta.json ← sources 注册表 +│ │ │ └── default/ ← 内置专家目录 +│ │ │ ├── meta.json ← 专家列表(categories + experts) +│ │ │ ├── expert_common.md +│ │ │ └── {expert}.md ← 专家技能文件 +│ │ ├── moderator_modes/ ← 主持风格库 +│ │ │ ├── meta.json +│ │ │ └── default/ +│ │ │ ├── meta.json +│ │ │ ├── moderator_common.md +│ │ │ └── {mode}.md +│ │ ├── assignable_skills/ ← 可分配技能库 +│ │ │ ├── meta.json +│ │ │ ├── default/ ← 内置技能(如 image_generation) +│ │ │ └── _submodules/ ← Git 子模块导入的外部技能集 +│ │ ├── mcps/ ← MCP 服务器配置库 +│ │ │ ├── meta.json +│ │ │ └── default/meta.json +│ │ └── agent_links/ ← Agent Link 蓝图目录 +│ │ └── {slug}/ +│ │ └── agent.json +│ │ +│ ├── workspace/ ← 运行时数据(Volume 挂载) +│ │ ├── topics/ +│ │ │ └── {topic_id}/ ← 每个话题的沙盒工作区 +│ │ │ ├── shared/ +│ │ │ │ ├── topic.md +│ │ │ │ ├── turns/ +│ │ │ │ │ └── round{N}_{expert}.md +│ │ │ │ ├── discussion_summary.md +│ │ │ │ └── generated_images/ +│ │ │ ├── agents/ +│ │ │ │ └── {expert}/ +│ │ │ │ └── role.md +│ │ │ ├── config/ +│ │ │ │ ├── moderator_mode.json +│ │ │ │ ├── moderator_skill.md +│ │ │ │ ├── experts_metadata.json +│ │ │ │ ├── workspace.json +│ │ │ │ ├── skills/ +│ │ │ │ │ └── {skill}.md +│ │ │ │ └── mcp.json +│ │ │ └── .claude/ +│ │ │ └── skills/ ← Claude SDK 自动发现目录 +│ │ └── users/ +│ │ └── {user_id}/ +│ │ └── profile/ ← Profile Helper 用户档案 +│ │ +│ └── app/ +│ ├── __init__.py +│ ├── api/ ← API 层(路由 + 入参出参) +│ ├── agent/ ← 智能体层(AI调度逻辑) +│ ├── auth/ ← 认证层 +│ ├── core/ ← 配置 + 元数据 +│ ├── db/ ← 数据库 ORM + 会话 +│ ├── models/ ← Pydantic Schema + Store +│ ├── services/ ← 业务服务层 +│ ├── integrations/ ← 外部系统集成 +│ └── prompts/ ← 内置 Prompt 模板 +│ +├── frontend/ ← 前端(React + Vite) +│ ├── Dockerfile ← 二阶段构建:node:20-slim + nginx:alpine +│ ├── package.json +│ ├── vite.config.ts +│ ├── nginx.conf / nginx.root.conf +│ └── src/ +│ ├── App.tsx ← 路由总入口 +│ ├── api/ +│ │ ├── client.ts ← axios + 所有 API 类型定义与调用函数 +│ │ └── auth.ts ← tokenManager(LocalStorage JWT) +│ ├── pages/ ← 页面组件 +│ ├── components/ ← 公共 UI 组件 +│ ├── hooks/ ← 自定义 Hook +│ └── modules/ +│ └── profile-helper/ ← Profile Helper 独立模块 +│ +└── topiclab-backend/ ← 子模块(另一个 FastAPI 服务) + └── ... ← 用户认证、信源、收藏等 +``` + +--- + +## 四、后端架构(Resonnet) + +### 4.1 四层架构 + +``` +main.py (FastAPI app) + │ + ├── API Layer (app/api/) ← 路由、入参验证、出参序列化(每文件 ≤200行) + │ ↓ + ├── Agent Layer (app/agent/) ← AI调度逻辑、工作区操作、沙盒执行 + │ ↓ + ├── Service Layer (app/services/)← 业务服务(agent_links、profile_helper) + │ ↓ + └── Data Layer + ├── app/db/models.py ← SQLAlchemy ORM 模型 + ├── app/db/session.py ← 同步 sessionmaker + session_scope() + ├── app/models/schemas.py ← Pydantic Schema(请求/响应类型) + └── app/models/store.py ← 数据库 CRUD 操作封装 +``` + +### 4.2 启动入口 `main.py` + +- **FastAPI 应用**:`app = FastAPI(title="Resonnet API", version="0.1.0")` +- **CORS 中间件**:允许所有来源(`allow_origins=["*"]`) +- **两种运行模式**(`RESONNET_MODE` 环境变量): + - `standalone`:完整模式。启动时执行 Alembic 数据库迁移,重置 RUNNING 状态的讨论,加载全部路由(含 topics/posts/discussion)。 + - `executor`:纯 Agent 执行模式。不加载 topics/posts/discussion 路由,不连数据库,只提供 Agent 执行 API。 +- **路由注册**: + +| 路由前缀 | 文件 | 功能 | +|---------|------|------| +| `/topics` | `topics.py` | 话题 CRUD、图片资源服务 | +| `/topics` | `posts.py` | 帖子管理、专家 @提及 | +| `/topics` | `discussion.py` | 圆桌讨论启动/状态查询 | +| `/topics` | `topic_experts.py` | 话题级专家管理 | +| `/moderator-modes` | `moderator_modes.py` | 主持风格管理 | +| `/skills` | `skills.py` | 可分配技能库 | +| `/experts` | `experts.py` | 全局专家库 | +| `/mcp` | `mcp.py` | MCP 服务器库 | +| `/libs` | `libs.py` | 资料库缓存管理 | +| `/profile-helper` | `profile_helper.py` | 数字档案助手 | +| `/agent-links` | `agent_links.py` | Agent Link 管理 | +| `/executor` | `executor.py` | Agent 执行器 | +| `/health` | inline | 健康检查 | + +### 4.3 数据库层 + +**ORM 框架**:SQLAlchemy(同步,绝不使用 `async with`) + +**连接配置**(`app/db/session.py`): +```python +# standalone 模式:SQLite(自动创建于 workspace/resonnet.sqlite3) +# executor 模式:PostgreSQL(通过 TOPICDATABASE_URL 或 DATABASE_URL 指定) + +_build_engine() 参数: + - pool_pre_ping=True + - pool_size=10(DB_POOL_SIZE) + - max_overflow=20(DB_MAX_OVERFLOW) + - pool_recycle=1800(DB_POOL_RECYCLE_SECONDS) + - SQLite: check_same_thread=False +``` + +**`session_scope()` 上下文管理器**:自动 commit/rollback/close,所有 DB 操作必须在此上下文内进行。 + +**数据模型**(`app/db/models.py`): + +| 表名 | 主键 | 主要字段 | +|------|------|----------| +| `topics` | `id` (UUID str) | title, body, category, status, mode, num_rounds, expert_names(JSON), discussion_status, moderator_mode_id | +| `discussion_runs` | `topic_id` (FK) | status, turns_count, cost_usd, completed_at | +| `discussion_turns` | `id` (UUID str) | topic_id(FK), turn_key, round_num, expert_name, body, source_file | +| `posts` | `id` (UUID str) | topic_id(FK), author, author_type, expert_name, body, mentions(JSON), in_reply_to_id, status | + +**关联关系**: +- `TopicRecord` → `DiscussionRunRecord`:一对一(`back_populates="discussion_run"`,cascade delete) +- `TopicRecord` → `PostRecord[]`:一对多(`back_populates="topic"`,cascade delete) + +### 4.4 配置层 + +**文件**:`app/core/config.py` + +**环境变量加载顺序**: +1. 先找项目根 `.env`(宿主仓库 `.env`) +2. fallback 到 `backend/.env` + +**关键配置分组**: + +| 配置组 | 环境变量 | 用途 | +|--------|---------|------| +| 讨论 Agent | `ANTHROPIC_API_KEY`、`ANTHROPIC_BASE_URL`、`ANTHROPIC_MODEL` | claude_agent_sdk 圆桌讨论调用 | +| AI 生成 | `AI_GENERATION_BASE_URL`、`AI_GENERATION_API_KEY`、`AI_GENERATION_MODEL` | 专家/主持风格 AI 生成(HTTP API 方式,非 SDK) | +| 数据库 | `TOPICDATABASE_URL` / `DATABASE_URL` | SQLAlchemy 连接串 | +| 工作区 | `WORKSPACE_BASE` | workspace 根目录,默认 `/app/workspace` | +| 认证 | `AUTH_MODE`(none/jwt/proxy)、`AUTH_REQUIRED`、`AUTH_SERVICE_BASE_URL` | 认证模式 | +| 运行模式 | `RESONNET_MODE`(standalone/executor) | 服务启动模式 | +| 资料库 | 路径函数族(`get_experts_dir()`等) | libs/ 各子目录路径 | + +**⚠️ 两套 AI 配置严格隔离**:`ANTHROPIC_*` 只用于 claude_agent_sdk 讨论,`AI_GENERATION_*` 只用于 HTTP 接口式生成(专家生成、主持风格生成),绝不互相 fallback。 + +--- + +## 五、Agent 调度系统 + +### 5.1 圆桌讨论流程 + +``` +POST /topics/{id}/discussion + │ + ├── 校验:话题存在、专家不为空、非 RUNNING 状态 + ├── 更新 DB: discussion_status = "running" + ├── 读取工作区 config/moderator_mode.json → 确定 num_rounds + ├── asyncio.create_task(run_discussion_background(...)) + └── 立即返回 202(RUNNING 状态) + +run_discussion_background(异步后台任务) + │ + ├── run_discussion_for_topic() + │ ├── ensure_topic_workspace() → 创建/确认工作区目录结构 + │ ├── init_discussion_history() → 写 shared/topic.md(供专家读取话题) + │ ├── copy_skills_to_workspace() → 从 libs/assignable_skills/ 拷贝选定技能到 config/skills/ + │ │ ⚠️ image_generation 技能始终被强制包含 + │ ├── copy_mcp_to_workspace() → 从 libs/mcps/ 拷贝选定 MCP 服务器到 config/mcp.json + │ ├── get_agent_config() → {api_key, base_url, model} + │ ├── exclusive_topic_sandbox(topic_id, ws_path, "discussion") → 互斥锁 + │ └── run_discussion(workspace_dir, config, topic, ...) + │ ├── build_experts_from_workspace() → AgentDefinition[] + │ │ 每个专家 = 工作区 agents/{name}/role.md + │ │ + libs/experts/{source}/expert_common.md(语言指令占位符替换) + │ │ + EXPERT_SECURITY_SUFFIX(反注入沙盒约束) + │ │ + build_workspace_boundary(ws_abs)(路径隔离) + │ ├── prepare_moderator_skill(ws_path, topic, expert_names, num_rounds) + │ │ → 渲染主持技能到 config/moderator_skill.md + │ │ → 包含:技能分配指令 + 图片生成指导 + 来源引用护栏 + │ ├── sync_claude_skill_discovery_files() → 镜像到 .claude/skills/ + │ ├── _load_mcp_servers_for_sdk() → 从 config/mcp.json 读取 MCP 配置 + │ ├── ClaudeAgentOptions( + │ │ allowed_tools=[Read,Write,Edit,Glob,Grep,Task,WebFetch,WebSearch] + mcp_* + │ │ permission_mode="bypassPermissions" + │ │ system_prompt=moderator_system.md + workspace_boundary + │ │ cwd=ws_abs, add_dirs=[ws_abs] + │ │ agents={name: AgentDefinition}(子 agent 定义) + │ │ mcp_servers={...} + │ │ ) + │ └── async for message in query(prompt, options): → 主持人开始主持 + │ 主持人读 config/moderator_skill.md → 调用子 agent Task → 专家轮流发言 + │ 每轮专家写文件:shared/turns/round{N}_{name}.md + │ 图片保存到:shared/generated_images/ + │ + ├── sanitize_discussion_turn_sources() → 过滤非可核验来源链接 + ├── validate_discussion_outputs() → 验证所有 turns 文件 + summary + 至少一张图片 + ├── sync_discussion_turns() → 从 shared/turns/ 同步到 DB discussion_turns 表 + └── 更新 DB: discussion_status = "completed" / "failed" +``` + +### 5.2 专家 @提及流程 + +``` +POST /topics/{id}/posts/mention + │ + ├── 校验:话题存在、非讨论运行中(409 冲突)、专家在工作区中存在 + ├── 写 user_post(status="completed")到工作区 + ├── 写 reply_post(status="pending")到工作区(占位符) + └── threading.Thread(target=_run_expert_reply_sync, daemon=True).start() + → run_expert_reply_sandboxed() + │ ├── 若 OS 沙盒可用(sandbox-exec/bwrap):通过 IPC JSON 在沙盒子进程中执行 + │ └── 若无沙盒:直接 asyncio.run(run_expert_reply(...)) + │ + └── run_expert_reply() + ├── claude_agent_sdk.query(prompt, options) + │ (专家读取 agents/{name}/role.md + shared/topic.md + posts + @mention 内容) + └── 更新 reply_post.body + status="completed" +``` + +### 5.3 OS 级沙盒隔离 + +**设计目的**:防止 Agent 写入工作区以外的文件(跨话题污染、系统文件篡改)。 + +**检测顺序**(`app/agent/sandbox_exec.py`): +1. **macOS Seatbelt**(`sandbox-exec`):Apple 内核沙盒,写权限仅允许 ws_path + IPC 临时目录 + ~/.claude + /tmp +2. **Linux Bubblewrap**(`bwrap`):namespace 隔离,只挂载 ws_path 和必要系统路径为 bind-mount +3. **fallback**:仅靠 Prompt 约束(`EXPERT_SECURITY_SUFFIX` + `build_workspace_boundary()`) + +**IPC 机制**: +- 主进程写 `/tmp/agent-topic-lab-{uuid}/input.json`(任务配置) +- 沙盒子进程(`sandbox_runner.py`)读 input.json → 执行 → 写 output.json +- 主进程读 output.json → 清理临时目录 + +### 5.4 专家系统 + +**专家来源**(`libs/experts/` 两层结构): + +``` +libs/experts/ +├── meta.json ← sources 注册表:{"sources": {"default": {...}, "topiclab_shared": {...}}} +└── {source_id}/ + ├── meta.json ← {"categories": {...}, "experts": {"physicist": {name, skill_file, description, perspective}}} + ├── expert_common.md ← 公共部分(工作区规则、讨论规则、语言指令占位符) + └── {skill_file}.md ← 专家角色技能文件 +``` + +**专家加载优先级**(`build_experts_from_workspace()`): +1. `workspace/agents/{name}/role.md`(话题级自定义)→ 追加 expert_common.md +2. fallback → `libs/experts/{source}/{skill_file}.md`(全局默认) +3. 工作区专属专家(AI 生成,无全局定义):直接读 role.md + +**专家类型**: +- `preset`:从全局库预设专家中添加 +- `custom`:用户手写 role_content 创建 +- `ai_generated`:系统用 AI_GENERATION API 生成角色描述 + +### 5.5 主持风格系统 + +**PRESET_MODES**:从 `libs/moderator_modes/default/meta.json` 加载,每个 mode 包含 `{id, name, description, num_rounds, convergence_strategy, prompt_file}`。 + +**自定义模式(`mode_id="custom"`)**:用户提供 `custom_prompt`,与 `moderator_common.md` 合并后保存到 `config/moderator_mode.json`。 + +**主持技能渲染流程**(`prepare_moderator_skill()`): +``` +config/moderator_mode.json → 读取 mode_id, num_rounds, custom_prompt + ↓ +加载 libs/moderator_modes/{source}/{mode_id}.md(主持角色部分) + + libs/moderator_modes/{source}/moderator_common.md(公共规则) + ↓ +替换占位符:{topic}, {ws_abs}, {expert_names_str}, {num_experts}, {num_rounds}, {output_language_instruction} + ↓ +追加技能分配指令(列出 config/skills/*.md) + ↓ +追加图片生成指导(含必须产出图片的规则) + ↓ +追加来源引用护栏 + ↓ +写入 config/moderator_skill.md +``` + +--- + +## 六、认证系统 + +### 6.1 三种认证模式 + +**配置**:`AUTH_MODE` 环境变量(`none` / `jwt` / `proxy`) + +| 模式 | 实现类 | 行为 | +|------|--------|------| +| `none` | `NoneAuthProvider` | 所有请求匿名通过(开发默认) | +| `jwt` | `JwtBridgeAuthProvider` | 读 Bearer Token → 调用 topiclab-backend `/auth/me` 验证 | +| `proxy` | `ProxyHeaderAuthProvider` | 读 HTTP 代理头(反向代理注入用户信息) | + +### 6.2 认证依赖链 + +```python +# FastAPI 依赖注入链 +get_current_auth_context(request, credentials) + → get_auth_provider().resolve_from_bearer(token) + → get_user_from_token(token) # jwt 模式 + → httpx.GET AUTH_SERVICE_BASE_URL/auth/me + → 返回 {"user": {"id": ..., "username": ...}} + → 返回 {"auth_context": AuthContext, "user": {...}, "token": "..."} + +get_current_user_from_auth_service(auth_ctx) ← 向下兼容的旧依赖 + → auth_ctx["user"] +``` + +### 6.3 作者名解析 + +帖子发布时:优先使用认证用户的 `username` / `phone` / `user-{id}`,仅在匿名时使用客户端传入的 `author` 字段。 + +--- + +## 七、工作区文件系统 + +每个话题有独立的沙盒工作区:`workspace/topics/{topic_id}/` + +### 7.1 目录结构与文件职责 + +``` +workspace/topics/{topic_id}/ +├── shared/ +│ ├── topic.md ← 话题标题 + 正文(专家从此读取,不从 DB 读) +│ ├── turns/ +│ │ └── round{N}_{expert}.md ← AI 专家每轮发言内容(命名规范严格) +│ ├── discussion_summary.md ← 圆桌总结(由主持人 Agent 写入) +│ └── generated_images/ ← AI 生成的图片文件 +│ └── round2_concept_map.png +│ +├── agents/ +│ └── {expert_name}/ +│ └── role.md ← 该话题下专家的角色定义(可个性化) +│ +├── config/ +│ ├── moderator_mode.json ← {mode_id, num_rounds, custom_prompt, skill_list, mcp_server_ids, model} +│ ├── moderator_skill.md ← 渲染后的主持技能(每次讨论前重新生成) +│ ├── workspace.json ← {output_language, output_language_name} +│ ├── experts_metadata.json ← [{name, label, description, source, added_at, masked, ...}] +│ ├── skills/ +│ │ └── {skill_id}.md ← 拷贝自 libs/assignable_skills/ 的技能文件 +│ └── mcp.json ← {"mcpServers": {sid: {command/url/...}}} +│ +└── .claude/ + └── skills/ ← 镜像 config/skills/,供 Claude SDK 自动发现 + └── {slug}/ + └── SKILL.md +``` + +### 7.2 关键操作 + +**`read_discussion_history(ws_path)`**: +- 扫描 `shared/turns/*.md`,按文件名字典排序 +- 从文件名提取 round 数和专家名(`roundN_expertname.md`) +- 查 `experts_metadata.json` 获取显示名 +- 拼接格式:`## Round N - ExpertLabel\n\n{content}\n\n---` + +**图片路径约定**: +- Agent 保存到:`shared/generated_images/{name}.png` +- Markdown 引用:`/api/topics/{id}/assets/generated_images/{path}` +- API 端点提供图片压缩/缓存服务(WebP 格式,quality 72) + +**语言检测**(`init_workspace_language_from_topic()`): +- 话题标题/正文中 CJK 字符占比 ≥ 30% → 设置 `output_language=zh` +- 否则设置 `en` +- 显式设置后不再覆盖 + +--- + +## 八、可分配资源库(libs/) + +所有 libs 子库遵循相同的**两层元数据结构**: + +``` +libs/{resource_type}/ +├── meta.json ← {"sources": {"default": {id, name}, "topiclab_shared": {...}}} +└── {source_id}/ + └── meta.json ← {"categories": {...}, "{resources}": {"id": {name, description, ...}}} +``` + +**双路径合并**:`builtin`(Docker 镜像内 `/app/libs_builtin`)+ `primary`(Volume 挂载 `/app/libs`)。主路径覆盖内置路径(`{**builtin, **primary}`),支持运行时热更新。 + +**缓存刷新**:`POST /libs/invalidate-cache` 可清除内存缓存,触发从磁盘重新加载。缓存 TTL 由 `LIBS_CACHE_TTL_SECONDS`(默认 60 秒)控制。 + +### 8.1 可分配技能(assignable_skills) + +- `default/`:内置技能(`image_generation` 等) +- `_submodules/`:通过 Git 子模块导入的外部技能集(如 `ai-research`、`anthropics`) +- 技能文件路径规则: + - 内置:`assignable_skills/default/{category}/{slug}.md` + - 子模块:`assignable_skills/_submodules/{source}/{skills_dir}/{category}/{slug}/SKILL.md` + +### 8.2 MCP 服务器(mcps) + +两种类型: +- **stdio 类型**:`{command, args, env}`,env 中 `${VAR_NAME}` 模式在运行时替换为实际环境变量 +- **HTTP 类型**:`{type: "http", url, headers}` + +--- + +## 九、Profile Helper(数字档案助手) + +### 9.1 架构 + +``` +前端 ProfileHelperPage + │ SSE / 轮询 + ▼ +POST /profile-helper/session + ↓ +app/services/profile_helper/ 模块 + ├── agent.py ← 主入口,SSE 流式响应 + ├── block_agent.py ← Block 渲染 Agent(将 AI 输出转为结构化 Block) + ├── llm_client.py ← AI_GENERATION_BASE_URL HTTP 调用 + ├── profile_parser.py ← 解析 Markdown 档案 + ├── prompts.py ← 系统提示词 + ├── scientist_match.py← 科学家匹配算法 + ├── scientists_db.py ← 科学家数据库 + ├── sessions.py ← 会话管理(文件存储) + └── tools.py ← Agent 工具函数 +``` + +### 9.2 档案存储 + +- 路径:`workspace/users/{user_id}/profile/` 或 `workspace/profile_helper/profiles/{session_id}/` +- 格式:Markdown 模板渲染后的 `.md` 文件 + +--- + +## 十、Agent Links(智能体链路) + +**概念**:将一个预配置的 AI 工作流("蓝图")暴露为可访问的聊天接口。 + +**蓝图结构**(`libs/agent_links/{slug}/agent.json`): +```json +{ + "slug": "tashan-profile-helper-demo", + "name": "...", + "module": "profile_helper", + "entry_skill": "collect-basic-info", + "blueprint_root": "/path/to/blueprint", + "rule_file_path": "/path/.cursor/rules/profile-collector.mdc", + "skills_path": "/path/.cursor/skills", + "welcome_message": "你好,我是科研数字分身采集助手。" +} +``` + +**发现顺序**: +1. `libs/agent_links/` 目录(disk) +2. `AGENT_BLUEPRINT_BASE` 环境变量指向的目录(自动扫描子目录) +3. 硬编码 fallback:`/Users/zeruifang/Documents/tashanlink/tashan-profile-helper_demo` + +--- + +## 十一、前端架构 + +### 11.1 技术栈 + +| 层 | 技术 | 版本 | +|---|---|---| +| 框架 | React | 18.2 | +| 语言 | TypeScript | 5.3 | +| 构建 | Vite | 5.1 | +| 路由 | react-router-dom | 6.22 | +| HTTP | axios | 1.6 | +| 样式 | Tailwind CSS | 3.4 | +| Markdown | react-markdown + remark-gfm + remark-math + rehype-katex | | +| 测试 | vitest + @testing-library/react | | + +### 11.2 路由结构 + +``` +/ → TopicList(话题广场) +/topics/new → CreateTopic(发布话题) +/topics/:id → TopicDetail(话题详情,含讨论结果 + 评论区) +/experts/:name/edit → ExpertEdit(专家技能编辑) +/library/:section → LibraryPage(experts/skills/mcp/moderator-modes) +/profile-helper/* → ProfileHelperPage(数字档案助手) +/agent-links → AgentLinkLibraryPage(Agent Link 列表) +/agent-links/:slug → AgentLinkChatPage(聊天界面) +/source-feed/:section → SourceFeedPage(信源流) +/login, /register → 登录注册 +/favorites → MyFavoritesPage(我的收藏) +``` + +### 11.3 API 客户端(`src/api/client.ts`) + +- **axios 实例**:`baseURL = ${BASE_URL}api`(配合 Vite 的 `BASE_URL` 参数) +- **请求拦截器**:自动从 `tokenManager.get()` 读取 JWT,注入 `Authorization: Bearer {token}` 头 +- **按资源分组的 API 对象**:`topicsApi`, `postsApi`, `discussionApi`, `expertsApi`, `topicExpertsApi`, `moderatorModesApi`, `skillsApi`, `mcpApi`, `profileHelperApi`, `sourceFeedApi`, `literatureApi`, `libsApi` + +### 11.4 内置模型列表(`ROUNDTABLE_MODELS`) + +``` +qwen3.5-plus, qwen-flash, qwen3-max, deepseek-v3.2, MiniMax-M2.1, kimi-k2.5, glm-5, glm-4.7 +``` + +--- + +## 十二、部署架构 + +### 12.1 Docker Compose 三服务 + +```yaml +services: + topiclab-backend: # Port 8001 → 8000 (用户认证、信源等) + image: python:3.11-slim (via daocloud镜像) + volumes: ${WORKSPACE_PATH}:/app/workspace + + backend: # Port 8000 → 8000 (Resonnet 核心) + image: python:3.11-slim (via daocloud镜像) + volumes: + - ${WORKSPACE_PATH}:/app/workspace + - ${LIBS_PATH}:/app/libs + + frontend: # Port 80 → ${FRONTEND_PORT} + image: node:20-slim + nginx:alpine (via daocloud镜像) + args: VITE_BASE_PATH=${VITE_BASE_PATH:-/topic-lab/} + depends_on: backend(healthy), topiclab-backend(healthy) +``` + +**健康检查**:`python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8000/health', timeout=2)"` + +### 12.2 前端 Nginx 路由 + +**两套配置**(由 `VITE_BASE_PATH` 决定): +- `VITE_BASE_PATH=/`:使用 `nginx.root.conf`(部署在域名根路径) +- `VITE_BASE_PATH=/topic-lab/`:使用 `nginx.conf`(部署在子路径) + +Nginx 代理规则(由 GitHub Actions 写入宿主机 `/etc/nginx/snippets/`): +```nginx +location / { + proxy_pass http://127.0.0.1:${FRONTEND_PORT}; +} +location /api/ { + proxy_pass http://127.0.0.1:${FRONTEND_PORT}/api/; +} +``` + +前端 Nginx 内部转发: +- `/api/` → `http://backend:8000`(Resonnet) +- 其余 → SPA index.html + +### 12.3 CI/CD 流程 + +**触发条件**:push to `main` 分支 + +**执行步骤**: +1. SSH 到部署服务器(`DEPLOY_HOST`/`DEPLOY_USER`/`SSH_PRIVATE_KEY`) +2. `git clone`(首次)或 `git fetch && git reset --hard origin/main` +3. 配置子模块 URL(HTTPS Token 认证) +4. `git submodule update --init --recursive`(含 backend + assignable_skills 子模块) +5. 写入 `DEPLOY_ENV` secret 到 `.env` +6. `docker compose build --no-cache` +7. `docker compose down && docker compose up -d` +8. `docker image prune -f` +9. 生成/更新宿主机 Nginx 配置 snippet +10. `sudo nginx -t && sudo nginx -s reload` + +--- + +## 十三、数据流示例 + +### 13.1 发布话题并启动圆桌讨论 + +``` +用户 → POST /topics(title, body, category) + → DB: 创建 TopicRecord(status=open, discussion_status=pending) + → FS: ensure_topic_workspace → 创建目录结构 + → FS: 写默认专家 role.md(4个内置专家) + → DB: 设置 moderator_mode_id="standard" + +用户 → POST /topics/{id}/discussion(num_rounds, skill_list, mcp_server_ids) + → DB: discussion_status = "running" + → asyncio.create_task(...) ← 立即返回 202 + → [后台] run_discussion_for_topic() + → 写 shared/topic.md + → 拷贝技能和 MCP 配置到工作区 + → claude_agent_sdk.query() → 主持人 + N个专家 Agent + → 专家轮流写 shared/turns/roundN_{expert}.md + → 至少一张图片写入 shared/generated_images/ + → 验证产出完整性 + → DB: discussion_status = "completed", cost_usd, turns_count + +用户 → GET /topics/{id}/discussion/status + → 读 DB + 实时读 shared/turns/*.md 构建 discussion_history + → 返回 progress(已完成轮数、当前发言者) +``` + +### 13.2 用户 @提及专家 + +``` +用户 → POST /topics/{id}/posts/mention(author, body, expert_name) + → 校验:非讨论运行中 + 专家在工作区 + → FS: 保存 user_post(status=completed) + → FS: 保存 reply_post(status=pending, body="") + → 启动 daemon 线程 → run_expert_reply_sandboxed() + → [OS 沙盒中] claude_agent_sdk.query() + → 专家读取话题 + 历史 posts + @mention 内容 + → 生成回复 + → FS: 更新 reply_post(body=回复内容, status=completed) + +用户 → GET /topics/{id}/posts/mention/{reply_post_id}(轮询) + → 返回 reply_post(status=pending/completed/failed) +``` + +--- + +## 十四、环境变量完整清单 + +| 变量名 | 必需 | 默认值 | 说明 | +|--------|------|--------|------| +| `ANTHROPIC_API_KEY` | ✅ | - | 圆桌讨论 claude_agent_sdk API Key | +| `ANTHROPIC_BASE_URL` | - | "" | 自定义 Claude API 地址 | +| `ANTHROPIC_MODEL` | - | "" | 默认讨论模型 | +| `AI_GENERATION_BASE_URL` | ✅ | - | 专家/主持风格生成 API 地址 | +| `AI_GENERATION_API_KEY` | ✅ | - | 专家/主持风格生成 API Key | +| `AI_GENERATION_MODEL` | ✅ | - | 专家/主持风格生成模型名 | +| `RESONNET_MODE` | - | `executor` | `standalone` / `executor` | +| `TOPICDATABASE_URL` | 生产必需 | SQLite(standalone) | PostgreSQL 连接串 | +| `DATABASE_URL` | 生产必需 | - | TOPICDATABASE_URL 的 fallback | +| `WORKSPACE_BASE` | - | `/app/workspace` | 工作区根目录 | +| `AUTH_MODE` | - | `none` | `none` / `jwt` / `proxy` | +| `AUTH_REQUIRED` | - | `false` | JWT 模式下是否强制认证 | +| `AUTH_SERVICE_BASE_URL` | jwt 模式需 | `http://topiclab-backend:8000` | 认证服务地址 | +| `ACCOUNT_SYNC_ENABLED` | - | `false` | 是否同步档案到外部账号系统 | +| `LIBS_CACHE_TTL_SECONDS` | - | `60` | 资料库内存缓存 TTL(0=不缓存) | +| `DB_POOL_SIZE` | - | `10` | 数据库连接池大小 | +| `DB_MAX_OVERFLOW` | - | `20` | 连接池溢出上限 | +| `DB_POOL_RECYCLE_SECONDS` | - | `1800` | 连接回收间隔(秒) | +| `FRONTEND_PORT` | - | `3000` | 前端容器对外端口 | +| `BACKEND_PORT` | - | `8000` | Resonnet 后端端口 | +| `TOPICLAB_BACKEND_PORT` | - | `8001` | topiclab-backend 端口 | +| `VITE_BASE_PATH` | - | `/topic-lab/` | 前端 SPA 基础路径 | +| `LIBS_PATH` | - | `./backend/libs` | libs 目录 volume 路径 | +| `WORKSPACE_PATH` | - | `./workspace` | workspace 目录 volume 路径 | + +--- + +## 十五、复现项目步骤 + +### 15.1 本地开发环境 + +```bash +# 1. 克隆主仓库(含子模块) +git clone --recurse-submodules +cd Tashan-TopicLab + +# 2. 配置环境变量 +cp .env.example .env +# 填写:ANTHROPIC_API_KEY, AI_GENERATION_BASE_URL, AI_GENERATION_API_KEY, AI_GENERATION_MODEL + +# 3. 本地开发 backend(在 backend/ 中) +cd backend +python -m venv .venv && source .venv/bin/activate +pip install -e . +cp .env.example .env # 填写 ANTHROPIC_* 和 AI_GENERATION_* +# 设置 RESONNET_MODE=standalone(会自动使用 SQLite) +uvicorn main:app --reload --port 8000 + +# 4. 本地开发 frontend +cd ../frontend +npm install +VITE_BASE_PATH=/ npm run dev # 访问 http://localhost:5173 +``` + +### 15.2 生产部署(Docker Compose) + +```bash +# 1. 准备 .env(参考 .env.deploy.example) +# 必须设置:ANTHROPIC_API_KEY, AI_GENERATION_*, DATABASE_URL +# 建议:RESONNET_MODE=standalone(若独立部署不依赖 topiclab-backend) + +# 2. 启动 +docker compose up -d + +# 3. 首次启动会自动执行 Alembic 数据库迁移(standalone 模式) +``` + +### 15.3 关键依赖 + +| 依赖 | 说明 | +|------|------| +| `claude_agent_sdk` | Anthropic Claude Agent SDK,用于多智能体圆桌讨论 | +| `fastapi` + `uvicorn` | API 框架 + ASGI 服务器 | +| `sqlalchemy` | ORM(同步模式) | +| `alembic` | 数据库迁移 | +| `httpx` | 异步 HTTP 客户端(用于认证回调) | +| `Pillow` | 图片压缩/格式转换 | +| `python-dotenv` | 环境变量加载 | +| `anthropic` | Anthropic Python SDK | + +--- + +*文档基于代码实际阅读生成,撰写时间:2026-03-17* diff --git "a/docs/getting-started/history/\346\212\200\346\234\257\346\236\266\346\236\204\346\200\273\350\247\210.md" "b/docs/getting-started/history/\346\212\200\346\234\257\346\236\266\346\236\204\346\200\273\350\247\210.md" new file mode 100644 index 0000000..371f486 --- /dev/null +++ "b/docs/getting-started/history/\346\212\200\346\234\257\346\236\266\346\236\204\346\200\273\350\247\210.md" @@ -0,0 +1,1234 @@ +# Tashan TopicLab 技术架构总览 + +> 基于 `reference/Tashan-TopicLab-main_0` 完整源码逐文件梳理 +> 文档层次:能看此文档直接复现整个项目 +> 更新时间:2026-03-17 + +--- + +## 目录 + +1. [系统全景](#1-系统全景) +2. [技术栈](#2-技术栈) +3. [服务间关系图](#3-服务间关系图) +4. [Resonnet 后端](#4-resonnet-后端) +5. [topiclab-backend 账号服务](#5-topiclab-backend-账号服务) +6. [前端](#6-前端) +7. [profile-helper 数字分身服务](#7-profile-helper-数字分身服务) +8. [部署架构](#8-部署架构) +9. [环境变量全表](#9-环境变量全表) +10. [本地开发启动手册](#10-本地开发启动手册) + +--- + +## 1 系统全景 + +TopicLab 是一个「以话题为容器、以 AI 多专家圆桌讨论为核心」的知识协作平台,并集成了科研人员数字分身(profile-helper)功能。 + +整体由**三个独立进程**组成: + +| 进程 | 仓库路径 | 语言 | 主要职责 | +|------|---------|------|---------| +| **Resonnet** | `backend/` | Python FastAPI | 话题/帖子存储、AI 讨论编排(Claude Agent SDK)、数字分身助手、专家/技能/MCP 库 | +| **topiclab-backend** | `topiclab-backend/` | Python FastAPI | 用户注册/登录/JWT、数字分身发布入库、信源文章、学术搜索(AMiner)、OpenClaw API Key 管理 | +| **前端** | `frontend/` | React 18 + TypeScript + Vite | SPA,通过 Nginx 反向代理到两个后端 | + +--- + +## 2 技术栈 + +### 2.1 Resonnet 后端(`backend/`) + +``` +FastAPI 0.109+ +Python 3.10+(要求) +Pydantic 2.x(数据验证) +claude-agent-sdk >= 0.1.29(讨论编排,多智能体) +anthropic >= 0.39.0(Claude API 底层客户端) +openai >= 1.0.0(profile-helper LLM,OpenAI-compatible 接口) +bcrypt 4.0(密码哈希) +python-jose[cryptography](JWT) +httpx 0.26(异步 HTTP) +supabase >= 2.0(用户认证数据存储,可选) +uvicorn[standard](ASGI 服务器) +``` + +**持久化**:纯文件系统(JSON + Markdown),无 ORM,内存 dict 作主缓存、每 5 秒与磁盘双向同步。 + +### 2.2 topiclab-backend(`topiclab-backend/`) + +``` +FastAPI 0.109+ +Python 3.10+ +SQLAlchemy 2.x(ORM) +psycopg2-binary(PostgreSQL 驱动)/ SQLite(开发 fallback) +bcrypt 4.0(密码哈希) +python-jose[cryptography](JWT HS256,7 天有效) +httpx 0.26(内部 HTTP 调用 Resonnet) +pillow 10.0(图片处理) +``` + +**持久化**:PostgreSQL(生产)/ SQLite(开发),自动建表。 + +### 2.3 前端(`frontend/`) + +``` +React 18.2 +TypeScript 5.3 +Vite 5.1(构建工具) +react-router-dom 6.22(SPA 路由) +axios 1.6(主 API client,无认证自动注入) +react-markdown 10.1 + remark-gfm(Markdown 渲染) +TailwindCSS 3.4(样式) +nginx:alpine(生产容器,前端容器内运行) +``` + +--- + +## 3 服务间关系图 + +``` +浏览器 + │ HTTPS + ▼ +Nginx(前端容器, :80/3000) + ├─ GET /topic-lab/* → 静态 SPA(index.html) + ├─ /topic-lab/api/* ──────────────► Resonnet(:8000) + └─ (注:topiclab-backend 在部署时由 Host Nginx 直接处理) + +Resonnet(:8000) + ├─ /topics/* # 话题/帖子 CRUD(文件存储) + ├─ /experts/* /skills/* /mcp/* # 资源库(文件存储) + ├─ /profile-helper/* # 数字分身 LLM 服务 + ├─ /agent-links/* # Agent Link 对话 + └─ (内部) HTTP ──────────────────► topiclab-backend(:8001) + POST /auth/digital-twins/upsert # 发布分身 + GET /auth/digital-twins # 分身列表 + +topiclab-backend(:8001) + ├─ /auth/* # 注册/登录/JWT/数字分身 + ├─ /topics/*(代理) # 话题详情(代理 + 持久化 DB) + ├─ /source-feed/* # 信源文章 + ├─ /literature/* # 学术文献(代理 IC 服务) + ├─ /aminer/* # AMiner 学术搜索 + ├─ /me/favorites/* # 收藏系统 + └─ POST /internal/discussion-snapshot/{topic_id} ◄── Resonnet 推送 + # 讨论进度实时回推(每轮结束后推送) + +数据库 + ├─ PostgreSQL(topiclab-backend 专用) # 用户表/话题表/帖子表/分身表 + └─ workspace/(文件系统,Resonnet 专用) + topics/{id}/ roles/ users/{uid}/ public_agents/ +``` + +--- + +## 4 Resonnet 后端 + +### 4.1 目录结构 + +``` +backend/ +├── main.py # FastAPI 入口,注册所有 router,启动时加载 workspace +├── pyproject.toml # 依赖声明 +├── Dockerfile # 多阶段构建:python:3.11-slim,pip 阿里云镜像 +├── app/ +│ ├── api/ +│ │ ├── auth.py # /auth/* 路由(已迁移到 topiclab-backend,此为 legacy) +│ │ ├── topics.py # /topics CRUD +│ │ ├── posts.py # /topics/{id}/posts + @mention +│ │ ├── discussion.py # /topics/{id}/discussion 启动/状态 +│ │ ├── topic_experts.py # /topics/{id}/experts 话题级专家管理 +│ │ ├── experts.py # /experts 全局专家库 +│ │ ├── moderator_modes.py # /moderator-modes 讨论方式库 +│ │ ├── skills.py # /skills 技能库 +│ │ ├── mcp.py # /mcp MCP 库 +│ │ ├── profile_helper.py # /profile-helper/* 数字分身助手 +│ │ ├── agent_links.py # /agent-links/* Agent Link 对话 +│ │ ├── libs.py # /libs/invalidate-cache +│ │ ├── executor.py # /executor/* 讨论执行(被 topiclab 调用) +│ │ └── auth_bridge.py # JWT 验证 Depends +│ ├── models/ +│ │ └── schemas.py # Pydantic 模型(Topic/Post/Expert 等) +│ ├── db/ # SQLAlchemy ORM(仅 topiclab 的持久化,Resonnet 用文件) +│ │ ├── models.py # ORM class 定义 +│ │ └── database.py # 连接池 +│ ├── core/ +│ │ └── config.py # 所有配置读取(环境变量) +│ ├── services/ +│ │ ├── profile_helper/ # 数字分身助手服务(见第7节) +│ │ └── agent_runtime/ # 通用 Agent 运行时 +│ ├── agent/ +│ │ └── discussion.py # 讨论编排核心逻辑(claude-agent-sdk) +│ ├── auth/ # JWT 工具(新架构) +│ │ ├── providers/ # 认证 Provider 工厂 +│ │ └── context.py # AuthContext dataclass +│ ├── storage/ +│ │ └── roles.py # Role 文件存储抽象 +│ └── integrations/ +│ └── account_sync.py # 同步用户分身到 topiclab-backend +└── libs/ + └── profile_helper/ # 画像系统资源文件 + ├── _template.md # 画像 Markdown 模板 + ├── skills/ # 11 个 Skill.md 文件 + └── docs/ # 量表原题 + 参考文档 +``` + +### 4.2 启动逻辑(`main.py`) + +```python +@asynccontextmanager +async def lifespan(app): + # 1. 若 RESONNET_MODE=standalone,运行数据库迁移 + # 2. 从 workspace/ 加载话题到内存 store + # 3. 启动后台任务:每 5 秒将内存 store 同步到磁盘 + # 4. 将 running 中的讨论重置为 FAILED(防重启后僵死) + yield + # 退出时最后一次同步 +``` + +**路由挂载**: + +```python +app.include_router(topics_router.router, prefix="/topics") +app.include_router(posts_router.router, prefix="/topics") +app.include_router(discussion_router.router, prefix="/topics") +app.include_router(topic_experts_router.router, prefix="/topics") +app.include_router(moderator_modes_router.router) # 无前缀 +app.include_router(skills_router.router, prefix="/skills") +app.include_router(experts_router.router, prefix="/experts") +app.include_router(mcp_router.router, prefix="/mcp") +app.include_router(libs_router.router, prefix="/libs") +app.include_router(profile_helper_router.router, prefix="/profile-helper") +app.include_router(agent_links_router.router, prefix="/agent-links") +app.include_router(executor_router.router, prefix="/executor") +``` + +### 4.3 完整 API 路由表(Resonnet) + +#### 话题(`/topics`) + +| Method | Path | 入参 | 出参 | 认证 | +|--------|------|------|------|------| +| GET | `/topics` | — | `list[Topic]` | 无 | +| POST | `/topics` | `{title, body?, category?}` | `Topic` 201 | 无 | +| GET | `/topics/{id}` | — | `Topic` | 无 | +| PATCH | `/topics/{id}` | `{title?, body?, category?, expert_names?}` | `Topic` | 无 | +| POST | `/topics/{id}/close` | — | `Topic` | 无 | + +#### 帖子(`/topics/{id}/posts`) + +| Method | Path | 入参 | 出参 | 认证 | +|--------|------|------|------|------| +| GET | `/topics/{id}/posts` | — | `list[Post]` | 无 | +| POST | `/topics/{id}/posts` | `{author, body, in_reply_to_id?}` | `Post` 201 | 无 | +| POST | `/topics/{id}/posts/mention` | `{author, body, expert_name, in_reply_to_id?}` | `{user_post, reply_post_id, status:pending}` 202 | 无 | +| GET | `/topics/{id}/posts/mention/{reply_post_id}` | — | `Post`(轮询 status) | 无 | + +#### 讨论(AI 圆桌) + +| Method | Path | 入参 | 出参 | +|--------|------|------|------| +| POST | `/topics/{id}/discussion` | `{num_rounds, max_turns, max_budget_usd, model?, skill_list?, mcp_server_ids?}` | 202 异步 | +| GET | `/topics/{id}/discussion/status` | — | `{status, result, progress}` | + +#### 话题级专家(`/topics/{id}/experts`) + +| Method | Path | 入参 | +|--------|------|------| +| GET | `/topics/{id}/experts` | — | +| POST | `/topics/{id}/experts` | `{source:preset/custom, preset_name?, name?, role_content?}` | +| PUT | `/topics/{id}/experts/{name}` | `{role_content}` | +| DELETE | `/topics/{id}/experts/{name}` | — | +| GET | `/topics/{id}/experts/{name}/content` | — | +| POST | `/topics/{id}/experts/{name}/share` | — | +| POST | `/topics/{id}/experts/generate` | `{expert_label, description, expert_name?}` | + +#### 主持人模式(`/moderator-modes`) + +| Method | Path | 说明 | +|--------|------|------| +| GET | `/moderator-modes` | 预设列表 | +| GET | `/moderator-modes/assignable` | 可分配列表(支持 `category/q/limit/offset` 筛选) | +| GET | `/topics/{id}/moderator-mode` | 当前话题配置 | +| PUT | `/topics/{id}/moderator-mode` | `{mode_id, num_rounds, custom_prompt?, skill_list?, model?}` | +| POST | `/topics/{id}/moderator-mode/generate` | AI 生成讨论方式 | +| POST | `/topics/{id}/moderator-mode/share` | 分享到平台 | + +#### 全局专家库(`/experts`) + +| Method | Path | 说明 | +|--------|------|------| +| GET | `/experts` | 列表 | +| GET | `/experts/{name}` | 详情 | +| GET | `/experts/{name}/content` | Markdown 内容 | +| PUT | `/experts/{name}` | 更新内容 | +| POST | `/experts/import-profile` | 从画像导入专家 | + +#### 技能库(`/skills`)、MCP 库(`/mcp`) + +均包含 `list/categories/getContent` 三个端点,支持 `category/q` 筛选。 + +#### 数字分身助手(`/profile-helper`,需 JWT) + +| Method | Path | 出参 | +|--------|------|------| +| GET | `/profile-helper/session?session_id?` | `{session_id}` | +| POST | `/profile-helper/session/reset/{id}` | `{ok, session_id}` | +| POST | `/profile-helper/chat` | **SSE 流**(Block 协议) | +| GET | `/profile-helper/profile/{id}` | `{profile, forum_profile}` | +| GET | `/profile-helper/profile/{id}/structured` | `StructuredProfile JSON` | +| GET | `/profile-helper/profile/{id}/scientists/famous` | `{top3, scatter_data, user_point}` | +| GET | `/profile-helper/profile/{id}/scientists/field` | `{recommendations}` | +| POST | `/profile-helper/publish-to-library` | `{ok, agent_name, ...}` | +| GET | `/profile-helper/download/{id}` | `profile.md` 文件 | +| GET | `/profile-helper/download/{id}/forum` | `forum-profile.md` 文件 | +| POST | `/profile-helper/scales/submit` | `{ok, scale_name}` | +| GET | `/profile-helper/scales/{id}` | `{scales}` | + +#### Agent Links(`/agent-links`) + +| Method | Path | 说明 | +|--------|------|------| +| GET | `/agent-links` | 列表 | +| GET | `/agent-links/{slug}` | 详情 | +| POST | `/agent-links/import/preview` | 预览 ZIP(multipart) | +| POST | `/agent-links/import` | 导入 ZIP | +| POST | `/agent-links/{slug}/session` | 创建/恢复会话 | +| POST | `/agent-links/{slug}/chat` | **SSE 流式**对话 | +| POST | `/agent-links/{slug}/files/upload` | 上传工作区文件(最大 30MB)| + +#### 用户智能体(需 JWT) + +| Method | Path | 说明 | +|--------|------|------| +| GET | `/me/agents` | 我的 Agent 列表 | +| POST | `/me/agents` | `{name, role_content, description?, visibility?}` | +| GET | `/me/agents/{name}` | 详情 | +| PUT | `/me/agents/{name}/visibility` | `{visibility:private/public}` | +| DELETE | `/me/agents/{name}` | 删除 | +| GET | `/public-agents` | 公开 Agent 列表 | + +### 4.4 Pydantic 数据模型(`app/models/schemas.py`) + +```python +class TopicStatus(str, Enum): # draft | open | closed +class TopicMode(str, Enum): # human_agent | discussion | both +class DiscussionStatus(str, Enum): # pending | running | completed | failed + +class Topic: + id: str # UUID + session_id: str # 同 id,对应 workspace/topics/{id}/ + title: str + body: str + category: Optional[str] + status: TopicStatus + mode: TopicMode + num_rounds: int = 5 + expert_names: list[str] + discussion_status: DiscussionStatus + discussion_result: Optional[DiscussionResult] + moderator_mode_id: Optional[str] + moderator_mode_name: Optional[str] + created_at: str + updated_at: str + +class Post: + id: str + topic_id: str + author: str + author_type: AuthorType # human | agent + expert_name: Optional[str] + expert_label: Optional[str] + body: str + mentions: list[str] + in_reply_to_id: Optional[str] + status: str # pending | completed | failed + created_at: str + +class ExpertInfo: + name: str # 英文 key + label: str # 显示名 + description: str + skill_file: str + skill_content: str + perspective: str + category: Optional[str] + source: Optional[str] # default | topiclab_shared + +class TopicExpert: + name: str + label: str + description: str + source: str # preset | custom | ai_generated + role_file: str + added_at: str + is_from_topic_creation: bool +``` + +### 4.5 文件系统存储结构(workspace/) + +``` +workspace/ +├── topics/ +│ └── {topic_id}/ # 每个话题一个目录 +│ ├── topic.json # Topic 元数据(所有字段) +│ ├── agents/ +│ │ └── {expert_name}/ +│ │ └── role.md # 专家 Markdown 描述 +│ ├── config/ +│ │ ├── moderator_mode.json +│ │ ├── mcp.json +│ │ └── skills/ # 分配的技能文件 +│ └── shared/ +│ ├── turns/ +│ │ └── round{N}_{expert}.md # 每轮讨论发言 +│ └── discussion_history.md # 完整讨论记录 +├── roles/ +│ └── {role_id}/ +│ ├── meta.json # RoleMeta(id/name/owner_type/visibility/role_type) +│ ├── identity.md # 角色身份描述 +│ ├── relations.json # 关联关系(chats/spaces/topics) +│ ├── config.json +│ ├── memory/ # 记忆文件 +│ ├── skills/ # 角色技能 +│ ├── inbox/ # 收件箱 +│ └── files/ # 工作区文件 +├── users/ +│ └── {user_id}/ +│ ├── profile/ +│ │ ├── profile.md # 科研画像 Markdown +│ │ ├── forum_profile.md # 论坛分身 +│ │ └── scales.json # 量表结果 +│ └── agents/ +│ └── my_twin/ +│ ├── role.md # 分身角色内容 +│ └── meta.json # {owner_user_id, visibility, source} +└── public_agents/ + └── {uid}_{agent_name}/ # 公开数字分身 +``` + +**内存 Store 同步机制**: +- 启动时:递归读取所有 `workspace/topics/*/topic.json` → `topics_db: Dict[str, Topic]` +- 运行中:每 5 秒 `asyncio.create_task(sync_store_to_disk())` +- 修改时:先写内存,后异步刷盘 + +### 4.6 认证机制(Resonnet) + +Resonnet 支持三种认证模式,通过 `AUTH_MODE` 环境变量切换: + +| 模式 | 说明 | +|------|------| +| `none`(默认)| 所有请求视为匿名(uid="anonymous"),不做任何验证 | +| `jwt` | 验证 Bearer Token(调用 topiclab-backend `/auth/me` 或本地 JWT 解码)| +| `proxy` | 从请求头提取 uid(反向代理注入)| + +`get_current_auth_context` Depends 解析 JWT → `AuthContext(subject, is_anonymous)`。 + +### 4.7 讨论编排(`app/agent/discussion.py`) + +``` +POST /topics/{id}/discussion → 202(异步) + ↓ 标记 discussion_status = RUNNING + ↓ asyncio.create_task() 后台执行: + 1. ensure_topic_workspace():创建目录结构 + 2. copy_skills_to_workspace():拷贝选定技能文件 + 3. copy_mcp_to_workspace():拷贝 MCP 配置 + 4. exclusive_topic_sandbox():加锁(防并发讨论+@mention冲突) + 5. build_experts_from_workspace():从 role.md 构建 AgentDefinition 列表 + 6. prepare_moderator_skill():生成主持人 Skill 内容 + 7. claude_agent_sdk.query(): + 主持人 Claude Agent 依次与各专家 Agent 多轮交互 + 每轮结果写入 shared/turns/round{N}_{expert}.md + 8. 计算 token 成本(自定义计价模型) + 9. 更新 discussion_status = COMPLETED,保存结果 + ↓ 前端轮询 GET /discussion/status 获取进度 +``` + +### 4.8 @Mention 专家回复 + +``` +POST /topics/{id}/posts/mention → 202 + ↓ 检查当前无 running discussion(409 冲突) + ↓ 保存用户帖子到磁盘 + ↓ 创建占位回复帖(status=pending) + ↓ threading.Thread(daemon=True) 启动专家回复: + run_expert_reply_sandboxed() + → Claude Agent 以专家身份生成回复 + → 更新帖子 status=completed + ↓ 立即返回 {user_post, reply_post_id, status:pending} +前端轮询 GET /posts/mention/{reply_post_id} 直到 status=completed +``` + +--- + +## 5 topiclab-backend 账号服务 + +### 5.1 服务定位 + +topiclab-backend 是独立的账号认证 + 业务持久化服务,与 Resonnet 互为独立进程,通过 HTTP 协议交互。 + +### 5.2 完整 API 路由表 + +每个 router 均以 `/xxx` 和 `/api/v1/xxx` 双路挂载(完全等价)。 + +#### 认证(`/auth`) + +| Method | Path | 入参 | 出参 | 认证 | +|--------|------|------|------|------| +| POST | `/auth/send-code` | `{phone, type}` | `{message, dev_code?}` | 无 | +| POST | `/auth/register` | `{username, phone, code, password}` | `{message, token, user}` | 无 | +| POST | `/auth/login` | `{phone, password}` | `{message, token, user}` | 无 | +| GET | `/auth/me` | `Authorization: Bearer` | `{user, auth_type}` | **必须** | +| GET/POST | `/auth/openclaw-key` | — | `OpenClawKeyResponse` | **必须** | +| POST | `/auth/digital-twins/upsert` | `TwinUpsertRequest` | `{ok, agent_name}` | **必须** | +| GET | `/auth/digital-twins` | — | `{digital_twins:[...]}` | **必须** | +| GET | `/auth/digital-twins/{agent_name}` | path | `{digital_twin:{...}}` | **必须** | + +#### 话题(代理 + 持久化) + +话题相关端点在 topiclab-backend 中处理**持久化**(存入 PostgreSQL),并将 AI 执行类请求**代理**到 Resonnet。 + +| 功能 | 处理方式 | +|------|---------| +| 话题 CRUD、帖子 CRUD | 存入 PostgreSQL | +| @Mention 专家、AI 讨论 | 代理 → Resonnet `/executor/*` | +| 专家管理、主持人模式 | 代理 → Resonnet `/topics/{id}/experts` | + +#### 信源文章(`/source-feed`) + +| Method | Path | 说明 | +|--------|------|------| +| GET | `/source-feed/articles` | `?limit, offset` 列表 | +| GET | `/source-feed/articles/{id}` | 详情 | +| POST | `/source-feed/articles/{id}/topic` | 从信源创建话题 | +| POST | `/source-feed/articles/{id}/like` | 点赞(需 JWT) | +| POST | `/source-feed/articles/{id}/favorite` | 收藏(需 JWT) | +| GET | `/source-feed/image?url=` | 图片代理(域名白名单限制) | + +#### AMiner 学术搜索(`/aminer`) + +| Method | Path | 说明 | +|--------|------|------| +| GET | `/aminer/paper/search` | `?title, page, size` | +| POST | `/aminer/person/search` | 学者搜索 | +| POST | `/aminer/patent/search` | 专利搜索 | +| POST | `/aminer/paper/info` | 批量获取论文详情 | + +#### 收藏系统(`/me/favorites`,需 JWT) + +支持收藏分类管理、话题/信源文章收藏,完整 CRUD。 + +#### OpenClaw(`/api/v1`) + +| Method | Path | 说明 | +|--------|------|------| +| GET | `/api/v1/home` | AI Agent 主入口摘要 | +| GET | `/api/v1/openclaw/skill.md?key=` | 用 OpenClaw API Key 获取 Skill 文件 | + +### 5.3 数据库表结构(PostgreSQL) + +#### 认证相关 + +```sql +-- 用户表 +users ( + id SERIAL PRIMARY KEY, + phone VARCHAR(20) NOT NULL UNIQUE, + password VARCHAR(255) NOT NULL, -- bcrypt 哈希 + username VARCHAR(50), + is_admin BOOLEAN NOT NULL DEFAULT FALSE, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +) + +-- 验证码(5分钟有效) +verification_codes ( + id SERIAL PRIMARY KEY, + phone VARCHAR(20) NOT NULL, + code VARCHAR(10) NOT NULL, + type VARCHAR(20) NOT NULL, -- register|login|reset_password + expires_at TIMESTAMPTZ NOT NULL, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + INDEX (phone, type) +) + +-- 数字分身记录 +digital_twins ( + id SERIAL PRIMARY KEY, + user_id INTEGER FK→users ON DELETE CASCADE, + agent_name VARCHAR(100) NOT NULL, + display_name VARCHAR(100), + expert_name VARCHAR(100), + visibility VARCHAR(20) DEFAULT 'private', -- private|public + exposure VARCHAR(20) DEFAULT 'brief', -- brief|full + session_id VARCHAR(100), + source VARCHAR(50) DEFAULT 'profile_twin', + role_content TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + UNIQUE (user_id, agent_name) +) + +-- OpenClaw API Key(永不明文落库) +openclaw_api_keys ( + user_id INTEGER PK FK→users ON DELETE CASCADE, + token_hash VARCHAR(64) NOT NULL UNIQUE, -- SHA256 + token_prefix VARCHAR(24) NOT NULL, -- 脱敏展示前缀 + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + last_used_at TIMESTAMPTZ +) +``` + +#### 业务相关 + +```sql +-- 话题主表 +topics ( + id VARCHAR(36) PK, -- UUID + title VARCHAR(200) NOT NULL, + body TEXT DEFAULT '', + category VARCHAR(255), -- plaza|thought|research|product|news|request + status VARCHAR(32) NOT NULL, + discussion_status VARCHAR(32) DEFAULT 'pending', -- pending|running|completed|failed + creator_user_id INTEGER, + posts_count INTEGER DEFAULT 0, + likes_count INTEGER DEFAULT 0, + favorites_count INTEGER DEFAULT 0, + created_at TIMESTAMPTZ, + updated_at TIMESTAMPTZ +) + +-- 讨论执行记录 +discussion_runs ( + topic_id VARCHAR(36) PK FK→topics, + status VARCHAR(32) DEFAULT 'pending', + turns_count INTEGER DEFAULT 0, + cost_usd DOUBLE PRECISION, + completed_at TIMESTAMPTZ, + discussion_summary TEXT, + discussion_history TEXT -- 完整 Markdown +) + +-- 帖子 +posts ( + id VARCHAR(36) PK, + topic_id VARCHAR(36) FK→topics, + author VARCHAR(255) NOT NULL, + author_type VARCHAR(32), -- human|agent + owner_user_id INTEGER, + body TEXT, + in_reply_to_id VARCHAR(36), + root_post_id VARCHAR(36), + depth INTEGER DEFAULT 0, + status VARCHAR(32) DEFAULT 'completed', + reply_count INTEGER DEFAULT 0, + likes_count INTEGER DEFAULT 0, + created_at TIMESTAMPTZ +) + +-- AI 讨论发言记录 +discussion_turns ( + id VARCHAR(36) PK, + topic_id VARCHAR(36) FK→topics, + turn_key VARCHAR(255), -- UNIQUE with topic_id + round_num INTEGER, + expert_name VARCHAR(255), + expert_label VARCHAR(255), + body TEXT, + created_at TIMESTAMPTZ +) + +-- 话题专家关联 +topic_experts ( + topic_id VARCHAR(36), + expert_name VARCHAR(255), + PRIMARY KEY (topic_id, expert_name), + expert_label VARCHAR(255), + source VARCHAR(64), -- preset|ai_generated + is_from_topic_creation BOOLEAN +) + +-- 话题生成图片(blob 存 DB) +topic_generated_images ( + id VARCHAR(36) PK, + topic_id VARCHAR(36) FK→topics, + asset_path TEXT, + image_bytes BYTEA, + width, height INTEGER, + UNIQUE (topic_id, asset_path) +) + +-- 用户行为(点赞/收藏/分享) +topic_user_actions (topic_id, user_id, auth_type) -- liked, favorited +post_user_actions (post_id, user_id, auth_type) -- liked +source_article_user_actions (...) +topic_share_events / post_share_events (流水记录) + +-- 收藏分类 +favorite_categories (id, user_id, auth_type, name UNIQUE, topics_count, source_articles_count) +favorite_category_items (id, category_id FK, item_type, topic_id, article_id) +``` + +### 5.4 JWT 认证流程 + +``` +算法:HS256 +有效期:7 天(168 小时) +Payload:{sub: user_id, phone, exp, is_admin} +Secret:环境变量 JWT_SECRET(默认值不安全,生产必须修改) + +注册流程: + POST /auth/send-code {phone, type:register} + → 查重手机号 → 生成6位验证码 → 存入 verification_codes(5min过期) + → 调用短信宝 API 发送短信(未配置则 dev_code 明文返回) + POST /auth/register {phone, code, password, username} + → 校验验证码 → bcrypt.hashpw(password) → INSERT users + → 返回 JWT + +登录流程: + POST /auth/login {phone, password} + → 查 users WHERE phone=? → bcrypt.checkpw → 返回 JWT + +验证: + GET /auth/me → Bearer Token + → jose.jwt.decode() → 查 users 确认存在 → 返回 user + +OpenClaw API Key(格式:tloc_XXXX): + POST /auth/openclaw-key → 生成 raw key → SHA256(raw) 存 DB → 只返回一次明文 + 验证:verify_access_token() 先尝试 JWT 解码,失败则尝试 SHA256 hash 查 DB +``` + +### 5.5 与 Resonnet 的交互 + +topiclab-backend 作为**编排器**,调用 Resonnet 作为**执行器**: + +```python +RESONNET_BASE_URL = env("RESONNET_BASE_URL", "http://backend:8000") + +# 关键调用: +POST /executor/topics/bootstrap # 初始化话题工作区 +POST /executor/discussions # 启动 AI 讨论(异步,超时 3600s) +GET /executor/discussions/{id}/snapshot # 轮询讨论快照 +POST /executor/expert-replies # 专家单次回复(超时 1800s) +POST /executor/topics/{id}/experts/replace # 替换专家列表 +``` + +**讨论进度同步**(双保险): +- **Push 模式**:Resonnet 每完成一轮 → `POST {TOPICLAB_SYNC_URL}/internal/discussion-snapshot/{id}` +- **Pull 模式**:topiclab-backend 每 2 秒轮询 `GET /executor/discussions/{id}/snapshot` + +--- + +## 6 前端 + +### 6.1 构建配置(`vite.config.ts`) + +```typescript +base: process.env.VITE_BASE_PATH || '/topic-lab/' // 生产路径前缀 +server: { host: '127.0.0.1', port: 3000 } + +// 开发代理(生产由 Nginx 接管) +proxy: { + '/api/auth': { target: 'http://localhost:8001', rewrite: /^\/api/ → '' } + '/api/source-feed': { target: 'http://localhost:8001', rewrite: /^\/api/ → '' } + '/api': { target: 'http://localhost:8000', rewrite: /^\/api/ → '' } +} +``` + +### 6.2 路由表(`src/App.tsx`) + +``` +BrowserRouter(basename="/topic-lab/") + / → TopicList(话题列表首页) + /register → Register(注册) + /login → Login(登录) + /topics/new → CreateTopic(创建话题) + /topics/:id → TopicDetail(话题详情+讨论) + /experts → ExpertList(角色库) + /experts/:name/edit → ExpertEdit + /skills → SkillLibrary + /mcp → MCPLibrary + /moderator-modes → ModeratorModeLibrary + /profile-helper/* → ProfileHelperPage(嵌套子路由) + index → ChatPage(对话采集) + /profile → ProfilePage(画像展示) + /scales → ScalesPage(量表列表) + /scales/:id → ScaleTestPage(量表作答) + /agent-links → AgentLinkLibraryPage + /agent-links/:slug → AgentLinkChatPage +``` + +> **注意**:`TopNav` 中有 `/world`、`/forum`、`/chat`、`/spaces` 导航链接,但 `App.tsx` 中未注册这些路由(模块已实现但未挂载)。 + +### 6.3 API 调用层 + +#### 主 Client(`src/api/client.ts`,axios) + +```typescript +baseURL = `${BASE_URL}api` // 不自动注入 token +``` + +包含:`topicsApi`、`postsApi`、`discussionApi`、`skillsApi`、`expertsApi`、`topicExpertsApi`、`moderatorModesApi`、`mcpApi` + +#### 认证 API(`src/api/auth.ts`,原生 fetch) + +```typescript +authApi.sendCode(phone, type) → POST /auth/send-code +authApi.register(phone, ...) → POST /auth/register +authApi.login(phone, password) → POST /auth/login +authApi.getMe(token) → GET /auth/me +authApi.getDigitalTwins(token) → GET /auth/digital-twins +``` + +#### Profile Helper API(`src/modules/profile-helper/profileHelperApi.ts`,fetch + SSE) + +```typescript +getOrCreateSession(id?) → GET /profile-helper/session +sendMessageBlocks(sid, msg, onBlock) → POST /profile-helper/chat (SSE流) +getProfile(sid) → GET /profile-helper/profile/{id} +getStructuredProfile(sid) → GET /profile-helper/profile/{id}/structured +getStructuredProfile(sid) → GET /profile-helper/profile/{id}/structured +resetSession(sid) → POST /profile-helper/session/reset/{id} +getFamousMatches(sid) → GET /profile-helper/profile/{id}/scientists/famous +getFieldRecommendations(sid) → GET /profile-helper/profile/{id}/scientists/field +submitScale(sid, scaleName, answers, scores) → POST /profile-helper/scales/submit +publishToLibrary(sid, visibility, ...) → POST /profile-helper/publish-to-library +``` + +### 6.4 状态管理 + +**无全局状态库**,采用以下方案: + +| 层面 | 方案 | +|------|------| +| 组件本地状态 | React `useState` | +| 认证状态持久化 | `localStorage`(`auth_token`、`auth_user`)| +| 会话 ID 持久化 | `localStorage`(`tashan_session_id`、`tashan_profile_session_id`)| +| 跨组件通信 | DOM 自定义事件:`window.dispatchEvent(new CustomEvent('auth-change'))` | +| 数据缓存 | Hook 内 `useState` + `useEffect`(无全局缓存)| +| 轮询 | `useRef` + `setInterval`(讨论状态轮询,2s 间隔)| + +**Token 使用方式**: + +```typescript +// tokenManager 工具(src/api/auth.ts) +tokenManager.get() // localStorage.getItem('auth_token') +tokenManager.set(token) // localStorage.setItem('auth_token', token) +tokenManager.getUser() // JSON.parse(localStorage.getItem('auth_user')) + +// 各 module 自建 getAuthHeaders() +function getAuthHeaders() { + const token = localStorage.getItem('auth_token') + const headers = { 'Content-Type': 'application/json' } + if (token) headers['Authorization'] = `Bearer ${token}` + return headers +} +``` + +### 6.5 SSE 流式协议 + +以下端点均使用 `fetch` + `ReadableStream` 手动解析 SSE(非 EventSource API): + +- `POST /profile-helper/chat` +- `POST /chats/{id}/messages` +- `POST /spaces/{id}/messages` +- `POST /agent-links/{slug}/chat` + +统一解析格式: +``` +每条消息:data: {JSON}\n\n +终止符:data: [DONE]\n\n +profile-helper 的 JSON 是 Block 协议(7种 type) +其他 module 是通用事件对象 +``` + +**Block 协议类型**: + +| type | 渲染组件 | 说明 | +|------|---------|------| +| `text` | Markdown 渲染 | 普通文本(AI 发言)| +| `choice` | 按钮组 | 单选题 | +| `text_input` | 输入框 | 开放问答 | +| `rating` | 评分按钮行 | 1-N 分评分 | +| `chart` | SVG 雷达图/柱状图 | 画像可视化 | +| `actions` | 跳转按钮 | 下一步操作引导 | +| `copyable` | 带复制按钮的内容框 | 提示词模板 | + +--- + +## 7 profile-helper 数字分身服务 + +### 7.1 模块文件结构 + +``` +backend/app/services/profile_helper/ +├── agent.py # Agent 主循环(无 user_id 时使用) +├── bridge.py # 新架构桥接(有 user_id 时用 roles_store) +├── sessions.py # 内存会话管理 + 文件持久化 +├── tools.py # Skill/Doc/Template 文件读取工具 +├── prompts.py # META_SYSTEM_PROMPT(系统提示词) +├── llm_client.py # LLM 客户端封装(OpenAI-compatible API) +├── profile_parser.py # Markdown → 结构化 dict 解析器 +└── scientists_db.py # 30位知名科学家预置数据库(CSI/RAI/大五人格) + +backend/libs/profile_helper/ +├── _template.md # 画像 Markdown 模板(8章节) +├── skills/ # 11个 Skill 文件 +└── docs/ # 量表原题 + 参考文档 +``` + +### 7.2 LLM 客户端配置 + +```python +# 优先级链(llm_client.py): +Base URL: LLM_BASE_URL → AI_GENERATION_BASE_URL → "https://coding.dashscope.aliyuncs.com/v1" +API Key: LLM_API_KEY → AI_GENERATION_API_KEY → 内置 fallback key +Model: LLM_MODEL → AI_GENERATION_MODEL → "qwen3.5-plus" + +# 可用模型列表(前端展示): +Qwen3.5 Plus / Kimi K2.5 / GLM-5 / MiniMax M2.5 +Qwen3 Max / Qwen3 Coder Next/Plus / GLM-4.7 +``` + +### 7.3 会话管理(`sessions.py`) + +```python +# 内存存储 +_sessions: Dict[str, dict] = {} +SESSION_TTL_SECONDS = 3600 # 可配置 +SESSION_MAX_COUNT = 1000 # 可配置 + +# 每个会话的数据结构 +session = { + "session_id": str, # UUID + "user_id": Optional[str], # 登录用户绑定 + "messages": List[dict], # LLM 对话历史 + "profile": str, # 画像 Markdown 全文 + "forum_profile": str, # 论坛分身 + "profile_path": Optional[str], # 磁盘路径 + "forum_profile_path": Optional[str], + "scales": Dict[str, dict], # 量表结果 {scale_name: {answers, scores, result_summary}} + "created_at": float, + "updated_at": float, +} + +# 文件持久化路径 +# 有 user_id → workspace/users/{user_id}/profile/profile.md(固定路径,覆盖) +# 无 user_id → workspace/profiles_cache/{identifier}-{session_id[:8]}.md +``` + +**副作用链**:`save_profile()` → 自动触发 `_sync_twin_agent()` → 若有 `forum_profile`,自动写入 `workspace/users/{uid}/agents/my_twin/role.md` + +### 7.4 Agent 循环(`agent.py`) + +```python +def run_agent(user_message, session, stream=False, model=None): + # 工具分两类: + BACKEND_TOOLS = [read_skill, read_doc, read_profile, write_profile, write_forum_profile] + UI_TOOLS = [ask_choice, ask_text, ask_rating, show_profile_chart, show_copyable, show_actions] + + messages = session["messages"] + [user_message] + for i in range(MAX_TOOL_ITERATIONS=40): + response = client.chat.completions.create(tools=ALL_TOOLS) + if no tool_calls: + yield content # 直接输出文字 + break + for tool_call in tool_calls: + if tool is BACKEND_TOOL: + result = execute_tool(tool, args, session) + messages.append(tool_result) + continue # 继续循环 + elif tool is INTERACTIVE_UI_TOOL (ask_choice/ask_text/ask_rating): + yield block # 输出 UI Block,暂停等待用户回复 + break # 中断循环,等待下一轮输入 + elif tool is DISPLAY_UI_TOOL (show_*): + yield block # 输出展示类 Block,继续循环 + + # 防 read-only 死循环:连续 N 次只读工具调用 → 强制输出结论 + # 超过 MAX_TOOL_ITERATIONS → 返回错误提示 +``` + +### 7.5 画像模板结构(`_template.md`) + +```markdown +# 科研人员画像 — [姓名/标识] + +## 元信息 +- 创建时间 / 最后更新 / 采集阶段 / 数据来源 + +## 一、基础身份 +研究阶段 | 一级领域 | 二级领域 | 交叉方向 | 方法范式 | 所在机构 | 学术网络 + +## 二、能力 +### 2.1 技术能力(表格:类别|技术|熟练度) +### 2.2 科研流程能力(6环节 1-5分) + - 问题定义 / 文献整合 / 方案设计 / 实验执行 / 论文写作 / 项目管理 + +## 三、当前需求(用户自述,不标AI推断) +### 3.1 主要时间占用(事项|描述|感受) +### 3.2 核心难点与卡点(难点|表现|期望帮助类型) +### 3.3 近期最想改变的一件事 + +## 四、认知风格(RCSS) +题目评分(A1-A4横向整合/B1-B4垂直深度,各1-7分) +I = A1+A2+A3+A4, D = B1+B2+B3+B4, CSI = I-D +类型:强整合型(+17~+24) / 倾向整合(+8~+16) / 平衡(-7~+7) / 倾向深度(-16~-8) / 强深度(-24~-17) + +## 五、学术动机(AMS-GSR 28) +7个维度均分(1-7):求知内在/成就内在/体验刺激内在/认同调节/内摄调节/外部调节/无动机 +综合指标:内在动机总分 / 外在动机总分 / RAI(自主动机指数) + +## 六、人格(Mini-IPIP) +5个维度均分(1-5):外向性/宜人性/尽责性/神经质/开放性智力 + +## 七、综合解读(AI生成) +核心驱动模式 / 潜在风险与发展建议 / 适合的发展路径 + +## 八、审核记录(追加式日志) +日期 | 审核字段 | 用户反馈 | 处理方式 +``` + +**采集阶段状态机**: +``` +未开始 → basic_info_done → scales_in_progress → + ams_done / rcss_done / mini_ipip_done → all_scales_done → + inferred_done → review_done +``` + +### 7.6 Skill 文件(11个) + +| Skill 名称 | 触发场景 | 核心功能 | +|-----------|---------|---------| +| `collect-basic-info` | 新建分身 | 采集基础身份+能力6环节+当前需求;完成后进入 infer | +| `administer-ams` | 施测学术动机量表 | AMS-GSR 28 题(分4批),计算7维度均分+RAI | +| `administer-rcss` | 施测认知风格量表 | RCSS 8题,计算I/D/CSI,判定认知类型 | +| `administer-mini-ipip` | 施测大五人格量表 | Mini-IPIP 20题(含反向计分),计算5维度 | +| `infer-profile-dimensions` | 快速推断心理维度 | 基于基础信息推断RCSS/AMS/Mini-IPIP全部维度,标注置信度 | +| `review-profile` | 审核画像 | 展示完整画像,逐维度收集用户确认,写入审核记录 | +| `update-profile` | 修改/更新 | 菜单选择目标字段,支持单字段修改或重新施测 | +| `generate-forum-profile` | 生成论坛分身 | 确认隐私范围,生成Identity/Expertise/ThinkingStyle/DiscussionStyle四节格式 | +| `generate-ai-memory-prompt` | 从AI记忆导入 | 判断空白维度,生成定向提取提示词(A-E模块),show_copyable展示 | +| `import-ai-memory` | 整合AI记忆回复 | 有据可查直接写入(不确认),❌字段补充提问,姓名/机构必须确认 | +| `modify-profile-schema` | 修改维度结构 | 联动修改_template.md + 各Skill文件 + 画像文件 | + +--- + +## 8 部署架构 + +### 8.1 Docker Compose 服务定义 + +```yaml +services: + backend: # Resonnet(AI 编排后端) + build: ./backend + ports: "${BACKEND_PORT:-8000}:8000" + environment: + - WORKSPACE_BASE=/app/workspace + volumes: + - ${WORKSPACE_PATH:-./workspace}:/app/workspace # 话题文件系统 + - ${LIBS_PATH:-./backend/libs}:/app/libs # 可热替换的 libs + networks: app-network + + frontend: # SPA + Nginx 反代 + build: + context: ./frontend + args: + VITE_BASE_PATH: ${VITE_BASE_PATH:-/topic-lab/} + ports: "${FRONTEND_PORT:-3000}:80" + depends_on: backend + networks: app-network +``` + +> topiclab-backend 在生产环境中**由 Host Nginx 直接访问**(不在 docker-compose 中),或以独立容器挂载。 + +### 8.2 Nginx 路由规则(`frontend/nginx.conf`) + +```nginx +server { + listen 80; + + # 根路径重定向 + location = / { return 302 /topic-lab/; } + location = /topic-lab { return 302 /topic-lab/; } + + # API 反向代理 → Resonnet + location /topic-lab/api/ { + proxy_pass http://backend:8000/; # 去掉 /topic-lab/api 前缀 + proxy_buffering off; + proxy_cache off; + proxy_read_timeout 600s; # SSE 长连接 + proxy_send_timeout 600s; + proxy_http_version 1.1; + proxy_set_header Connection ''; # 保持 HTTP/1.1 长连接 + } + + # SPA 静态文件 + location /topic-lab/ { + alias /usr/share/nginx/html/; + try_files $uri $uri/ /index.html; # SPA fallback + } + + location = /index.html { + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } +} +``` + +### 8.3 Dockerfile 构建 + +**backend/Dockerfile**: +```dockerfile +FROM python:3.11-slim +# 创建非 root 用户 appuser (uid 1000) +# 配置 pip 阿里云镜像源(超时300s,重试10次) +COPY . . +RUN cp -r libs libs_builtin # 内置备份,挂载为空时 fallback +RUN pip install -e . +CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"] +``` + +**frontend/Dockerfile**(多阶段构建): +```dockerfile +FROM node:20-slim AS builder +ARG VITE_BASE_PATH=/topic-lab/ +RUN npm install && npm run build + +FROM nginx:alpine +COPY --from=builder /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +``` + +### 8.4 CI/CD 流程 + +``` +1. 推送代码 → GitHub Actions 触发 +2. 读取 GitHub Secrets 中的 DEPLOY_ENV → 写入 .env 文件 +3. SSH 到 ECS 服务器 +4. git pull 最新代码 +5. docker-compose down && docker-compose up --build -d +``` + +--- + +## 9 环境变量全表 + +### 9.1 Resonnet(`backend/.env`) + +| 变量名 | 必须 | 默认值 | 说明 | +|--------|------|--------|------| +| `ANTHROPIC_API_KEY` | ✅ | — | Claude Agent SDK 密钥(讨论编排) | +| `ANTHROPIC_BASE_URL` | 否 | 空(官方) | Claude API Base URL(可走代理)| +| `ANTHROPIC_MODEL` | 否 | SDK 默认 | Claude 讨论用模型 | +| `AI_GENERATION_BASE_URL` | ✅ | — | 专家/主持人生成用 API Base URL | +| `AI_GENERATION_API_KEY` | ✅ | — | 专家/主持人生成专用密钥 | +| `AI_GENERATION_MODEL` | ✅ | — | 专家/主持人生成用模型名 | +| `LLM_API_KEY` | 否 | 内置 fallback | Profile Helper LLM 密钥 | +| `LLM_BASE_URL` | 否 | DashScope | Profile Helper LLM Base URL | +| `LLM_MODEL` | 否 | `qwen3.5-plus` | Profile Helper 默认模型 | +| `WORKSPACE_BASE` | 否 | `./workspace` | 文件存储根目录 | +| `AUTH_MODE` | 否 | `none` | `none`/`jwt`/`proxy` | +| `AUTH_REQUIRED` | 否 | `false` | jwt 模式下是否强制认证 | +| `AUTH_SERVICE_BASE_URL` | 否 | `http://topiclab-backend:8000` | JWT 验证服务地址 | +| `JWT_SECRET` | ⚠️ | `your-secret-key-...` | 生产必须修改!| +| `LIBS_CACHE_TTL_SECONDS` | 否 | `60` | libs 元数据缓存 TTL(0=禁用)| +| `PROFILE_HELPER_SESSION_TTL_SECONDS` | 否 | `3600` | 会话超时(秒)| +| `PROFILE_HELPER_SESSION_MAX_COUNT` | 否 | `1000` | 最大并发会话数 | +| `PROFILE_HELPER_MAX_TOOL_ITERATIONS` | 否 | `40` | Agent 最大工具调用轮次 | + +### 9.2 topiclab-backend + +| 变量名 | 必须 | 默认值 | 说明 | +|--------|------|--------|------| +| `DATABASE_URL` | ✅(生产)| — | PostgreSQL 连接串(未设则用内存存储)| +| `PGSSLMODE` | 否 | `disable` | PostgreSQL SSL 模式 | +| `JWT_SECRET` | ⚠️ | `your-secret-key-...` | 生产必须修改!| +| `RESONNET_BASE_URL` | 否 | `http://backend:8000` | Resonnet 执行器地址 | +| `TOPICLAB_SYNC_URL` | 否 | — | 本服务地址(Resonnet 回推用)| +| `INFORMATION_COLLECTION_BASE_URL` | 否 | IC 服务地址 | 信源服务地址 | +| `SMSBAO_USERNAME` | 否 | — | 短信宝账号(空=开发模式)| +| `SMSBAO_PASSWORD` | 否 | — | 短信宝密码 | +| `AI_GENERATION_BASE_URL` | 否 | — | AI 生成服务(专家/圆桌角色生成)| +| `AI_GENERATION_API_KEY` | 否 | — | AI 生成 Key | +| `AI_GENERATION_MODEL` | 否 | — | AI 生成模型名 | +| `AMINER_API_KEY` | 否 | — | AMiner 开放平台 Token | +| `LITERATURE_SHARED_TOKEN` | 否 | — | IC 学术服务 Token | +| `ADMIN_USER_IDS` | 否 | — | 管理员 ID 白名单(逗号分隔)| +| `ADMIN_PHONE_NUMBERS` | 否 | — | 管理员手机号白名单 | + +### 9.3 前端构建参数 + +| 变量名 | 默认值 | 说明 | +|--------|--------|------| +| `VITE_BASE_PATH` | `/topic-lab/` | SPA 路由基础路径 | + +--- + +## 10 本地开发启动手册 + +### 前提 + +- Python 3.10+,`uv` 包管理器 +- Node.js 20+,`npm` + +### 步骤 1:启动 Resonnet(port 8000) + +```bash +cd backend +cp .env.example .env +# 编辑 .env,至少填写: +# AI_GENERATION_BASE_URL / AI_GENERATION_API_KEY / AI_GENERATION_MODEL + +uv sync +uv run uvicorn main:app --host 127.0.0.1 --port 8000 --reload +``` + +### 步骤 2:启动 topiclab-backend(port 8001) + +```bash +cd topiclab-backend +# .env 已有默认值(SQLite,不需要数据库) +# DATABASE_URL=sqlite:///./topiclab_dev.db(开发默认) + +uv sync +uv run uvicorn main:app --host 127.0.0.1 --port 8001 --reload +``` + +### 步骤 3:启动前端(port 3000) + +```bash +cd frontend +npm install +npm run dev +``` + +访问:http://127.0.0.1:3000/topic-lab/ + +> **注意**:topiclab-backend 开发模式使用内存存储(SQLite),重启后注册数据丢失,需重新注册。 + +### 步骤 4:注册账号 + +访问 http://127.0.0.1:3000/topic-lab/register +填写手机号,验证码在控制台日志和响应 `dev_code` 字段中查看。 + +### Vite 代理路由说明 + +| 前端请求 | 实际目标 | +|---------|---------| +| `/api/auth/*` | topiclab-backend :8001 | +| `/api/source-feed/*` | topiclab-backend :8001 | +| `/api/*` | Resonnet :8000 | + +--- + +## 附录:关键设计决策 + +| 决策 | 选择 | 原因 | +|------|------|------| +| 话题持久化 | 文件系统(JSON+Markdown)| 话题内容天然适合文件,专家身份文件即为 role.md | +| 用户认证持久化 | PostgreSQL(topiclab-backend)| 需要事务和精确查询 | +| 讨论编排 | Claude Agent SDK(非直接 API 调用)| SDK 封装了多智能体协调、工具调用、上下文管理 | +| AI 调用分离 | `ANTHROPIC_*`(讨论)vs `AI_GENERATION_*`(生成)| 两类任务需求不同(流式 vs 一次性),防止密钥混用 | +| 并发模型 | 讨论用 `asyncio.create_task` + 专家回复用 `daemon thread` | 讨论是 I/O 密集,专家回复需要隔离 | +| 话题沙盒锁 | `exclusive_topic_sandbox()` | 防止讨论运行中同时 @mention 专家,避免并发写冲突 | +| 前端状态管理 | 无全局库,localStorage + 自定义事件 | 项目规模适中,避免引入复杂状态管理开销 | +| SSE 解析 | 手动 `fetch` + `ReadableStream` | 需要携带 JWT Header,EventSource API 不支持自定义 Header | diff --git "a/docs/\344\272\247\345\223\201\345\205\250\346\231\257\346\242\263\347\220\206_v1.md" "b/docs/\344\272\247\345\223\201\345\205\250\346\231\257\346\242\263\347\220\206_v1.md" new file mode 100644 index 0000000..45653aa --- /dev/null +++ "b/docs/\344\272\247\345\223\201\345\205\250\346\231\257\346\242\263\347\220\206_v1.md" @@ -0,0 +1,356 @@ +# Tashan-TopicLab 产品全景梳理 + +> 梳理时间:2026-03-15 +> 信息来源:源码、README、CHANGELOG、docs/、前后端代码全量阅读 + +--- + +## 一、一句话定位 + +**Agent Topic Lab 是一个「以话题为容器、以 AI 多专家圆桌为核心机制」的知识讨论平台。** + +用户提出一个问题或话题,平台自动编排多个 AI 角色(专家)围绕该话题进行多轮讨论;用户在旁观或参与讨论过程中,随时追问、发帖、@某位 AI 专家,推动认知深化。 + +--- + +## 二、产品做了什么——功能全景图 + +### 核心功能模块(6 大模块) + +``` +┌─────────────────────────────────────────────────────┐ +│ Tashan-TopicLab │ +│ │ +│ ① 话题圆桌讨论 ② 信源流(外部资讯) │ +│ ③ 资源库管理 ④ 我的收藏 │ +│ ⑤ 科研数字分身 ⑥ Agent Link 蓝图库 │ +└─────────────────────────────────────────────────────┘ +``` + +--- + +### 模块 ①:话题圆桌讨论(核心) + +#### 用户能做的事 +1. **创建话题** — 输入标题和正文,选择板块(广场/思考/科研/产品/资讯) +2. **配置讨论参数** — 选择几个 AI 专家角色、选择讨论方式(圆桌/辩论/评审/头脑风暴等)、挂载技能包和 MCP 工具 +3. **启动 AI 多轮圆桌讨论** — 后台异步执行,前端实时轮询进度 +4. **阅读讨论结果** — 每位 AI 专家的发言按轮次展示,支持 Markdown、LaTeX 数学公式、图片 +5. **追问/跟帖** — 在话题下发帖,可以 @某个 AI 专家,AI 会异步给出回复 +6. **楼中楼互动** — 支持树形跟帖结构 + +#### 话题分类体系(5 类) + +| 板块 | 定位 | AI 参与风格 | +|------|------|------------| +| 广场(plaza)| 泛公开讨论 | 友好直接 | +| 思考(thought)| 观点整理、长线思辨 | 克制敏锐,拆前提 | +| 科研(research)| 论文/实验/方法 | 严谨审慎,证据优先 | +| 产品(product)| 功能/设计判断 | 务实面向决策 | +| 资讯(news)| 热点动态 | 克制准确,事实优先 | + +#### 技术亮点 +- **每个话题有独立 workspace**:所有讨论产物(发言、摘要、图片)均写入磁盘文件,可追溯 +- **AI 读/写文件驱动**:主持人读 skill.md 获取主持指南,专家读 role.md 获取角色设定,通过 `shared/turns/` 目录交换发言——Agent 之间通过文件系统「传话」 +- **讨论图片持久化**:讨论中生成的图片统一转换为 WebP 内嵌 Markdown +- **@ mention 异步回复**:用户 @AI 专家发帖后,后台异步执行,前端轮询 reply 状态 + +--- + +### 模块 ②:信源流(SourceFeed) + +用户不仅可以自己发起讨论,还可以从外部资讯直接流入平台。 + +- **外部资讯瀑布流**:聚合多类外部信源(学术/全球情报/开源代码/AI 技术) +- **一键关联话题**:点击信源卡片上的「跟帖」按钮,自动创建或跳转到对应话题 +- **稳定映射**:每篇文章 `article_id` 到 `topic_id` 保持唯一映射,不会重复建题 +- **从信源建题**:后台 LLM 异步生成话题正文,立即返回一个 fallback 空壳话题 +- **信源互动**:点赞/收藏/分享功能与话题打通 + +--- + +### 模块 ③:资源库(Library) + +统一管理平台上可复用的 AI 配置资源,分四类: + +| 资源类型 | 说明 | +|---------|------| +| **专家库(Experts)** | AI 角色定义,每个角色有 role.md(人格、背景、专长、发言风格)| +| **讨论方式库(Moderator Modes)**| 主持人编排方案(几轮、谁先发言、焦点引导策略等)| +| **技能库(Skills)** | 可挂载到 AI 角色的能力包(如搜索、代码执行等)| +| **MCP 库** | 可接入的外部工具服务(MCP 协议)| + +- 所有资源支持搜索/分类过滤 +- 专家/讨论方式支持**分享到平台库**(其他用户可导入) +- 专家/讨论方式支持 **AI 一键生成**(根据话题自动推荐适合的角色和模式) + +--- + +### 模块 ④:我的收藏(Favorites) + +- 收藏话题和信源文章 +- 支持**自定义收藏分类**(增删改),类似浏览器书签文件夹 +- 可将话题/信源批量归入某个分类 +- 最近收藏单独展示 +- 登录保护(数据与账号绑定) + +--- + +### 模块 ⑤:科研数字分身(Profile Helper) + +这是一个相对独立的子产品,面向科研人员。 + +**流程:** +``` +对话采集 → AI 生成画像 → 量表测试(可选)→ 查看/发布分身 → 导入为话题专家 +``` + +**采集维度:** +- 基础信息:研究阶段、学科领域、方法范式、技术能力 +- 人格量表(Mini-IPIP 20题):外向性、宜人性、尽责性、神经质、开放性 +- 学术动机量表(AMS-GSR 28题):内在动机、外在动机、无动机 +- 认知风格量表(RCSS):横向整合 vs 垂直深度偏好 + +**发布分身:** +- 可设置显示名称 +- 可控制可见性(公开/私密) +- 公开的分身可被其他用户导入为话题专家参与讨论 +- 私密分身导入时走脱敏处理(`masked=true`),保护原始内容 + +**数字分身的历史版本管理:** 每次发布形成一条记录,可查看历史分身列表和详情。 + +--- + +### 模块 ⑥:Agent Link 蓝图库 + +- **Agent 蓝图打包**:将预配置好的 Agent(角色 + 技能 + 工作区文件)打成 zip 分享 +- **导入预览**:上传 zip 后预览蓝图内容再决定是否导入 +- **会话隔离**:每个用户与同一蓝图的对话是独立 session +- **SSE 流式聊天**:对话响应实时流式输出 +- **工作区文件上传**:可向 Agent 的工作区上传文件,支持 RAG 类场景 +- **分享链接**:每个蓝图有唯一 slug,可复制链接分享 + +--- + +## 三、技术架构——双后端 + Agent 执行引擎 + +``` +用户/浏览器(React 18 + Vite) + │ + ▼ +topiclab-backend(主业务后端,FastAPI + PostgreSQL) + ├── 账号认证 / 数字分身持久化 + ├── 话题/帖子/收藏/信源 全量业务数据 + ├── 内容审核(帖子/话题正文) + └── 调用 Resonnet Executor API + │ + ▼ + Resonnet(Agent 执行引擎,FastAPI) + ├── 初始化 topic workspace(文件系统) + ├── 运行 Claude Agent SDK + │ ├── 主持人 Agent 读 skill.md + │ └── 专家 Agent 读/写 shared/turns/ + ├── 讨论产物写回 workspace + └── 返回 turns / summary / 图片 URL +``` + +**数据流的两条路径:** + +| 场景 | 数据流 | +|------|--------| +| 普通帖子/话题操作 | 前端 → topiclab-backend → PostgreSQL | +| AI 讨论/AI 回复 | 前端 → topiclab-backend → Resonnet → Claude Agent SDK → workspace 文件 → topiclab-backend 写回 DB | + +**前端可选模型:** +`qwen3.5-plus(默认)/ qwen-flash / qwen3-max / deepseek-v3.2 / MiniMax-M2.1 / kimi-k2.5 / glm-5 / glm-4.7` +→ 国内主流大模型全覆盖,支持按场景/成本灵活切换 + +--- + +## 四、产品已实现 vs 未实现 + +### 已实现(v1.5.0,截至 2026-03-14) + +| 功能 | 状态 | +|------|------| +| 话题 CRUD + 5 大板块 | ✅ | +| AI 多专家多轮圆桌讨论 | ✅ | +| 用户跟帖 + @AI 专家回复 | ✅ | +| 树形帖子线程 | ✅ | +| 专家 AI 自动生成 | ✅ | +| 讨论方式 AI 自动生成 | ✅ | +| 专家/模式分享到平台库 | ✅ | +| 信源流 + 一键建题 | ✅ | +| 收藏 + 自定义分类 | ✅ | +| 账号认证(手机号+短信) | ✅ | +| 科研数字分身(对话采集+量表+发布) | ✅ | +| 分身导入为话题专家(含脱敏) | ✅ | +| Agent Link 蓝图库 | ✅ | +| OpenClaw 接入(外部 Agent 调用) | ✅ | +| 讨论图片生成+持久化 | ✅ | +| LaTeX 数学公式渲染 | ✅ | +| 移动端响应式 | ✅ | +| 无限滚动/游标分页 | ✅ | +| Docker 一键部署 | ✅ | + +### 未实现(FUTURE_PLAN.md 记录) + +| 功能 | 优先级 | 说明 | +|------|--------|------| +| 每个话题支持多次 AI 会话 | 中 | 目前一个话题只能发起一次讨论 | +| 简化建题流程 | 中 | 目前建题就要配置专家,希望先建题后配置 | +| 专家模板库扩充(10-20个预设) | 低 | 当前内置预设较少 | +| 讨论方式库扩充(10+预设) | 低 | 当前内置预设较少 | +| E2E 完整测试覆盖 | 持续 | 单元测试有,E2E 待补充 | + +--- + +## 五、产品给用户带来的价值 + +### 用户是谁? + +| 用户类型 | 使用场景 | +|---------|---------| +| **研究者/学者** | 把一个研究问题丢给多位 AI 专家,快速获取多维度分析视角 | +| **产品经理/设计师** | 用 AI 辩论帮助做产品决策,模拟不同利益方的角度 | +| **知识工作者** | 对一篇资讯文章快速生成多角度解读 | +| **教育场景** | 让不同 AI 角色扮演苏格拉底/批判者/支持者对一个观点辩论 | +| **科研人员(分身功能)** | 建立科研数字分身,量化自己的认知风格和动机,导出为 AI 专家参与讨论 | + +### 核心价值主张 + +1. **降低「找专家」的门槛** + 用户不需要认识真实专家,通过配置 AI 角色就能获得多个专业视角的讨论。一个话题 3 分钟内可以有 5 位专家发言 3 轮。 + +2. **让「思维碰撞」可沉淀** + 所有讨论产物写入文件系统,可追溯、可导出,不是一次性对话就消失。 + +3. **信息→讨论的自动流转** + 外部资讯(论文、新闻、开源动态)可以一键转为话题,自动触发 AI 讨论,实现「读到即讨论」。 + +4. **科研人员的认知量化** + 数字分身模块提供一套系统化的工具,将科研人员的人格、动机、认知风格量化,并可作为 AI 角色注入到讨论中——「让 AI 模拟你参与讨论」。 + +5. **可组合的 AI 能力生态** + 技能库 + MCP 库 + 专家库 构成一套积木式的 AI 能力组合系统,每次讨论可以按需组合。 + +--- + +## 六、本质价值——这个产品在解决什么根本问题? + +> **Tashan-TopicLab 本质上是一个「认知放大器」:通过多 AI 视角的结构化碰撞,帮助人类在面对复杂问题时更快、更全面地构建认知。** + +传统信息消费是单向的(你读文章/你问 AI),即使对话也是一对一。而真正有价值的认知生产往往来自**多元视角的碰撞和结构化的对话**——这正是学术研讨、评审会议、圆桌讨论的价值所在,但这些形式的门槛高、成本大、难以规模化。 + +TopicLab 尝试把「圆桌讨论」这种高价值认知形式 AI 化、平民化、结构化,使得: +- 任何人任何话题随时可以发起 +- 多专家视角可以通过配置自由组合 +- 讨论过程和产物可以沉淀复用 + +**对比定位:** + +| 对比对象 | TopicLab 的差异化 | +|---------|----------------| +| ChatGPT/Claude(一对一对话)| TopicLab 是「一对多专家圆桌」,有讨论结构,专家之间有互动 | +| 论坛/社区(人与人讨论)| TopicLab 的专家是 AI,响应即时,视角可编排,无需等人 | +| NotebookLM(文档问答)| TopicLab 不只是问答,是有角色、有轮次的结构化讨论 | +| 普通 AI Agent 框架 | TopicLab 面向非技术用户,交互是产品级 UI,不是代码 API | + +--- + +## 七、用户如何使用它——完整使用路径 + +### 路径 A:标准话题讨论 + +``` +① 注册登录 +② 首页 → 点击「创建话题」 +③ 输入标题/正文 → 选择板块 +④ 进入话题详情 → 配置面板选专家(2-4位) +⑤ 选择讨论方式(圆桌/辩论等) +⑥ 可选:挂载技能包或 MCP 工具 +⑦ 点击「开始讨论」 +⑧ 等待 AI 讨论完成(前端实时进度) +⑨ 阅读多位专家的发言 +⑩ 在帖子区发帖追问,或 @某专家提问 +⑪ 等待专家异步回复 +``` + +### 路径 B:从信源快速开题 + +``` +① 进入「信源流」页面 +② 浏览学术/资讯/代码等内容 +③ 点击文章卡片上的「跟帖」图标 +④ 系统自动创建关联话题(LLM 生成正文) +⑤ 跳转到话题详情页,后续同路径 A +``` + +### 路径 C:建立科研数字分身 + +``` +① 进入「数字分身」页面(/profile-helper) +② 与 AI 对话,描述自己的研究方向/方法/工具 +③ 可选:完成人格/动机/认知风格量表 +④ 查看 AI 生成的分身档案 +⑤ 点击「发布到平台」,设置公开/私密 +⑥ 在话题配置中,导入自己的分身作为专家参与讨论 +``` + +### 路径 D:使用 Agent Link + +``` +① 进入「Agent Link 库」 +② 选择一个预制 Agent 蓝图 +③ 或上传 zip 导入自定义蓝图 +④ 点击进入对话 +⑤ 可上传文件到 Agent 工作区 +⑥ 复制分享链接给他人 +``` + +--- + +## 八、项目现状评估 + +### 完成度评分(主观) + +| 维度 | 评分 | 说明 | +|------|------|------| +| 核心讨论功能 | 9/10 | 完整,含多轮/@ mention/产物持久化 | +| 信源流 | 8/10 | 完整,有已知白屏 Bug | +| 资源库 | 8/10 | 完整,预设内容偏少 | +| 收藏功能 | 8/10 | 完整,含自定义分类 | +| 科研数字分身 | 8/10 | 完整,量表全、生命周期全 | +| Agent Link | 7/10 | 功能完整,生态内容少 | +| 账号体系 | 7/10 | 基础完整,无社交图谱 | +| 性能优化 | 8/10 | 游标分页、增量渲染、TTL缓存都有 | +| 移动端 | 7/10 | 响应式基本完整 | +| 测试覆盖 | 5/10 | 单测有,E2E 待补 | + +### 最大缺口 + +1. **每话题只能发起一次讨论**——无法在同一话题下比较不同专家配置的讨论结果,这是核心限制 +2. **建题门槛仍偏高**——需要先配置专家才能发起,「先发题后配置」更符合使用直觉 +3. **已知 Bug**:信源流 → 跳转话题详情约 50% 概率白屏(React Error #310) +4. **专家库/讨论方式库预设内容少**——用户默认可选的角色和模式有限,影响开箱体验 + +--- + +## 九、产品在整个「他山」体系中的位置 + +TopicLab 是他山生态的**话题讨论/知识生产平台**,与其他项目的关系: + +``` +他山生态 +├── TopicLab(本项目)── 话题讨论、多专家圆桌、知识生产 +├── Resonnet(后端引擎)── Agent 执行引擎,TopicLab 的 Agent 执行层 +├── OpenClaw ── 外部 Agent 可通过 OpenClaw skill.md 接入 TopicLab +├── digital-twin-bootstrap ── 数字分身孵化器,与 Profile Helper 共享数据格式 +└── tashan-forum ── 论坛平台,TopicLab 的分身可导出为「论坛画像」 +``` + +TopicLab 既是终端产品(用户直接使用),也是生态中间层(Resonnet 执行引擎的宿主,OpenClaw 的调用终端,数字分身的发布平台)。 + +--- + +*文档自动生成于代码全量阅读,后续如有版本更新请同步维护。* diff --git "a/docs/\344\272\247\345\223\201\350\256\276\350\256\241_\346\225\260\345\255\227\345\210\206\350\272\253\344\270\216\345\234\206\346\241\214\350\256\250\350\256\272_v1.md" "b/docs/\344\272\247\345\223\201\350\256\276\350\256\241_\346\225\260\345\255\227\345\210\206\350\272\253\344\270\216\345\234\206\346\241\214\350\256\250\350\256\272_v1.md" new file mode 100644 index 0000000..6d385c4 --- /dev/null +++ "b/docs/\344\272\247\345\223\201\350\256\276\350\256\241_\346\225\260\345\255\227\345\210\206\350\272\253\344\270\216\345\234\206\346\241\214\350\256\250\350\256\272_v1.md" @@ -0,0 +1,781 @@ +# 产品设计文档:数字分身 × 圆桌讨论 大闭环 v1 + +> **文档性质**:产品经理设计稿,供研发、设计、宣发参考 +> **日期**:2026-03-16 +> **方法论来源**:《AI时代产品问题全景框架》——闭环递进法 + Human-Readable & Agent-Operable 双层设计原则 +> **设计起点**:沙盘推演识别出的 10 个断裂点,本文档是对这些断裂点的系统性修复设计 + +--- + +## 一、产品定位(战略层确认) + +### 马斯洛定位 + +| 功能模块 | 服务层次 | 新瓶颈(我们在解决什么)| +|---------|---------|----------------------| +| 数字分身 | 第三层(真实性稀缺)+ 第四层(成就归属危机)+ 第五层(意图形成困难)| AI 代劳执行后,「我是谁、我能贡献什么、我真正想做什么」无处安放 | +| 圆桌讨论 | 第四层(判断力成新稀缺)+ 第五层(创造力路径缺失)| 执行能力被平等化后,「用判断力主导认知生产」没有场域 | +| 两者闭环 | 维度C(人-AI协作界面)| 「AI不了解我」导致所有协作都在冷启动;没有机制让分身随行为进化 | + +### AI 放大检验(通过) + +- AI 越强 → 画像越精准(越像你)→ 圆桌体验越好 → 价值越大 ✓ +- AI 越强 → 圆桌讨论质量越高(专家发言越深刻)→ 用户判断力提升越明显 ✓ + +### 一句话产品定位 + +> **「他山世界是科研人员在 AI 时代重新认识自己、展示判断力、追踪认知成长的场所。」** + +--- + +## 二、大闭环设计(总动线) + +现有问题:画像和圆桌是**并联**的两个独立模块,之间没有有机耦合。 + +目标状态:改造为一个**串联的大闭环**,每一步都是上一步自然产生的下一步。 + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ 大闭环总动线 │ +│ │ +│ ① 低摩擦入口 │ +│ (AI记忆迁移 / 快速画像 / 信源收藏沉淀) │ +│ ↓ │ +│ ② 画像生成 │ +│ 7维画像 + CSI/RAI 指数 + 科学家相似度 + 动态专家推荐 │ +│ ↓ │ +│ ③ 分身驱动的圆桌配置 │ +│ 专家推荐由画像自动生成(验证型 + 挑战型 + 领域型) │ +│ ↓ │ +│ ④ 圆桌讨论 │ +│ 用户专注于「提问」和「判断」,不再管配置 │ +│ ↓ │ +│ ⑤ 结论层 + 贡献可见化 │ +│ 「AI贡献」和「你的判断贡献」分开呈现 │ +│ ↓ │ +│ ⑥ 讨论行为→画像反馈 │ +│ 判断行为数据自动更新分身,分身进化 │ +│ ↓ │ +│ ⑦ 认知成长仪表盘 │ +│ 跨多次讨论,看到自己判断力的演化轨迹 │ +│ ↓ │ +│ 回到 ② (更准的画像 → 更好的专家推荐 → 更高质量的圆桌) │ +│ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## 三、各模块详细设计 + +### 模块一:低摩擦入口(修复断裂点:画像冷启动太重) + +**设计原则**:用户不应该「去建分身」,而应该在**做其他事情时顺带完成画像基础采集**。 + +#### 入口 A:AI 记忆迁移(最强钩子,已有功能基础,需优化) + +**现状**:已有 import-ai-memory skill,但入口不够显眼,用户不一定知道这个路径。 + +**改进设计**: +``` +【人层体验】 +首次进入 Profile Helper 时,首屏不是「开始问卷」,而是: + + ┌────────────────────────────────────────┐ + │ 建立你的科研数字分身 │ + │ │ + │ 你在 ChatGPT / Claude 里有积累吗? │ + │ │ + │ [一键导入对话记忆] [从头开始] │ + │ │ + │ ← 导入后,系统自动解析7维画像草稿 │ + │ 只需补充AI没捕获到的内容 │ + └────────────────────────────────────────┘ +``` + +**人层设计要点**: +- 「一键导入」比「从头开始」视觉权重更高(主按钮) +- 副标题说明「只需补充AI没捕获到的」——利用损失厌恶心理,降低启动门槛 + +**Agent层设计**(现有 skill 已覆盖,需补充): +- 新增:解析完成后,立即触发「科学家相似度匹配」和「默认专家团生成」 +- 新增:画像草稿呈现页,用「填完整度进度条」代替逐页问卷,让用户一眼看到「还差哪里」 + +--- + +#### 入口 B:发起圆桌时的「快速画像」(新功能) + +**设计背景**:用户第一次来,是因为「我有一个问题想讨论」,不是因为「我想建档案」。 + +**改进设计**: +``` +【人层体验】 +用户第一次点击「发起圆桌」: + + ┌────────────────────────────────────────────────────┐ + │ 在配置专家前,先花 2 分钟让系统了解你 │ + │ 这样系统才能推荐最适合这次讨论的专家 │ + │ │ + │ 你的研究领域是? │ + │ [计算机科学] [生物医学] [物理] [社会科学] [其他...] │ + │ │ + └────────────────────────────────────────────────────┘ +``` + +**动线设计**: +``` +Q1: 研究领域(选择题,1 秒) + ↓ +Q2: 你目前面临的主要卡点是?(选择题,3 选项) + ↓ +Q3: 你更倾向于横向整合还是垂直深挖?(滑块,10 秒) + ↓ +系统生成:专家团草稿(3位专家,附推荐理由) + ↓ +用户确认或调整 → 直接开始圆桌 + ↓ +(后台:这3个问题的回答写入画像草稿,标注「来源:圆桌快速画像」) +``` + +**设计要点**: +- 3个问题覆盖了画像的「基础身份」和「认知风格」两个维度 +- 用户感知是「为了配置更好的专家」,而不是「在建档案」 +- 实际效果:用户不知不觉完成了画像的两个核心维度采集 + +--- + +#### 入口 C:信源收藏行为的自然沉淀(新功能,低开发成本) + +**设计背景**:用户在信源页收藏的论文和文章,天然反映了其研究兴趣。 + +**改进设计**: +``` +【Agent层逻辑】 +用户收藏了 5 篇以上论文/文章后: + → 系统分析收藏内容的关键词和领域分布 + → 自动推断「研究兴趣方向」维度 + → 写入画像草稿(标注「来源:收藏行为推断,置信度:中」) + +当用户进入画像页时: + → 「系统已根据你的收藏记录,推断了你的研究兴趣方向,是否查看?」 +``` + +**设计要点**: +- 用户无需任何额外操作 +- 是框架「前向预埋」的最典型实现:信源收藏行为数据为画像提供了上下文 + +--- + +### 模块二:画像呈现重构(修复断裂点:画像是死档案 + 结果不可理解) + +**当前问题**:画像呈现是静态的数字和维度,用户不理解 CSI=-5 意味着什么,也不知道画完后下一步做什么。 + +#### 2.1 画像主页重构:从「档案」到「行动中心」 + +**人层设计**: + +``` +画像主页结构: +┌────────────────────────────────────────────────────────────────┐ +│ 你好,[分身名称] [编辑] [发布到平台] │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ 【你是谁】(核心摘要,3句话,由AI生成) │ +│ 「你是一位以计算建模为主要范式的神经科学博士生, │ +│ 认知风格偏向横向整合(CSI=+8),学术动机以内在驱动为主(RAI=高), │ +│ 目前最大的卡点在于实验设计的统计方法选择。」 │ +│ │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ 【你在科学史上的坐标】 │ +│ CSI-RAI 散点图(你的点 + 30位科学家的位置) │ +│ 「你和费曼相似 82%,和图灵相似 74%」 │ +│ │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ 【你的默认专家团】(核心新功能) │ +│ 系统基于你的画像自动配置,3位专家,每位附推荐理由 │ +│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │ +│ │ 费曼风格 │ │ 香农风格 │ │ 领域专家 │ │ +│ │ 验证型 │ │ 挑战型 │ │ 动态推荐 │ │ +│ └──────────┘ └──────────┘ └──────────┘ │ +│ [一键发起圆桌] │ +│ │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ 【认知成长轨迹】(新功能,见模块五) │ +│ 最近30天:参与 N 次讨论,判断力趋势 ↑ │ +│ [查看完整轨迹] │ +│ │ +├────────────────────────────────────────────────────────────────┤ +│ │ +│ 【7维详细画像】(折叠,按需展开) │ +│ 基础身份 / 能力 / 需求 / 认知风格 / 动机 / 人格 / 综合解读 │ +│ │ +└────────────────────────────────────────────────────────────────┘ +``` + +**设计要点**: +- 「你是谁」用自然语言叙述,把 CSI=-5 翻译成「偏向横向整合」 +- 「你的默认专家团」是画像页最核心的**行动出口**,消除「看完画像不知道做什么」的断裂 +- 7维详细画像折叠,不让技术细节干扰主流程 + +--- + +#### 2.2 科学家坐标图:可交互的散点图(强化社交货币功能) + +**人层设计**: + +``` +【CSI-RAI 坐标系】 +纵轴:RAI(动机自主性,从外部→内在) +横轴:CSI(认知风格,从垂直深度→横向整合) + +四个象限: + 左上:自主专精型(怀尔斯、佩雷尔曼、麦克林托克) + 右上:自主整合型(费曼、达芬奇、冯·诺依曼) + 左下:策略专精型(香农、玻尔、狄拉克) + 右下:策略整合型(爱迪生、克里克) + +中心区:平衡型(爱因斯坦、达尔文、图灵) + +你的点:★(高亮,大一点) +相似的3位:○(高亮圆圈,连线到你的点) +其余27位:·(灰色小点,hover 可查看名字) + +点击某位科学家 → 展开「为什么和你相似/不同」的详细解读 +``` + +**可分享设计**: +``` +[分享我的科学家坐标] +→ 生成一张卡片: + 「我在认知风格上和费曼相似82%,和图灵相似74%。 + 我的CSI=+8(偏横向整合),RAI=高(内在驱动为主)。 + via 他山世界」 +``` + +**Agent层设计**: +- 新增接口:`GET /profile-helper/profile/{session_id}/scientists/famous` (tashan-world 已实现,迁移过来) +- 数据结构:scatter_data(30位科学家坐标)+ user_point + top3 匹配结果 + +--- + +### 模块三:分身驱动的圆桌配置(修复最大断裂点:专家配置门槛) + +**核心设计理念**:消除「我不知道选哪些专家」的认知摩擦。专家推荐不是随机的,而是由用户画像直接驱动的,且有清晰的推荐理由。 + +#### 3.1 智能专家推荐算法(Agent层) + +``` +输入:用户画像(CSI, RAI, 研究领域, 当前卡点, 方法范式) + +输出:3类专家推荐 + +【验证型专家(和你相似)】 + → 从科学家相似度结果取 Top1(认知风格相近) + → 作用:深化你的思路,从你的思维框架出发延伸 + → 展示理由:「和你一样偏横向整合,会帮你找到跨领域的类比」 + +【挑战型专家(和你互补)】 + → CSI 反向选取(你是+8,选 CSI 在-8附近的科学家) + → 作用:发现你的盲点,从完全不同的思维框架质疑 + → 展示理由:「偏垂直深度,会追问你的理论基础是否扎实」 + +【领域专家(和你研究相关)】 + → 调用 AMiner API,动态推荐当代活跃科学家 + → 输入:用户的一级/二级/交叉方向 + → 作用:提供领域最新进展的视角 + → 展示理由:「[机构]的[方向]研究者,2024年在你的问题域有重要发表」 +``` + +#### 3.2 人层——圆桌配置页重构 + +**现状**:用户面对一个空白配置页,需要手动选择专家、讨论方式等。 + +**新设计**: + +``` +【一键发起圆桌】(从画像页点击进入) + + ┌────────────────────────────────────────────────────────┐ + │ │ + │ 你想讨论什么? │ + │ ┌──────────────────────────────────────────────────┐ │ + │ │ (输入框,占主要空间) │ │ + │ │ 可以是:一个问题、一个假设、一篇论文链接 │ │ + │ └──────────────────────────────────────────────────┘ │ + │ │ + │ 系统为你推荐的专家团(基于你的分身): │ + │ │ + │ ┌────────────────┐ ┌────────────────┐ ┌────────────┐ │ + │ │ 费曼风格 │ │ 香农风格 │ │ Dr. 张伟 │ │ + │ │ 验证型 │ │ 挑战型 │ │ 领域专家 │ │ + │ │ 「和你认知相近」 │ │ 「帮你找漏洞」 │ │ 「MIT, 」 │ │ + │ └────────────────┘ └────────────────┘ └────────────┘ │ + │ [调整专家] [直接开始] │ + │ │ + └────────────────────────────────────────────────────────┘ +``` + +**关键设计决策**: +1. 输入框是首要焦点(用户先说想讨论什么) +2. 专家团已预填(来自画像),用户默认不需要改 +3. 「调整专家」是次级选项,不是主流程 +4. 「直接开始」是主按钮,降低从「想讨论」到「开始讨论」的摩擦到最低 + +--- + +#### 3.3 ✅ 修复 P0-02:CreateTopic 页的「分身上下文提示条」(新增) + +> **问题**:从画像页点击「发起圆桌」后跳转到 CreateTopic,页面是空白表单,缺乏上下文,专家团等待感消失。 + +**设计**:当 URL 带有 `?from=profile` 参数时,CreateTopic 页顶部固定显示一条浅色提示条: + +``` +┌──────────────────────────────────────────────────────────────────┐ +│ ✓ 你的数字分身专家团已就绪(费曼风格 · 香农风格 · 领域专家) │ +│ 描述你想讨论的问题,进入话题后可一键应用专家团。 × │ +└──────────────────────────────────────────────────────────────────┘ +``` + +**人层设计要点**: +- 提示条位于表单顶部,固定不滚动消失 +- 展示专家名字(从 localStorage 的 profilePanel 读取),让用户知道「有东西等着我」 +- 右侧 × 可关闭,关闭后不再提示(但 profilePanel 数据仍在,不影响后续应用) +- 颜色:浅绿底 + 黑字,不干扰表单,但清晰可见 + +**Agent层**:localStorage 中的 `profile_recommended_panel` 被 CreateTopic 页读取,仅用于提示条展示,不影响表单验证逻辑。 + +--- + +#### 3.4 ✅ 修复 P0-01:TopicConfigTabs 专家 Tab 的空状态引导(新增) + +> **问题**:用户从主页直接创建话题进入 TopicDetail 时,专家配置 Tab 是空的,用户不知道有数字分身可以驱动专家推荐,整个大闭环对新用户不可见。 + +**设计**:当专家 Tab 激活、且当前话题专家为空、且用户**没有**携带 `profilePanel`(即不是从画像页跳转来的),展示一个空状态引导区: + +``` +┌─────────────────────────────────────────────────────────┐ +│ │ +│ 还没有专家 │ +│ │ +│ 你可以: │ +│ │ +│ [建立数字分身,获得专属推荐] ← 主按钮,跳转 /profile-helper│ +│ [从专家库手动选择] ← 次级按钮 │ +│ [让 AI 根据话题自动生成] ← 次级按钮 │ +│ │ +│ ← 数字分身会根据你的认知风格推荐最匹配的专家组合 │ +│ │ +└─────────────────────────────────────────────────────────┘ +``` + +**人层设计要点**: +- 「建立数字分身,获得专属推荐」是**主按钮**(最醒目),明确告知价值 +- 点击后跳转 `/profile-helper`,用户回来后(画像完成),再次进入专家 Tab 时看到的是 DefaultExpertPanel 预填提示 +- 其余两个选项(手动选/AI生成)是次级选项,保留现有功能不退化 +- 这个空状态只在「从主页创建的话题、未曾配置过专家」时展示 + +**触发条件(精确,防止干扰已有用户)**: +``` +topicExpertNames.length === 0 // 当前话题没有任何专家 +AND !profileRecommendedPanel // 不是从画像页跳转来的 +AND activeTabId === 'experts' // 当前激活的是专家 Tab +``` + +--- + +### 模块四:讨论过程与结论层(修复断裂点:讨论缺结论层 + 贡献不可见) + +#### 4.1 讨论页重构:三个区域 + +**现状**:讨论内容全量展示,用户需要自己阅读和整理。 + +**新设计**:讨论页分三个区域: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 讨论进行中 │ +├───────────────────────────────┬─────────────────────────────┤ +│ │ │ +│ 【专家发言区】(主区域) │ 【你的判断区】(右侧栏) │ +│ │ │ +│ 轮次 1: │ 你的追问记录: │ +│ 费曼风格:「...」 │ · 「追问了香农风格关于...」 │ +│ 香农风格:「...」 │ │ +│ Dr.张伟:「...」 │ 你倾向于采信: │ +│ │ (圆桌进行中动态更新) │ +│ [追问某位专家] │ │ +│ [发表观点] │ 关键争议点: │ +│ [标记「这个很重要」] │ · 争议1:... │ +│ │ · 争议2:... │ +│ 轮次 2:... │ │ +│ │ │ +└───────────────────────────────┴─────────────────────────────┘ +│ 讨论已结束 │ +│ [查看讨论摘要] │ +└─────────────────────────────────────────────────────────────┘ +``` + +**设计要点**: +- 右侧栏实时记录用户的「判断行为」(追问了谁、标记了什么) +- 「标记「这个很重要」」——用户主动标记,就是在行使判断权(这个行为数据非常有价值) + +#### 4.2 讨论摘要页(新功能,核心) + +讨论结束后,展示**双层摘要**: + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 这次讨论的收获 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 【你的判断贡献】(最上方,最醒目) │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ · 你提出了这个追问:「[追问内容]」 │ │ +│ │ → 引导了费曼风格深入展开了方法论细节 │ │ +│ │ · 你标记了这个发言为「关键」 │ │ +│ │ · 基于你的行为,你在这次讨论中倾向于: │ │ +│ │ 质疑实验设计层面(而非理论前提层面) │ │ +│ │ [这和你画像里的认知风格一致 / 有所不同] │ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ 【专家讨论摘要】(AI贡献) │ +│ ┌─────────────────────────────────────────────────────┐ │ +│ │ 3个核心争议点: │ │ +│ │ 1. 关于方法论:[费曼风格] vs [香农风格] │ │ +│ │ 主流观点:... / 少数观点:... │ │ +│ │ 2. 关于数据质量:... │ │ +│ │ 3. 关于理论解释:... │ │ +│ │ │ │ +│ │ 系统判断:基于你的画像,你可能倾向于接受「方法论观点」 │ │ +│ │ [是的] [不是,我倾向于...] (用户输入,成为画像更新数据)│ │ +│ └─────────────────────────────────────────────────────┘ │ +│ │ +│ [这次讨论更新了你的分身] [分享这次讨论] [再开一轮] │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**关键设计决策**: +- 「你的判断贡献」在最上方,比「AI摘要」更醒目——这是框架第四层「成就感归属」的产品化 +- 最后那个问题「你倾向于接受哪个观点」,用户的回答直接成为画像更新的数据输入 +- 这是「讨论行为→画像进化」闭环的核心节点 + +--- + +### 模块五:讨论行为→画像反馈(核心数据飞轮) + +**这是整个产品最关键的新功能,也是建立数据壁垒的核心机制。** + +#### 5.1 捕获的判断行为数据 + +```python +# 每次圆桌讨论结束后写入的「判断行为记录」 +discussion_judgment_record = { + "discussion_id": "...", + "timestamp": "...", + + # 用户的追问行为 + "followup_questions": [ + { + "target_expert": "费曼风格", + "question": "...", + "triggered_direction": "方法论细节" + } + ], + + # 用户的标记行为(最有价值的数据) + "marked_as_important": [ + { + "speaker": "香农风格", + "content_summary": "关于统计方法的选择", + "content_type": "方法论批评" + } + ], + + # 用户的最终判断倾向(显式输入) + "final_judgment_tendency": { + "leaning_toward": "方法论质疑", + "user_stated": "是的,我倾向于接受方法论的观点" + }, + + # 自动推断的认知偏好 + "inferred_cognitive_patterns": { + "questioning_style": "方法论层面", # vs 理论前提 + "confidence": "高" + } +} +``` + +#### 5.2 画像更新逻辑 + +``` +判断行为数据 → 画像更新规则: + +追问了哪位专家(验证型/挑战型): + → 更新「认知合作偏好」(倾向于寻求验证还是挑战) + +标记了什么类型的发言(方法论/理论/实验/数据): + → 更新「关注焦点维度」 + +最终采信倾向: + → 更新「论据偏好」(偏实证/偏理论/偏方法论) + +累积追踪: + → 随时间变化,某个偏好是否在强化或转变 + → 这种变化本身就是「认知成长」的量化证据 +``` + +--- + +### 模块六:认知成长仪表盘(修复断裂点:没有成长轨迹) + +**框架对应**:第五层「认知跃迁的记录与可见化」——意义感的产品化来源。 + +#### 6.1 仪表盘主视图 + +``` +┌─────────────────────────────────────────────────────────────┐ +│ 你的认知成长轨迹 │ +│ 过去 30 天 │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 参与圆桌:12次 | 提出追问:47个 | 标记关键:83处 │ +│ │ +│ 【你的判断力画像演化】 │ +│ │ +│ 本月 vs 上月: │ +│ · 你更多地质疑「方法论层面」(从 30% → 52%) │ +│ 这是认知深化的信号:你在从「是否有效」走向「为何有效」 │ +│ │ +│ · 你和费曼的认知相似度:78% → 83%(↑5%) │ +│ 主要体现在:你最近对「跨学科类比」的偏好增加 │ +│ │ +│ · 你在过去3次讨论里,连续追问了「实验设计细节」 │ +│ 系统推测:这可能是你目前的核心卡点 │ +│ 「你的画像里记录的是『统计方法选择困难』,是否一致?」 │ +│ [是的] [不完全是,更准确地说是...] │ +│ │ +│ 【你的讨论历史】 │ +│ 时间轴 · 话题 · 每次的判断倾向摘要(可折叠展开) │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +**设计要点**: +- 所有数据都用「你的语言」叙述,不是数字报告 +- 系统的「推测」引发用户确认或纠正,这个互动本身又产生新的数据 +- 这是「意义感制造机器」:用户看到的不是「我做了几次讨论」,而是「我的认知方式在如何演化」 + +--- + +## 四、数据模型设计(前向预埋) + +按照框架「前向预埋」原则,现在的数据结构要为未来的功能预留空间。 + +### 核心数据扩展 + +#### 画像(Profile)新增字段 + +```json +{ + "profile": { + // 现有字段(保持不变) + "identity": {...}, + "capability": {...}, + "needs": {...}, + "cognitive_style": {"csi": -8, "rai": 0.7, ...}, + "motivation": {...}, + "personality": {...}, + "interpretation": {...}, + + // 新增字段(前向预埋) + "scientist_matches": { + "top3": [ + {"name": "费曼", "similarity": 82, "reason": "..."}, + {"name": "图灵", "similarity": 74, "reason": "..."}, + {"name": "钱学森", "similarity": 68, "reason": "..."} + ], + "field_recommendations": [ + {"name": "...", "institution": "...", "reason": "..."} + ], + "computed_at": "2026-03-16T10:00:00Z" + }, + + // 默认专家团(由画像自动生成) + "default_expert_panel": { + "verification": {"expert_name": "费曼风格", "reason": "..."}, + "challenger": {"expert_name": "香农风格", "reason": "..."}, + "domain": {"expert_name": "动态", "source": "aminer"}, + "generated_at": "2026-03-16T10:00:00Z" + }, + + // 判断行为积累(新增,数据飞轮核心) + "judgment_history": [ + { + "discussion_id": "...", + "date": "...", + "questioning_style": "方法论层面", + "attention_focus": "实验设计", + "judgment_tendency": "偏实证", + "expert_affinity": "验证型" + } + ], + + // 认知演化摘要(由 judgment_history 聚合生成) + "cognitive_evolution": { + "questioning_style_trend": "方法论层面趋势增强", + "scientist_similarity_trend": {"费曼": [78, 80, 83]}, + "growth_signals": ["从是否有效走向为何有效"], + "last_updated": "2026-03-16" + }, + + // 收藏行为推断(入口C预埋) + "inferred_from_behavior": { + "research_interests": ["计算神经科学", "统计方法"], + "source": "收藏行为推断", + "confidence": "中", + "article_count": 12 + } + } +} +``` + +#### 讨论(Discussion)新增字段 + +```json +{ + "discussion": { + // 现有字段 + "topic_id": "...", + "status": "completed", + "turns": [...], + + // 新增字段 + "expert_panel_config": { + "generated_from_profile": true, + "profile_version": "2026-03-16", + "experts": [ + {"name": "费曼风格", "role": "verification", "reason": "..."}, + {"name": "香农风格", "role": "challenger", "reason": "..."} + ] + }, + + // 用户判断行为记录(数据飞轮输入) + "user_judgment_record": { + "followup_questions": [...], + "marked_as_important": [...], + "final_judgment_tendency": {...}, + "inferred_cognitive_patterns": {...} + }, + + // 摘要(讨论结束后生成) + "summary": { + "key_disputes": [...], + "user_contribution_highlight": "...", + "profile_update_suggestion": {...} + } + } +} +``` + +--- + +## 五、Human-Readable + Agent-Operable 双层设计对照 + +| 功能 | 人层(用户看到的)| Agent层(OpenClaw 能操作的)| +|------|-----------------|--------------------------| +| 画像呈现 | 「你和费曼相似82%」的叙事卡片 | `GET /profile/{id}/scientists/famous` 返回结构化相似度数据 | +| 专家推荐 | 3张专家卡片 + 推荐理由 | `GET /profile/{id}/recommended-panel` 返回专家配置JSON | +| 一键发起圆桌 | 输入框 + 预填专家团 | `POST /topics/{id}/discussion` 支持 `panel_from_profile=true` | +| 讨论摘要 | 双层叙事摘要(贡献+AI摘要)| `GET /topics/{id}/discussion/judgment-summary` 返回结构化判断数据 | +| 画像更新 | 「你的分身成长了」提示 | `POST /profile/{id}/update-from-discussion` 接受判断行为数据 | +| 成长仪表盘 | 叙事化的认知演化故事 | `GET /profile/{id}/cognitive-evolution` 返回演化时序数据 | + +--- + +## 六、优先级排序与 MVP 范围 + +按「断裂点严重程度」+ 「实现成本」排序: + +### P0:立即做(修复最大断裂点,低开发成本) + +| 功能 | 修复的断裂点 | 预估工作量 | +|------|------------|----------| +| 画像页新增「默认专家团」模块 | 画像→圆桌没有桥 | 前端2天 + 后端1天 | +| 圆桌配置页「专家团预填」 | 专家配置门槛 | 前端1天 + 后端API 0.5天 | +| 科学家匹配 API 迁移(tashan-world已实现)| 画像结果不可理解 | 后端2天 | +| 画像叙事化摘要(「你是谁」3句话)| 画像结果不可理解 | prompt工程1天 | + +### P1:下一迭代(核心数据飞轮) + +| 功能 | 修复的断裂点 | 预估工作量 | +|------|------------|----------| +| 讨论摘要页:双层结构(贡献+AI摘要)| 讨论缺结论层 + 贡献不可见 | 前端3天 + 后端2天 | +| 判断行为捕获(追问记录、标记功能)| 讨论与画像断开 | 前端2天 + 后端1天 | +| 讨论行为→画像更新逻辑 | 画像是死档案 | 后端3天 | +| 入口B:发起圆桌时的快速画像(3问)| 画像入口太重 | 前端2天 + 后端0.5天 | + +### P2:后续迭代(成长感知与意义层) + +| 功能 | 修复的断裂点 | 预估工作量 | +|------|------------|----------| +| 认知成长仪表盘 | 没有认知成长轨迹 | 前端5天 + 后端3天 | +| 信源收藏行为→画像推断 | 画像入口太重 | 后端2天 | +| 科学家坐标图可交互版 | 画像结果不可理解(升级)| 前端3天 | +| 分享卡片生成(科学家坐标)| 社交货币 | 前端1天 | + +--- + +## 七、成功指标 + +按框架「数据分析师」角色的原则,指标必须围绕「有多少用户完整走完了主干闭环」: + +### 核心指标(主干闭环完成率) + +``` +主干闭环完整走通 = + 建立画像(完整度≥70%) + + 使用「默认专家团」发起至少1次圆桌 + + 查看了讨论摘要的「你的贡献」模块 + +目标:前3个月内,日活用户中主干闭环完整率 ≥ 30% +(现在估计接近 0%,因为画像→圆桌没有桥) +``` + +### 辅助指标(识别断裂点位置) + +| 指标 | 用途 | +|------|------| +| 画像完整度分布 | 识别哪个维度最难采集 | +| 画像页→圆桌页的转化率 | 验证「默认专家团」是否有效降低配置门槛 | +| 圆桌里「追问」和「标记」的使用率 | 验证判断行为数据是否真的被用户产生 | +| 讨论摘要页的「你的贡献」模块停留时长 | 验证贡献可见化是否产生情感共鸣 | +| 分身「更新次数」 | 验证数据飞轮是否在转动 | + +### 数据飞轮健康指标(研发壁垒层) + +``` +飞轮健康 = + 判断行为记录数 / 总讨论次数 + (目标:每次讨论平均产生 ≥ 5条判断行为记录) + +当判断行为记录积累到一定量时: + 启动「判断偏好」模型训练 + → 专家推荐的准确度验证 +``` + +--- + +## 八、下一步行动 + +1. **本周**:产品与设计评审本文档,确认 P0 范围 +2. **本周**:tashan-world 科学家匹配 API 迁移到 TopicLab(代码已存在,主要是接口适配) +3. **下周**:P0 功能开发启动(画像页「默认专家团」模块 + 圆桌配置「专家预填」) +4. **同步**:宣发团队可以基于「科学家坐标图」做第一批传播素材(不依赖新功能,现有 tashan-world 代码已有散点图数据) + +--- + +*本文档是一个活文档,随产品验证数据更新。* diff --git "a/docs/\345\267\245\347\250\213\345\256\236\346\226\275\350\256\241\345\210\222_\346\225\260\345\255\227\345\210\206\350\272\253\344\270\216\345\234\206\346\241\214\345\244\247\351\227\255\347\216\257_v1.md" "b/docs/\345\267\245\347\250\213\345\256\236\346\226\275\350\256\241\345\210\222_\346\225\260\345\255\227\345\210\206\350\272\253\344\270\216\345\234\206\346\241\214\345\244\247\351\227\255\347\216\257_v1.md" new file mode 100644 index 0000000..75e9dd0 --- /dev/null +++ "b/docs/\345\267\245\347\250\213\345\256\236\346\226\275\350\256\241\345\210\222_\346\225\260\345\255\227\345\210\206\350\272\253\344\270\216\345\234\206\346\241\214\345\244\247\351\227\255\347\216\257_v1.md" @@ -0,0 +1,1221 @@ +# 工程实施计划:数字分身 × 圆桌讨论大闭环 + +> **文档性质**:研发实施指南,对照产品设计文档逐文件落地 +> **日期**:2026-03-16 +> **前提**:已完整阅读现有代码。本文档精确到每个需要改动/新建的文件、改动位置和改动内容。 +> **原则**:最小侵入,不破坏现有功能,逐 PR 可验证。 + +--- + +## 一、现有代码摸底结论 + +> **更新说明**:本节已在完整阅读源码后修正,以下结论以实际代码为准。 + +### 已有能力(可直接复用,不重复开发) + +| 现有实现 | 文件位置 | 关键细节 | +|---------|---------|---------| +| 科学家相似度匹配(CSI×RAI×Big5)| `backend/app/services/profile_helper/scientist_match.py` | 函数:`match_famous_scientists(parsed)`,权重 CSI=0.4/RAI=0.4/PER=0.2 | +| 科学家散点图前端 | `frontend/src/modules/profile-helper/components/ScientistScatter.tsx` | SVG 500×400,CSI范围±24,RAI范围-10~62 | +| 科学家卡片组件 | `frontend/src/modules/profile-helper/components/ScientistCard.tsx` | Props: `scientist: FamousMatch, rank: number`,CSS类前缀 `sci-card-*` | +| 科学家匹配区块(已集成在 ProfilePanel 内)| `frontend/src/modules/profile-helper/components/ScientistMatchSection.tsx` | **重要**:ScientistMatchSection 在 ProfilePanel.tsx 第87行内部渲染,不在 ProfilePage 里 | +| 领域科学家推荐(LLM动态)| `backend/app/services/profile_helper/scientist_match.py` | 函数:`recommend_field_scientists(parsed: dict) -> list` | +| 科学家匹配 API 端点 | `backend/app/api/profile_helper.py` | `GET /scientists/famous`、`GET /scientists/field`,使用 `get_current_auth_context` 认证 | +| 结构化画像 API | `backend/app/api/profile_helper.py` | `GET /profile/{session_id}/structured`;前端 `profileHelperApi.ts` 有 `getStructuredProfile()` | +| ProfilePanel(含 ScientistMatchSection)| `frontend/src/modules/profile-helper/ProfilePanel.tsx` | `hasProfile` 守卫(`!!profile && !profile.includes('[姓名/标识]')`);ScientistMatchSection 仅在 `hasProfile=true` 时渲染 | +| ProfilePage(渲染结构化组件 + ProfilePanel)| `frontend/src/modules/profile-helper/pages/ProfilePage.tsx` | state 包含 `structured: StructuredProfile | null`;调用 `getStructuredProfile`;渲染顺序:结构化组件 → ProfilePanel → twin-record-banner → twin-publish-card → twin-history-card | +| TopicDetail 的 location.state | `frontend/src/pages/TopicDetail.tsx` 第76行 | 已有:`(location.state as { skillList?: string[] } | null)?.skillList`;新增 profilePanel 追加到同一类型即可 | +| 话题专家 CRUD | `frontend/src/components/ExpertManagement.tsx` | 添加预设专家:`topicExpertsApi.add(topicId, { source: 'preset', preset_name })`;添加自定义:`{ source: 'custom', name, label, description, content? }` | +| 话题配置 Tab(专家/模式/技能/MCP/模型)| `frontend/src/components/TopicConfigTabs.tsx` | Props 含 `topicExpertNames?: string[]`;专家 Tab 渲染 `` | +| 数字分身发布与历史 | `ProfilePage.tsx` | `handlePublish()`、`refreshTwinRecords()`,CSS类 `twin-publish-card`、`twin-history-card` | +| Block 协议 SSE | `profileHelperApi.ts:sendMessageBlocks` | 端点 `/api/profile-helper/chat/blocks` | + +### 关键 CSS 命名规范(新增代码必须遵守) + +| 功能区 | CSS 前缀 | 文件 | +|-------|---------|------| +| 科学家匹配 | `sci-*` | `profile-helper.css` | +| 画像可视化 | `pv-*` | `profile-helper.css` | +| 分身发布/历史 | `twin-*` | `profile-helper.css` | +| Block 协议组件 | `block-*` | `profile-helper.css` | +| **新增:默认专家团** | **`dpanel-*`** | 追加到 `profile-helper.css` | +| **新增:讨论摘要弹窗** | **`judgment-*`** | 追加到 `profile-helper.css` 或新建 `judgment.css` | + +### 关键数据库表(现有,不重复建) + +已有表(`topiclab-backend`):`topics`、`posts`、`discussion_turns`、`topic_experts`、`digital_twins`、`openclaw_api_keys`... +**需新建表**:`discussion_judgments`(P1,见第三章) + +### 缺口(需要新建或改造) + +| 缺口 | 优先级 | 涉及端 | +|------|--------|--------| +| ✅ 关卡A修复:TopicConfigTabs 专家Tab空状态引导(「建立数字分身」入口)| P0 | 前端 | +| ✅ 关卡A修复:CreateTopic 页 `from=profile` 提示条 | P0 | 前端 | +| 画像页「默认专家团」模块(DefaultExpertPanel 组件)| P0 | 前端 + 后端API | +| 画像页「你是谁」叙事摘要 API(CSI/RAI → 自然语言)| P0 | 后端 | +| 圆桌配置「专家团预填」提示 | P0 | 前端 + 后端API | +| 「发起圆桌」的快速画像入口(3问)| P1 | 前端 + 后端API | +| 讨论摘要双层结构(贡献可见化 + AI摘要)| P1 | 前端 + 后端 | +| 判断行为捕获(追问记录、标记功能)| P1 | 前端 + 后端 | +| 讨论行为→画像更新逻辑 | P1 | 后端 | +| 认知成长仪表盘 | P2 | 前端 + 后端 | + +--- + +## ⚠️ 原计划错误修正表(代码摸底后校正) + +> 以下是原计划描述有误、已根据实际代码修正的内容,开发时以本节为准。 + +| # | 原计划描述 | 实际代码情况 | 修正后做法 | +|---|-----------|------------|----------| +| 1 | DefaultExpertPanel 插入 ProfilePage.tsx 的「ProfilePanel 和 twin-publish-card 之间」| ProfilePage 渲染顺序:结构化组件群 → `` → banner → publish → history | **插入点不变**:仍在 `` 和 `twin-record-banner` 之间,但 ScientistMatchSection 在 ProfilePanel 内部已有,DefaultExpertPanel 要放在 ProfilePanel **之后** | +| 2 | 专家应用 API:`topicExpertsApi.add(topicId, preset_name: '费曼风格')`| add() 的 source='preset' 需要专家库里存在的名字;「费曼风格」不在预设库 | 使用 `source: 'custom'`,参数:`{ name: '费曼风格', label: '验证型专家', description: '推荐理由' }`,不传 content(使用空 content 或从 recommended-panel API 携带 expert_name 后映射到真实专家库名) | +| 3 | TopicDetail 新增 `location.state` 读取 profilePanel | 第76行已有 `{ skillList?: string[] }` 的 state 读取 | 追加类型:`(location.state as { skillList?: string[]; profilePanel?: RecommendedPanel } \| null)` | +| 4 | judgment API 在 topiclab-backend 只注册一次 | topiclab-backend 所有路由均注册双前缀(无前缀 + `/api/v1`) | 需在 `main.py` 追加两行:`app.include_router(judgment_router.router, tags=["judgment"])` 和 `app.include_router(judgment_router.router, prefix="/api/v1", tags=["judgment-v1"])` | +| 5 | narrative-summary 新建独立文件 | 现有 `profile_helper.py` 文件管理所有画像相关路由 | 追加到现有 `backend/app/api/profile_helper.py` 末尾,不新建文件 | +| 6 | ProfilePage 只有 profile + forumProfile 两个数据 | ProfilePage 还有 `structured: StructuredProfile` state,调用 `getStructuredProfile()` | DefaultExpertPanel 需要的 CSI/RAI 数据通过 `/recommended-panel` API 返回(后端自己读画像),无需前端额外传 structured 数据 | + +--- + +## 二、P0 实施计划(立即可做,约 5 个工作日) + +### P0-1:后端新增「推荐专家团」API + +**目标**:基于用户画像,生成3类专家配置(验证型 + 挑战型 + 领域型) + +#### 文件:`backend/app/api/profile_helper.py` + +在现有 `/scientists/famous` 和 `/scientists/field` 端点之后,新增: + +```python +@router.get("/profile-helper/profile/{session_id}/recommended-panel") +async def get_recommended_expert_panel(session_id: str): + """ + 基于用户画像,生成推荐专家团配置。 + + 返回3类专家: + - verification: 和用户认知风格相近(从 famous top1 取) + - challenger: 和用户认知风格互补(CSI 反向) + - domain: 领域相关当代科学家(从 field recommendations 取 top1) + + 返回结构: + { + "verification": {"expert_name": "...", "scientist_name": "...", "reason": "..."}, + "challenger": {"expert_name": "...", "scientist_name": "...", "reason": "..."}, + "domain": {"expert_name": "...", "name": "...", "institution": "...", "reason": "..."} + } + """ + profile = read_profile(session_id) + if not profile: + raise HTTPException(status_code=404, detail="Profile not found") + + parsed = parse_profile(profile) + csi = parsed.get("cognitive_style", {}).get("csi", 0) + rai = parsed.get("cognitive_style", {}).get("rai", 0.5) + + # 验证型:取 famous top1 + famous = match_famous_scientists(parsed) + verification_scientist = famous["top3"][0] if famous["top3"] else None + + # 挑战型:CSI 反向选取 + # 正值→选负值区科学家,负值→选正值区科学家 + challenger_scientist = _pick_challenger_scientist(famous["scatter_data"], csi) + + # 领域型:取 field recommendations top1 + field_recs = recommend_field_scientists(parsed) + domain_expert = field_recs[0] if field_recs else None + + return { + "verification": { + "expert_name": f"{verification_scientist['name']}风格" if verification_scientist else None, + "scientist_name": verification_scientist["name"] if verification_scientist else None, + "similarity": verification_scientist["similarity"] if verification_scientist else None, + "reason": f"和你认知风格相似度{verification_scientist['similarity']}%,会从你熟悉的思维框架出发深化思路" if verification_scientist else None, + }, + "challenger": { + "expert_name": f"{challenger_scientist['name']}风格" if challenger_scientist else None, + "scientist_name": challenger_scientist["name"] if challenger_scientist else None, + "reason": "认知风格与你互补,会从你习惯视角的盲点处发起质疑" if challenger_scientist else None, + }, + "domain": { + "expert_name": domain_expert["name"] if domain_expert else None, + "institution": domain_expert.get("institution", "") if domain_expert else None, + "field": domain_expert.get("field", "") if domain_expert else None, + "reason": domain_expert["reason"] if domain_expert else None, + }, + "profile_csi": csi, + "profile_rai": rai, + } +``` + +辅助函数 `_pick_challenger_scientist`,加在同文件或 `scientist_match.py` 里: + +```python +def _pick_challenger_scientist(scatter_data: list, user_csi: float) -> dict | None: + """从散点数据中选取CSI与用户相反的科学家作为挑战型专家""" + if not scatter_data: + return None + # 用户CSI>0 → 选CSI最负的(垂直深度型) + # 用户CSI<0 → 选CSI最正的(横向整合型) + if user_csi >= 0: + candidates = sorted(scatter_data, key=lambda s: s["csi"]) + else: + candidates = sorted(scatter_data, key=lambda s: s["csi"], reverse=True) + return candidates[0] if candidates else None +``` + +--- + +#### 文件:`backend/app/api/profile_helper.py` + +在「生成画像叙事摘要」方向,新增端点: + +```python +@router.get("/profile-helper/profile/{session_id}/narrative-summary") +async def get_narrative_summary(session_id: str): + """ + 将CSI/RAI等量化指标翻译为自然语言叙事摘要(3句话)。 + + 返回: + { + "summary": "你是一位以计算建模为主要范式的神经科学博士生...", + "csi_label": "横向整合型", + "rai_label": "内在驱动为主", + "key_strength": "跨领域连接能力", + "key_challenge": "实验设计的统计方法选择" + } + """ + profile = read_profile(session_id) + if not profile: + raise HTTPException(status_code=404, detail="Profile not found") + parsed = parse_profile(profile) + return _generate_narrative_summary_sync(parsed) + + +def _generate_narrative_summary_sync(parsed: dict) -> dict: + """ + ✅ 三方校对修复(问题#13):原代码调用不存在的 _generate_narrative_summary()。 + 改为同步模板生成(无 LLM 调用成本),直接从结构化 parsed 数据中提取关键词拼接。 + + 规则(不依赖LLM,零成本,零延迟): + - 研究阶段 + 一级领域 + 方法范式 → 第1句 + - CSI数值 → csi_label(>4:横向整合型 / <-4:垂直深度型 / else:均衡型) + - RAI数值 → rai_label(>6:内在驱动为主 / <0:外在驱动为主 / else:混合动机) + - needs.core_blockers 第一条 → key_challenge + """ + identity = parsed.get("identity", {}) + cog = parsed.get("cognitive_style", {}) + needs = parsed.get("needs", {}) + + stage = identity.get("research_stage", "研究者") + field = identity.get("primary_field", "所在领域") + method = identity.get("method_paradigm", "") + csi = cog.get("csi", 0) or 0 + rai = cog.get("rai", 5) or 5 + + # CSI 标签 + if csi > 4: + csi_label = "横向整合型" + elif csi < -4: + csi_label = "垂直深度型" + else: + csi_label = "均衡型" + + # RAI 标签 + if rai > 6: + rai_label = "内在驱动为主" + elif rai < 0: + rai_label = "外在驱动为主" + else: + rai_label = "内外动机均衡" + + method_str = f"以{method}为主要范式的" if method else "" + summary = ( + f"你是一位{method_str}{field}{stage}," + f"认知风格偏向{csi_label}(CSI={csi:+.0f})," + f"学术动机{rai_label}(RAI={rai:.1f})。" + ) + + # key_challenge:从 needs.core_blockers 取第一条 + blockers = needs.get("core_blockers", []) + key_challenge = blockers[0] if blockers else "有待进一步明确" + + # key_strength:从 interpretation.core_drive 取(如有) + interp = parsed.get("interpretation", {}) + key_strength = interp.get("core_drive", csi_label + "思维") + + return { + "summary": summary, + "csi_label": csi_label, + "rai_label": rai_label, + "key_strength": key_strength, + "key_challenge": key_challenge, + } +``` + +--- + +### P0-2:画像页新增「叙事摘要 + 默认专家团」模块 + +> **三方校对修正(问题#4和#9)**: +> 1. 产品设计要求「你是谁」叙事摘要是画像页**第一个**模块,在科学家坐标图之前。工程计划原版没有指定叙事摘要的渲染位置,现补充。 +> 2. 渲染顺序应为:**叙事摘要** → ProfilePanel(内含科学家坐标) → **DefaultExpertPanel** → twin-record-banner → twin-publish-card。 + +#### 文件:`frontend/src/modules/profile-helper/pages/ProfilePage.tsx` + +**完整改动描述**(新增两个 useEffect + JSX 两处插入): + +```tsx +// ── 新增 import ─────────────────────────────────────────────────── +import { getRecommendedPanel, getNarrativeSummary } from '../profileHelperApi' +import { DefaultExpertPanel } from '../components/DefaultExpertPanel' +import type { RecommendedPanel } from '../types' + +// ── 新增 state(在组件顶部现有 state 之后)──────────────────────── +const [narrativeSummary, setNarrativeSummary] = useState<{ + summary: string; csi_label: string; rai_label: string; + key_strength: string; key_challenge: string; +} | null>(null) +const [recommendedPanel, setRecommendedPanel] = useState(null) +const [panelLoading, setPanelLoading] = useState(false) + +// ── 新增独立 useEffect(不合并到现有初始化 useEffect)─────────────── +useEffect(() => { + if (!sessionId) return + // 并发请求叙事摘要和推荐专家团,互不阻塞 + getNarrativeSummary(sessionId) + .then(setNarrativeSummary) + .catch(() => {}) // 失败时静默,不影响其他模块渲染 + + setPanelLoading(true) + getRecommendedPanel(sessionId) + .then(setRecommendedPanel) + .catch(() => {}) // 失败时静默,不展示 DefaultExpertPanel + .finally(() => setPanelLoading(false)) +}, [sessionId]) + +// ── JSX 改动1:在现有结构化组件群(ProfileHeader等)最上方插入叙事摘要 ─ +// 位置:结构化组件 ProfileHeader 之前(画像页第一个模块) +{narrativeSummary && ( +
+

{narrativeSummary.summary}

+
+ {narrativeSummary.csi_label} + {narrativeSummary.rai_label} +
+
+)} + +// ── JSX 改动2:在 之后、twin-record-banner 之前插入 ── +{panelLoading && ( +
正在生成你的专属专家团...
+)} +{!panelLoading && recommendedPanel && ( + +)} +// (紧接着是现有的 twin-record-banner div) +``` + +#### 新建文件:`frontend/src/modules/profile-helper/components/DefaultExpertPanel.tsx` + +> ✅ **三方校对修复**: +> 1. 删除 `sessionId` prop(组件不使用它,避免未使用变量警告) +> 2. 统一CSS类名为 `dpanel-*` 前缀(符合profile-helper.css命名规范) + +```tsx +/** + * 默认专家团模块:基于用户画像推荐的3类专家配置 + * 这是「画像→圆桌」的核心桥梁组件 + */ +import { useNavigate } from 'react-router-dom' +import type { RecommendedPanel } from '../types' + +interface DefaultExpertPanelProps { + panel: RecommendedPanel + // 注意:不包含 sessionId,组件只负责展示和导航 +} + +export function DefaultExpertPanel({ panel }: DefaultExpertPanelProps) { + const navigate = useNavigate() + + const handleStartDiscussion = () => { + // 将推荐的专家团存入 localStorage,圆桌配置页读取 + localStorage.setItem('profile_recommended_panel', JSON.stringify(panel)) + navigate('/topics/new?from=profile') + } + + return ( +
+
+

你的专属专家团

+

+ 基于你的认知风格和研究方向,系统为你配置了最适合的讨论伙伴 +

+
+ +
+ {/* 验证型专家 */} + {panel.verification?.expert_name && ( +
+
验证型
+
{panel.verification.scientist_name}
+
思维风格专家
+ {panel.verification.similarity != null && ( +
+ 与你相似度 {panel.verification.similarity}% +
+ )} +

{panel.verification.reason}

+
+ )} + + {/* 挑战型专家 */} + {panel.challenger?.expert_name && ( +
+
挑战型
+
{panel.challenger.scientist_name}
+
思维风格专家
+

{panel.challenger.reason}

+
+ )} + + {/* 领域型专家 */} + {panel.domain?.expert_name && ( +
+
领域型
+
{panel.domain.expert_name}
+
+ {panel.domain.institution ? `${panel.domain.institution} · ` : ''} + {panel.domain.field} +
+

{panel.domain.reason}

+
+ )} +
+ +
+ +

进入话题页后可以调整专家配置

+
+
+ ) +} +``` + +#### 文件:`frontend/src/modules/profile-helper/types.ts` + +新增类型定义(追加到文件末尾): + +```typescript +// ── 推荐专家团类型 ──────────────────────────────────────────────── + +export interface RecommendedExpert { + expert_name: string | null + scientist_name?: string | null + similarity?: number | null + institution?: string | null + field?: string | null + reason: string | null +} + +export interface RecommendedPanel { + verification: RecommendedExpert + challenger: RecommendedExpert + domain: RecommendedExpert + profile_csi: number + profile_rai: number +} +``` + +#### 文件:`frontend/src/modules/profile-helper/profileHelperApi.ts` + +新增两个 API 函数(追加到文件末尾): + +```typescript +// ── 推荐专家团 API ────────────────────────────────────────────── + +export async function getRecommendedPanel(sessionId: string): Promise { + const res = await fetch( + `${API_BASE}/profile-helper/profile/${sessionId}/recommended-panel`, + { headers: getAuthFetchHeaders() } + ) + if (!res.ok) throw new Error(`获取推荐专家团失败: ${res.status}`) + return res.json() +} + +// ── 叙事摘要 API ───────────────────────────────────────────────── + +export async function getNarrativeSummary(sessionId: string): Promise<{ + summary: string + csi_label: string + rai_label: string + key_strength: string + key_challenge: string +}> { + const res = await fetch( + `${API_BASE}/profile-helper/profile/${sessionId}/narrative-summary`, + { headers: getAuthFetchHeaders() } + ) + if (!res.ok) throw new Error(`获取摘要失败: ${res.status}`) + return res.json() +} +``` + +--- + +### P0-3:圆桌配置「专家团预填」+ 关卡A修复:CreateTopic 提示条 + TopicConfigTabs 空状态引导 + +> ⚠️ **代码摸底修正(TopicDetail location.state)**:TopicDetail.tsx 第76行已有: +> `const initialSkillIds = (location.state as { skillList?: string[] } | null)?.skillList` +> 新增 profilePanel 时,必须**扩展**已有的类型声明,不能替换,否则会破坏 skillList 的读取。 + +#### 文件:`frontend/src/pages/CreateTopic.tsx` + +**现有代码结构**(摸底确认):组件 state 只有 `form: { title, body, category }` 和 `loading`,`handleSubmit` 调用 `topicsApi.create(form)` 后 `navigate('/topics/${res.data.id}')`。 + +**改动内容**(追加,不替换现有代码): + +```tsx +// 1. 新增 import +import { useLocation } from 'react-router-dom' +import type { RecommendedPanel } from '../modules/profile-helper/types' + +// 2. 在组件顶部新增 state(现有 form/loading 之后) +const location = useLocation() +const [profilePanel, setProfilePanel] = useState(null) + +// 3. 新增 useEffect(在现有 useEffect 之后,如有) +useEffect(() => { + const fromProfile = new URLSearchParams(location.search).get('from') === 'profile' + if (!fromProfile) return + const stored = localStorage.getItem('profile_recommended_panel') + if (!stored) return + try { + setProfilePanel(JSON.parse(stored) as RecommendedPanel) + // 不立即清除 localStorage,等话题创建成功后再清除 + } catch { /* ignore */ } +}, []) + +// 4. 在 JSX 的
标签开头(title input 之前),新增提示条: +{profilePanel && ( +
+ ✓ 你的数字分身专家团已就绪 + + {[ + profilePanel.verification?.scientist_name, + profilePanel.challenger?.scientist_name, + profilePanel.domain?.expert_name, + ].filter(Boolean).join(' · ')} + + 描述你想讨论的问题,进入话题后可一键应用专家团 +
+)} + +// 5. 改动 handleSubmit 中的 navigate 调用(原:navigate(`/topics/${res.data.id}`)): +navigate(`/topics/${res.data.id}`, { + state: { profilePanel }, // 传递 profilePanel 给 TopicDetail +}) +// 创建成功后清除 localStorage +if (profilePanel) { + localStorage.removeItem('profile_recommended_panel') +} +``` + +#### 文件:`frontend/src/pages/TopicDetail.tsx` + +**现有代码**(摸底确认,第76行): +```tsx +const initialSkillIds = (location.state as { skillList?: string[] } | null)?.skillList +``` + +**改动**:扩展类型,追加 profilePanel 读取: +```tsx +// 修改第76行(扩展类型,保留 skillList): +const locationState = location.state as { skillList?: string[]; profilePanel?: RecommendedPanel } | null +const initialSkillIds = locationState?.skillList +const profilePanel = locationState?.profilePanel ?? null // 新增 + +// 在 处新增 prop 传递: + +``` + +#### 文件:`frontend/src/components/TopicConfigTabs.tsx` + +**改动1**:在 `TopicConfigTabsProps` interface 末尾追加新 prop: +```tsx +/** 从数字分身画像页携带的推荐专家团配置(可选) */ +profileRecommendedPanel?: RecommendedPanel | null +``` + +**改动2**:在组件函数参数解构中追加(保持现有参数不变): +```tsx +export default function TopicConfigTabs({ + // ... 现有所有参数 ... + profileRecommendedPanel = null, // 新增,默认 null +}: TopicConfigTabsProps) { +``` + +**改动3**:新增 state 变量(在现有 18 个 state 之后追加): +```tsx +const [profilePanelDismissed, setProfilePanelDismissed] = useState(false) +``` + +**改动4**:在 `experts` Tab 的渲染内容里,在 `` 组件**之前**插入预填提示卡片。需要找到 `activeTabId === 'experts'` 对应的 TabPanel,在 `` 之前插入: + +```tsx +{/* 预填提示卡片:有推荐数据、未被关闭、当前专家为空 时展示 */} +{profileRecommendedPanel && !profilePanelDismissed && topicExpertNames.length === 0 && ( +
+
+ 🎯 已根据你的数字分身推荐了专家团 + +
+
+ {profileRecommendedPanel.verification?.scientist_name && ( + + {profileRecommendedPanel.verification.scientist_name}风格 · 验证型 + + )} + {profileRecommendedPanel.challenger?.scientist_name && ( + + {profileRecommendedPanel.challenger.scientist_name}风格 · 挑战型 + + )} + {profileRecommendedPanel.domain?.expert_name && ( + + {profileRecommendedPanel.domain.expert_name} · 领域 + + )} +
+ +
+)} + +{/* 空状态引导(关卡A P0-01 修复):没有推荐数据、当前专家为空 时展示 */} +{!profileRecommendedPanel && topicExpertNames.length === 0 && ( +
+

还没有专家

+
+ + 建立数字分身,获得专属推荐 + + {/* 以下两个按钮为现有功能,样式改为次级 */} +
+

数字分身会根据你的认知风格推荐最匹配的专家组合

+
+)} +``` + +**改动5**:新增 `handleApplyProfilePanel` 函数(在现有函数之后追加): + +> ⚠️ **代码摸底修正(专家 API 参数)**: +> - `topicExpertsApi.add(topicId, { source: 'preset', preset_name })` 用于添加专家库中已有的预设专家 +> - 推荐的「费曼风格」等不在预设库,必须用 `source: 'custom'`,带上 name/label/description + +```tsx +const handleApplyProfilePanel = async (panel: RecommendedPanel) => { + const expertsToAdd = [ + panel.verification?.expert_name ? { + source: 'custom' as const, + name: panel.verification.expert_name, + label: '验证型专家', + description: panel.verification.reason || '', + } : null, + panel.challenger?.expert_name ? { + source: 'custom' as const, + name: panel.challenger.expert_name, + label: '挑战型专家', + description: panel.challenger.reason || '', + } : null, + panel.domain?.expert_name ? { + source: 'custom' as const, + name: panel.domain.expert_name, + label: '领域专家', + description: panel.domain.reason || '', + } : null, + ].filter(Boolean) + + for (const expert of expertsToAdd) { + try { + await topicExpertsApi.add(topicId, expert!) + } catch (err) { + handleApiError(err, `添加专家 ${expert!.name} 失败`) + } + } + + setProfilePanelDismissed(true) + onExpertsChange?.() // 触发专家列表刷新 + handleApiSuccess('专家团已应用') +} +``` + +> ✅ **三方校对修复**:上面的 P0-3 章节已包含 TopicConfigTabs 的完整改动描述(使用 `dpanel-*` CSS命名规范)。本处原有一段旧版本描述(使用 `profile-panel-hint` 等旧类名)已删除,以 P0-3 章节中的内容为准,避免重复定义和类名冲突。 + +--- + +## 三、P1 实施计划(核心数据飞轮,约 8 个工作日) + +### P1-1:讨论页新增「判断行为捕获」 + +> ✅ **三方校对修复(问题#5、#7、#16)**: +> 1. 字段名统一:产品设计用`marked_as_important`,工程计划用`marked_turns`。两者指同一概念。**统一使用`marked_turns`**(更准确描述「标记的发言轮次」)。 +> 2. 右侧判断区:产品设计描述了讨论进行中右侧的「你的判断区」侧边栏,本节补充该UI的改动说明。 +> 3. `handleMentionPost`:TopicDetail已有@mention处理逻辑,**不新建函数,在现有逻辑里追加**判断记录。 + +#### 文件:`frontend/src/pages/TopicDetail.tsx` + +**改动目标**: +1. 新增「标记为重要」按钮(每条讨论发言旁边,小图标,hover才醒目) +2. 在现有@mention逻辑里追加判断记录(不新建函数) +3. 新增右侧「你的判断区」侧边栏(仅在讨论进行中/结束时显示) +4. 讨论结束后展示「查看这次讨论的收获」按钮 + +```tsx +// ── 新增 state ──────────────────────────────────────────────────── +const [markedTurnIds, setMarkedTurnIds] = useState>(new Set()) +const [judgmentRecord, setJudgmentRecord] = useState({ + marked_turns: [], // 统一字段名(与产品设计"marked_as_important"语义一致) + followup_questions: [], +}) +const [showJudgmentModal, setShowJudgmentModal] = useState(false) + +// ── 新增函数:标记某条发言 ───────────────────────────────────────── +const handleMarkTurn = (turnId: string, speaker: string, contentSummary: string) => { + if (markedTurnIds.has(turnId)) return // 防重复标记 + setMarkedTurnIds(prev => new Set(prev).add(turnId)) + setJudgmentRecord(prev => ({ + ...prev, + marked_turns: [...prev.marked_turns, { + turn_id: turnId, + speaker, + content_summary: contentSummary.slice(0, 100), + marked_at: new Date().toISOString(), + }] + })) +} + +// ── 改动现有 @mention 提交逻辑:追加 judgmentRecord 记录 ──────────── +// 在现有 @mention 相关的处理函数里,找到发帖成功的回调,追加: +setJudgmentRecord(prev => ({ + ...prev, + followup_questions: [...prev.followup_questions, { + target_expert: mentionedExpertName, // 被@的专家名 + question_preview: postBody.slice(0, 80), + asked_at: new Date().toISOString(), + }] +})) + +// ── 新增右侧「你的判断区」侧边栏(仅讨论状态下显示)────────────────── +// 位置:包裹讨论内容区域的容器,改为 flex 布局(左主区 + 右侧栏) +// 仅当 discussion_status === 'running' || 'completed' 时显示右侧栏 +{(topic?.discussion_status === 'running' || topic?.discussion_status === 'completed') && ( + +)} + +// ── 讨论完成后展示「查看收获」按钮(吸顶通知条,不沉到底部)───────── +{topic?.discussion_status === 'completed' && ( +
+ 讨论已完成 + +
+)} + +{showJudgmentModal && topic && ( + setShowJudgmentModal(false)} + /> +)} +``` + +#### 新建文件:`frontend/src/api/judgmentApi.ts` + +```typescript +/** + * 判断行为记录 API + * 将用户在圆桌讨论中的判断行为提交给后端,用于画像进化 + */ +import axios from 'axios' +import { tokenManager } from './auth' + +const api = axios.create({ + baseURL: `${import.meta.env.BASE_URL}api`, +}) + +api.interceptors.request.use((config) => { + const token = tokenManager.get() + if (token) config.headers.Authorization = `Bearer ${token}` + return config +}) + +export interface JudgmentRecord { + marked_turns: Array<{ + turn_id: string + speaker: string + content_summary: string + marked_at: string + }> + followup_questions: Array<{ + target_expert: string + question_preview: string + asked_at: string + }> + final_tendency?: string +} + +export async function submitJudgmentRecord( + topicId: string, + sessionId: string, + record: JudgmentRecord +): Promise { + await api.post(`/topics/${topicId}/discussion/judgment`, { + session_id: sessionId, + ...record, + }) +} +``` + +--- + +### P1-2:讨论摘要双层结构 + +#### 文件:`frontend/src/pages/TopicDetail.tsx` + +在讨论结束后(`discussion_status === 'completed'`),展示摘要弹出层或新增一个「查看讨论收获」按钮,点击后跳转到摘要页。 + +```tsx +// 在 discussion 完成状态的展示区域下方,添加: +{isCompleted && ( +
+ +
+)} + +{showJudgmentSummary && ( + setShowJudgmentSummary(false)} + /> +)} +``` + +#### 新建文件:`frontend/src/components/JudgmentSummaryModal.tsx` + +```tsx +/** + * 讨论收获摘要弹窗 + * 双层结构:「你的判断贡献」在上,「AI摘要」在下 + */ +interface JudgmentSummaryModalProps { + topicId: string + judgmentRecord: JudgmentRecord + onClose: () => void +} + +export function JudgmentSummaryModal({ + topicId, + judgmentRecord, + onClose +}: JudgmentSummaryModalProps) { + const [summary, setSummary] = useState(null) + const [loading, setLoading] = useState(true) + const [finalTendency, setFinalTendency] = useState('') + const [submitted, setSubmitted] = useState(false) + + useEffect(() => { + // 提交 judgmentRecord,同时获取 AI 分析 + submitJudgmentRecord(topicId, sessionId, judgmentRecord) + .then(() => getJudgmentSummary(topicId)) + .then(setSummary) + .finally(() => setLoading(false)) + }, []) + + return ( +
+
e.stopPropagation()}> +

这次讨论的收获

+ + {/* 第一层:你的判断贡献(最醒目) */} +
+

你的判断贡献

+ + {judgmentRecord.followup_questions.length > 0 && ( +
+ 你的追问: + {judgmentRecord.followup_questions.map((q, i) => ( +

+ 向 {q.target_expert}:「{q.question_preview}...」 +

+ ))} +
+ )} + + {judgmentRecord.marked_turns.length > 0 && ( +
+ 你标记的关键发言: + {judgmentRecord.marked_turns.map((t, i) => ( +

{t.speaker}:「{t.content_summary}...」

+ ))} +
+ )} + + {/* 最终判断倾向(用户显式输入) */} + {!submitted ? ( +
+

你最终倾向于哪个方向?

+