Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3378bfc61d | |||
| 0c3166060d | |||
| 190cee20bd | |||
| 83f7a66e32 | |||
| 81beb914ee | |||
| 8dcebff7a6 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -108,8 +108,8 @@ develop-eggs/
|
|||||||
downloads/
|
downloads/
|
||||||
eggs/
|
eggs/
|
||||||
.eggs/
|
.eggs/
|
||||||
lib/
|
/lib/
|
||||||
lib64/
|
/lib64/
|
||||||
parts/
|
parts/
|
||||||
sdist/
|
sdist/
|
||||||
var/
|
var/
|
||||||
|
|||||||
375
README.md
375
README.md
@@ -1,121 +1,318 @@
|
|||||||
# Interactive Mindmap
|
# Interactive Mindmap
|
||||||
|
|
||||||
基于 Next.js、FastAPI 和 SQLite 的交互式思维导图应用,支持通过腾讯云 AI Agent 对话生成思维导图。
|
基于 `Next.js 14`、`FastAPI` 和 `SQLite` 的思维导图项目。
|
||||||
|
|
||||||
|
当前项目的实际使用方式是:
|
||||||
|
|
||||||
|
- 腾讯云智能体工作流生成思维导图 JSON
|
||||||
|
- 后端 `POST /api/mindmaps` 保存脑图并返回访问链接
|
||||||
|
- 前端通过 `/mindmap/[id]` 展示脑图
|
||||||
|
- 在脑图详情页中,双击节点可打开右侧聊天面板,继续通过腾讯云智能体进行对话
|
||||||
|
|
||||||
|
## 当前功能
|
||||||
|
|
||||||
|
- 保存思维导图 JSON,并生成可访问链接
|
||||||
|
- 展示思维导图详情页
|
||||||
|
- 支持节点展开/收缩
|
||||||
|
- 支持在详情页中对节点发起对话
|
||||||
|
- 后端代理腾讯云智能体 SSE 接口
|
||||||
|
|
||||||
## 项目结构
|
## 项目结构
|
||||||
|
|
||||||
```
|
```text
|
||||||
mindmap/
|
mindmap/
|
||||||
├── backend/ # FastAPI 后端
|
├── backend/
|
||||||
│ ├── app/
|
│ ├── app/
|
||||||
│ │ ├── main.py # 应用入口
|
│ │ ├── main.py
|
||||||
│ │ ├── config.py # 配置
|
│ │ ├── config.py
|
||||||
│ │ ├── database.py # 数据库
|
│ │ ├── database.py
|
||||||
│ │ ├── models.py # 数据模型
|
│ │ ├── models.py
|
||||||
│ │ ├── schemas.py # Pydantic schemas
|
│ │ ├── schemas.py
|
||||||
│ │ └── routers/
|
│ │ └── routers/
|
||||||
│ │ ├── mindmaps.py # 思维导图 CRUD API
|
│ │ ├── chat.py
|
||||||
│ │ └── chat.py # AI 对话代理 (SSE)
|
│ │ └── mindmaps.py
|
||||||
│ ├── data/ # SQLite 数据库文件
|
│ ├── data/
|
||||||
│ ├── requirements.txt
|
│ ├── requirements.txt
|
||||||
│ └── .env.example
|
│ └── .env.example
|
||||||
├── frontend/ # Next.js 前端
|
├── frontend/
|
||||||
│ ├── app/
|
│ ├── app/
|
||||||
│ │ ├── page.tsx # 首页
|
│ │ ├── page.tsx
|
||||||
│ │ └── mindmap/
|
│ │ └── mindmap/
|
||||||
│ │ ├── [id]/page.tsx # 思维导图详情页
|
│ │ └── [id]/page.tsx
|
||||||
│ │ └── chat/page.tsx # AI 对话 + 思维导图页
|
|
||||||
│ ├── components/
|
│ ├── components/
|
||||||
│ │ ├── MindmapCanvas.tsx # 思维导图画布
|
│ │ ├── ChatPanel.tsx
|
||||||
│ │ ├── MindmapNodeCard.tsx # 节点卡片
|
│ │ ├── MarkdownMessage.tsx
|
||||||
│ │ ├── ChatPanel.tsx # 聊天面板
|
│ │ ├── MindmapCanvas.tsx
|
||||||
│ │ └── CreateMindmapForm.tsx # 创建表单
|
│ │ └── MindmapNodeCard.tsx
|
||||||
│ ├── lib/
|
│ ├── lib/
|
||||||
│ │ ├── api.ts # API 调用
|
│ │ ├── api.ts
|
||||||
│ │ └── treeToGraph.ts # 树转图布局
|
│ │ ├── tencentSse.ts
|
||||||
│ └── types/
|
│ │ └── treeToGraph.ts
|
||||||
│ └── mindmap.ts # TypeScript 类型
|
│ ├── types/
|
||||||
└── mind_prompt.md # AI Agent 系统提示词
|
│ │ └── mindmap.ts
|
||||||
```
|
│ └── package.json
|
||||||
|
├── mind_prompt.md
|
||||||
## 环境配置
|
├── 宝塔面板部署指南.md
|
||||||
|
└── 腾讯云工作流说明.md
|
||||||
### 后端
|
|
||||||
|
|
||||||
1. 安装依赖:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd backend
|
|
||||||
pip install -r requirements.txt
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 配置环境变量:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 复制示例配置
|
|
||||||
cp .env.example .env
|
|
||||||
|
|
||||||
# 编辑 .env,填入腾讯云 AI Agent 的 bot_app_key
|
|
||||||
TENCENT_BOT_APP_KEY=your-key-here
|
|
||||||
```
|
|
||||||
|
|
||||||
3. 启动后端:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
TENCENT_BOT_APP_KEY=your-key-here uvicorn app.main:app --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
### 前端
|
|
||||||
|
|
||||||
1. 安装依赖:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd frontend
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
2. 启动开发服务器:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
npm run dev
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 页面说明
|
## 页面说明
|
||||||
|
|
||||||
### 首页 `/`
|
### 首页 `/`
|
||||||
|
|
||||||
输入主题创建思维导图(使用 mock 数据)。
|
当前首页是项目说明页,不负责直接创建脑图。
|
||||||
|
|
||||||
### 思维导图详情 `/mindmap/[id]`
|
### 思维导图详情页 `/mindmap/[id]`
|
||||||
|
|
||||||
查看已保存的思维导图,支持节点展开/收缩。
|
该页面会:
|
||||||
|
|
||||||
### AI 对话生成 `/mindmap/chat?sessionId=xxx`
|
- 从后端读取指定 `unique_id` 的脑图数据
|
||||||
|
- 渲染交互式脑图
|
||||||
|
- 双击节点后打开右侧聊天面板
|
||||||
|
- 聊天面板通过 `/api/chat` 与腾讯云智能体交互
|
||||||
|
|
||||||
通过与腾讯云 AI Agent 对话生成思维导图:
|
## 后端接口
|
||||||
|
|
||||||
- **左侧**: 思维导图画布,初始为空,AI 返回有效数据后自动渲染
|
### `GET /`
|
||||||
- **右侧**: 聊天面板,支持实时 SSE 流式响应
|
|
||||||
|
|
||||||
URL 参数:
|
健康检查接口。
|
||||||
- `sessionId` (必填): 会话 ID,用于标识对话会话
|
|
||||||
|
|
||||||
## 数据流
|
返回:
|
||||||
|
|
||||||
```
|
```json
|
||||||
用户输入 → ChatPanel → POST /api/chat → 后端代理 → 腾讯云 SSE API
|
{"message":"Interactive Mindmap API is running"}
|
||||||
↓
|
|
||||||
画布更新 ← onMindmapUpdate ← JSON 解析 ← SSE 流式响应 ←─┘
|
|
||||||
```
|
```
|
||||||
|
|
||||||
1. 用户在聊天面板输入消息
|
### `POST /api/mindmaps`
|
||||||
2. 前端发送 POST 请求到后端 `/api/chat`
|
|
||||||
3. 后端将请求代理到腾讯云 AI Agent SSE 接口
|
|
||||||
4. SSE 流式响应逐步返回到前端
|
|
||||||
5. 前端解析 SSE 事件,逐步显示 AI 回复
|
|
||||||
6. 当 AI 回复完成后,尝试从回复中提取思维导图 JSON
|
|
||||||
7. 如果提取成功,更新左侧画布
|
|
||||||
|
|
||||||
## AI Agent 配置
|
创建并保存思维导图。
|
||||||
|
|
||||||
参见 [mind_prompt.md](./mind_prompt.md) 获取 AI Agent 的系统提示词配置。
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "session_001",
|
||||||
|
"mindmap_json": {
|
||||||
|
"id": "node_0",
|
||||||
|
"label": "主题",
|
||||||
|
"parent_id": null,
|
||||||
|
"level": 0,
|
||||||
|
"is_leaf": false,
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回中会包含:
|
||||||
|
|
||||||
|
- `unique_id`
|
||||||
|
- `title`
|
||||||
|
- `url`
|
||||||
|
- `tree`
|
||||||
|
|
||||||
|
### `GET /api/mindmaps/{unique_id}`
|
||||||
|
|
||||||
|
读取指定脑图详情。
|
||||||
|
|
||||||
|
### `POST /api/chat`
|
||||||
|
|
||||||
|
后端代理腾讯云智能体 SSE 接口。
|
||||||
|
|
||||||
|
请求体:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "session_001",
|
||||||
|
"content": "帮我解释一下这个节点",
|
||||||
|
"visitor_biz_id": "default_visitor"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
返回:
|
||||||
|
|
||||||
|
- `text/event-stream`
|
||||||
|
|
||||||
|
## 环境变量
|
||||||
|
|
||||||
|
## 后端 `backend/.env`
|
||||||
|
|
||||||
|
复制:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cp backend/.env.example backend/.env
|
||||||
|
```
|
||||||
|
|
||||||
|
实际使用的变量名是:
|
||||||
|
|
||||||
|
```env
|
||||||
|
BOT_APP_KEY=your-app-key
|
||||||
|
FRONTEND_BASE_URL=http://localhost:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `BOT_APP_KEY`:腾讯云智能体平台 AppKey
|
||||||
|
- `FRONTEND_BASE_URL`:后端返回脑图链接时使用的前端基地址
|
||||||
|
|
||||||
|
注意:当前代码读取的是 `BOT_APP_KEY`,不是 `TENCENT_BOT_APP_KEY`。
|
||||||
|
|
||||||
|
## 前端 `frontend/.env.production`
|
||||||
|
|
||||||
|
如果前后端分端口直连,推荐写成:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_API_BASE_URL=http://你的公网IP:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
如果后面切到统一入口反向代理,可以改成:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_API_BASE_URL=
|
||||||
|
```
|
||||||
|
|
||||||
|
## 本地开发
|
||||||
|
|
||||||
|
## 启动后端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd backend
|
||||||
|
python -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install -r requirements.txt
|
||||||
|
uvicorn app.main:app --reload
|
||||||
|
```
|
||||||
|
|
||||||
|
默认地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 启动前端
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
默认地址:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 生产构建
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm ci
|
||||||
|
rm -rf .next
|
||||||
|
npm run build
|
||||||
|
npm run start -- -p 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 测试一条脑图创建链路
|
||||||
|
|
||||||
|
后端启动后,可以直接调用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://127.0.0.1:8000/api/mindmaps \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"session_id": "test-session",
|
||||||
|
"mindmap_json": {
|
||||||
|
"id": "node_0",
|
||||||
|
"label": "测试主题",
|
||||||
|
"parent_id": null,
|
||||||
|
"level": 0,
|
||||||
|
"is_leaf": false,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "node_1",
|
||||||
|
"label": "测试子节点",
|
||||||
|
"parent_id": "node_0",
|
||||||
|
"level": 1,
|
||||||
|
"is_leaf": true,
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
成功后会返回一个 `url`,打开即可查看脑图详情页。
|
||||||
|
|
||||||
|
## 腾讯云智能体接入说明
|
||||||
|
|
||||||
|
当前项目更适合由腾讯云工作流或插件来创建脑图,而不是由前端首页直接生成。
|
||||||
|
|
||||||
|
建议查看:
|
||||||
|
|
||||||
|
- [mind_prompt.md](./mind_prompt.md)
|
||||||
|
- [腾讯云工作流说明.md](./腾讯云工作流说明.md)
|
||||||
|
|
||||||
|
如果你走当前工作流方案,关键链路是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
开始
|
||||||
|
-> 大模型生成内容
|
||||||
|
-> 大模型整理 JSON
|
||||||
|
-> 大模型检查 JSON
|
||||||
|
-> 变量转换(JSON 反序列化)
|
||||||
|
-> 插件调用 /api/mindmaps
|
||||||
|
-> 回复脑图链接
|
||||||
|
```
|
||||||
|
|
||||||
|
## 部署说明
|
||||||
|
|
||||||
|
当前实际部署文档见:
|
||||||
|
|
||||||
|
- [宝塔面板部署指南.md](./宝塔面板部署指南.md)
|
||||||
|
|
||||||
|
当前推荐部署方式与实际落地一致:
|
||||||
|
|
||||||
|
- 前端放在宝塔 `Node 项目`
|
||||||
|
- 后端放在宝塔 `Python 项目`
|
||||||
|
- 通过公网 IP + `3000/8000` 直接访问
|
||||||
|
|
||||||
|
## 已知注意事项
|
||||||
|
|
||||||
|
### 1. `frontend/lib` 不能被 `.gitignore` 误伤
|
||||||
|
|
||||||
|
根目录 `.gitignore` 如果写成:
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
lib/
|
||||||
|
```
|
||||||
|
|
||||||
|
会把 `frontend/lib/` 一起忽略掉,导致服务器缺少:
|
||||||
|
|
||||||
|
- `frontend/lib/api.ts`
|
||||||
|
- `frontend/lib/tencentSse.ts`
|
||||||
|
- `frontend/lib/treeToGraph.ts`
|
||||||
|
|
||||||
|
正确写法应该是:
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
/lib/
|
||||||
|
/lib64/
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 前端接口地址改动后必须重建
|
||||||
|
|
||||||
|
只要改了:
|
||||||
|
|
||||||
|
- `NEXT_PUBLIC_API_BASE_URL`
|
||||||
|
|
||||||
|
就必须重新执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
rm -rf .next
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 后端返回链接依赖 `FRONTEND_BASE_URL`
|
||||||
|
|
||||||
|
如果这个值不对,后端返回的 `url` 就会不对。
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useParams } from "next/navigation";
|
import { useParams } from "next/navigation";
|
||||||
import {
|
import {
|
||||||
@@ -8,10 +8,10 @@ import {
|
|||||||
useState,
|
useState,
|
||||||
type MouseEvent as ReactMouseEvent,
|
type MouseEvent as ReactMouseEvent,
|
||||||
} from "react";
|
} from "react";
|
||||||
import MindmapCanvas from "@/components/MindmapCanvas";
|
import MindmapCanvas from "../../../components/MindmapCanvas";
|
||||||
import ChatPanel from "@/components/ChatPanel";
|
import ChatPanel from "../../../components/ChatPanel";
|
||||||
import { getMindmap } from "@/lib/api";
|
import { getMindmap } from "../../../lib/api";
|
||||||
import type { Mindmap } from "@/types/mindmap";
|
import type { Mindmap } from "../../../types/mindmap";
|
||||||
|
|
||||||
type TriggerRequest = {
|
type TriggerRequest = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import MarkdownMessage from "@/components/MarkdownMessage";
|
import MarkdownMessage from "./MarkdownMessage";
|
||||||
import { sendChatMessage } from "@/lib/api";
|
import { sendChatMessage } from "../lib/api";
|
||||||
import {
|
import {
|
||||||
parseJsonSafely,
|
parseJsonSafely,
|
||||||
splitSseEvents,
|
splitSseEvents,
|
||||||
type TencentErrorEvent,
|
type TencentErrorEvent,
|
||||||
type TencentReplyEvent,
|
type TencentReplyEvent,
|
||||||
} from "@/lib/tencentSse";
|
} from "../lib/tencentSse";
|
||||||
import type { ChatMessage } from "@/types/mindmap";
|
import type { ChatMessage } from "../types/mindmap";
|
||||||
|
|
||||||
type TriggerRequest = {
|
type TriggerRequest = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import ReactFlow, { Background, Controls, MiniMap, type ReactFlowInstance } from "reactflow";
|
import ReactFlow, { Background, Controls, MiniMap, type ReactFlowInstance } from "reactflow";
|
||||||
import "reactflow/dist/style.css";
|
import "reactflow/dist/style.css";
|
||||||
import MindmapNodeCard from "@/components/MindmapNodeCard";
|
import MindmapNodeCard from "./MindmapNodeCard";
|
||||||
import { treeToGraph } from "@/lib/treeToGraph";
|
import { treeToGraph } from "../lib/treeToGraph";
|
||||||
import type { MindmapNode } from "@/types/mindmap";
|
import type { MindmapNode } from "../types/mindmap";
|
||||||
|
|
||||||
type MindmapCanvasProps = {
|
type MindmapCanvasProps = {
|
||||||
tree: MindmapNode;
|
tree: MindmapNode;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { memo, useCallback, type MouseEvent } from "react";
|
import { memo, useCallback, type MouseEvent } from "react";
|
||||||
import { Handle, Position, type NodeProps } from "reactflow";
|
import { Handle, Position, type NodeProps } from "reactflow";
|
||||||
import type { GraphNodeData } from "@/types/mindmap";
|
import type { GraphNodeData } from "../types/mindmap";
|
||||||
|
|
||||||
function MindmapNodeCard({ data }: NodeProps<GraphNodeData>) {
|
function MindmapNodeCard({ data }: NodeProps<GraphNodeData>) {
|
||||||
const isRoot = data.level === 0;
|
const isRoot = data.level === 0;
|
||||||
|
|||||||
60
frontend/lib/api.ts
Normal file
60
frontend/lib/api.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import type { CreateMindmapPayload, Mindmap } from "../types/mindmap";
|
||||||
|
|
||||||
|
const API_BASE_URL =
|
||||||
|
process.env.NEXT_PUBLIC_API_BASE_URL ?? "http://127.0.0.1:8000";
|
||||||
|
|
||||||
|
async function request<T>(path: string, init?: RequestInit): Promise<T> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}${path}`, {
|
||||||
|
...init,
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
...(init?.headers ?? {}),
|
||||||
|
},
|
||||||
|
cache: "no-store",
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = await response.text();
|
||||||
|
throw new Error(message || "请求失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (await response.json()) as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createMindmap(
|
||||||
|
payload: CreateMindmapPayload,
|
||||||
|
): Promise<Mindmap> {
|
||||||
|
return request<Mindmap>("/api/mindmaps", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getMindmap(uniqueId: string): Promise<Mindmap> {
|
||||||
|
return request<Mindmap>(`/api/mindmaps/${uniqueId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function sendChatMessage(
|
||||||
|
sessionId: string,
|
||||||
|
content: string,
|
||||||
|
visitorBizId: string = "default_visitor",
|
||||||
|
signal?: AbortSignal,
|
||||||
|
): Promise<Response> {
|
||||||
|
const response = await fetch(`${API_BASE_URL}/api/chat`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
signal,
|
||||||
|
body: JSON.stringify({
|
||||||
|
session_id: sessionId,
|
||||||
|
content,
|
||||||
|
visitor_biz_id: visitorBizId,
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const message = await response.text();
|
||||||
|
throw new Error(message || "请求失败");
|
||||||
|
}
|
||||||
|
|
||||||
|
return response;
|
||||||
|
}
|
||||||
77
frontend/lib/tencentSse.ts
Normal file
77
frontend/lib/tencentSse.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
export type SseEvent = {
|
||||||
|
event: string;
|
||||||
|
data: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TencentReplyEvent = {
|
||||||
|
type?: "reply";
|
||||||
|
payload?: {
|
||||||
|
content?: string;
|
||||||
|
is_evil?: boolean;
|
||||||
|
is_final?: boolean;
|
||||||
|
is_from_self?: boolean;
|
||||||
|
is_llm_generated?: boolean;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export type TencentErrorEvent = {
|
||||||
|
type?: "error";
|
||||||
|
error?: {
|
||||||
|
code?: number;
|
||||||
|
message?: string;
|
||||||
|
};
|
||||||
|
message?: string;
|
||||||
|
status?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function parseJsonSafely<T>(value: string): T | null {
|
||||||
|
try {
|
||||||
|
return JSON.parse(value) as T;
|
||||||
|
} catch {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitSseEvents(buffer: string): {
|
||||||
|
events: SseEvent[];
|
||||||
|
rest: string;
|
||||||
|
} {
|
||||||
|
const blocks = buffer.split(/\r?\n\r?\n/);
|
||||||
|
const rest = blocks.pop() ?? "";
|
||||||
|
const events = blocks
|
||||||
|
.map(parseSseEventBlock)
|
||||||
|
.filter((event): event is SseEvent => event !== null);
|
||||||
|
|
||||||
|
return { events, rest };
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSseEventBlock(block: string): SseEvent | null {
|
||||||
|
const lines = block.split(/\r?\n/);
|
||||||
|
let eventName = "message";
|
||||||
|
const dataLines: string[] = [];
|
||||||
|
|
||||||
|
for (const rawLine of lines) {
|
||||||
|
const line = rawLine.trimEnd();
|
||||||
|
if (!line || line.startsWith(":")) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("event:")) {
|
||||||
|
eventName = line.slice(6).trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (line.startsWith("data:")) {
|
||||||
|
dataLines.push(line.slice(5).trimStart());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataLines.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
event: eventName,
|
||||||
|
data: dataLines.join("\n"),
|
||||||
|
};
|
||||||
|
}
|
||||||
129
frontend/lib/treeToGraph.ts
Normal file
129
frontend/lib/treeToGraph.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import type { Edge, Node } from "reactflow";
|
||||||
|
import { Position } from "reactflow";
|
||||||
|
import type { GraphData, GraphNodeData, MindmapNode } from "../types/mindmap";
|
||||||
|
|
||||||
|
const LEVEL_GAP = 320;
|
||||||
|
const SIBLING_GAP = 120;
|
||||||
|
const PADDING_X = 80;
|
||||||
|
const PADDING_Y = 100;
|
||||||
|
|
||||||
|
type PositionMap = Map<string, { x: number; y: number }>;
|
||||||
|
|
||||||
|
type TreeToGraphOptions = {
|
||||||
|
expandedNodeIds?: Set<string>;
|
||||||
|
onToggleNode?: (nodeId: string) => void;
|
||||||
|
onOpenNodeChat?: (nodeLabel: string) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
function getVisibleChildren(
|
||||||
|
node: MindmapNode,
|
||||||
|
expandedNodeIds: Set<string>,
|
||||||
|
): MindmapNode[] {
|
||||||
|
if (node.children.length === 0) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
return expandedNodeIds.has(node.id) ? node.children : [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function assignPositions(
|
||||||
|
node: MindmapNode,
|
||||||
|
positions: PositionMap,
|
||||||
|
leafIndexRef: { value: number },
|
||||||
|
expandedNodeIds: Set<string>,
|
||||||
|
): number {
|
||||||
|
const visibleChildren = getVisibleChildren(node, expandedNodeIds);
|
||||||
|
|
||||||
|
if (visibleChildren.length === 0) {
|
||||||
|
const y = leafIndexRef.value * SIBLING_GAP;
|
||||||
|
leafIndexRef.value += 1;
|
||||||
|
positions.set(node.id, { x: node.level * LEVEL_GAP, y });
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
const childYs = visibleChildren.map((child) =>
|
||||||
|
assignPositions(child, positions, leafIndexRef, expandedNodeIds),
|
||||||
|
);
|
||||||
|
const y =
|
||||||
|
childYs.reduce((total, current) => total + current, 0) / childYs.length;
|
||||||
|
|
||||||
|
positions.set(node.id, { x: node.level * LEVEL_GAP, y });
|
||||||
|
return y;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildGraph(
|
||||||
|
node: MindmapNode,
|
||||||
|
positions: PositionMap,
|
||||||
|
nodes: Node<GraphNodeData>[],
|
||||||
|
edges: Edge[],
|
||||||
|
expandedNodeIds: Set<string>,
|
||||||
|
options: TreeToGraphOptions,
|
||||||
|
): void {
|
||||||
|
const position = positions.get(node.id);
|
||||||
|
|
||||||
|
if (!position) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasChildren = node.children.length > 0;
|
||||||
|
const isExpanded = expandedNodeIds.has(node.id);
|
||||||
|
const onToggleNode = options.onToggleNode;
|
||||||
|
const onOpenNodeChat = options.onOpenNodeChat;
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
id: node.id,
|
||||||
|
type: "mindmapNode",
|
||||||
|
position: {
|
||||||
|
x: position.x + PADDING_X,
|
||||||
|
y: position.y + PADDING_Y,
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
cursor: "default",
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
label: node.label,
|
||||||
|
level: node.level,
|
||||||
|
isLeaf: node.is_leaf,
|
||||||
|
hasChildren,
|
||||||
|
isExpanded,
|
||||||
|
onToggle: hasChildren && onToggleNode ? () => onToggleNode(node.id) : undefined,
|
||||||
|
onOpenChat: onOpenNodeChat ? () => onOpenNodeChat(node.label) : undefined,
|
||||||
|
},
|
||||||
|
sourcePosition: Position.Right,
|
||||||
|
targetPosition: Position.Left,
|
||||||
|
draggable: false,
|
||||||
|
selectable: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
getVisibleChildren(node, expandedNodeIds).forEach((child) => {
|
||||||
|
edges.push({
|
||||||
|
id: `${node.id}-${child.id}`,
|
||||||
|
source: node.id,
|
||||||
|
target: child.id,
|
||||||
|
type: "smoothstep",
|
||||||
|
animated: false,
|
||||||
|
style: {
|
||||||
|
stroke: "#60a5fa",
|
||||||
|
strokeWidth: 2,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
buildGraph(child, positions, nodes, edges, expandedNodeIds, options);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function treeToGraph(
|
||||||
|
tree: MindmapNode,
|
||||||
|
options: TreeToGraphOptions = {},
|
||||||
|
): GraphData {
|
||||||
|
const positions: PositionMap = new Map();
|
||||||
|
const leafIndexRef = { value: 0 };
|
||||||
|
const nodes: Node<GraphNodeData>[] = [];
|
||||||
|
const edges: Edge[] = [];
|
||||||
|
const expandedNodeIds = options.expandedNodeIds ?? new Set([tree.id]);
|
||||||
|
|
||||||
|
assignPositions(tree, positions, leafIndexRef, expandedNodeIds);
|
||||||
|
buildGraph(tree, positions, nodes, edges, expandedNodeIds, options);
|
||||||
|
|
||||||
|
return { nodes, edges };
|
||||||
|
}
|
||||||
@@ -18,8 +18,9 @@
|
|||||||
"name": "next"
|
"name": "next"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
||||||
|
|||||||
393
宝塔面板部署指南.md
Normal file
393
宝塔面板部署指南.md
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
# Interactive Mindmap 宝塔部署指南
|
||||||
|
|
||||||
|
## 1. 当前部署形态
|
||||||
|
|
||||||
|
这份文档按当前实际部署方式整理:
|
||||||
|
|
||||||
|
- 前端放在宝塔 `Node 项目`
|
||||||
|
- 后端放在宝塔 `Python 项目`
|
||||||
|
- 当前直接通过公网 IP + 原始端口访问
|
||||||
|
- 前端地址:`http://公网IP:3000`
|
||||||
|
- 后端地址:`http://公网IP:8000`
|
||||||
|
|
||||||
|
以下示例以当前服务器目录为例:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/mindmap/minimap
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你的实际目录不同,把文档里的路径替换成你的真实路径即可。
|
||||||
|
|
||||||
|
## 2. 当前推荐结构
|
||||||
|
|
||||||
|
### 前端
|
||||||
|
|
||||||
|
- 项目目录:`/mindmap/minimap/frontend`
|
||||||
|
- 运行方式:宝塔 `Node 项目`
|
||||||
|
- 运行端口:`3000`
|
||||||
|
- Node 版本:建议 `20`
|
||||||
|
- 启动命令:`npm run start -- -p 3000`
|
||||||
|
|
||||||
|
### 后端
|
||||||
|
|
||||||
|
- 项目目录:`/mindmap/minimap/backend`
|
||||||
|
- 运行方式:宝塔 `Python 项目`
|
||||||
|
- Python 版本:建议 `3.11`
|
||||||
|
- 运行端口:`8000`
|
||||||
|
- 启动命令:`uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers`
|
||||||
|
|
||||||
|
### 数据库
|
||||||
|
|
||||||
|
- 使用 SQLite
|
||||||
|
- 数据文件:`/mindmap/minimap/backend/data/mindmap.db`
|
||||||
|
|
||||||
|
## 3. 部署前需要确认的代码行为
|
||||||
|
|
||||||
|
这个项目里有几个配置不能写错:
|
||||||
|
|
||||||
|
- 后端环境变量名是 `BOT_APP_KEY`
|
||||||
|
- 后端返回脑图链接依赖 `FRONTEND_BASE_URL`
|
||||||
|
- 前端请求后端依赖 `NEXT_PUBLIC_API_BASE_URL`
|
||||||
|
- 前端详情页路径是 `/mindmap/[id]`
|
||||||
|
- 后端创建脑图接口是 `POST /api/mindmaps`
|
||||||
|
- 后端对话接口是 `POST /api/chat`
|
||||||
|
|
||||||
|
对应代码文件:
|
||||||
|
|
||||||
|
- [backend/app/config.py](./backend/app/config.py)
|
||||||
|
- [backend/app/routers/mindmaps.py](./backend/app/routers/mindmaps.py)
|
||||||
|
- [frontend/lib/api.ts](./frontend/lib/api.ts)
|
||||||
|
|
||||||
|
## 4. 前端部署到宝塔 Node 项目
|
||||||
|
|
||||||
|
## 4.1 前端目录
|
||||||
|
|
||||||
|
前端目录:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/mindmap/minimap/frontend
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4.2 前端环境变量
|
||||||
|
|
||||||
|
在前端目录创建或修改 `.env.production`:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_API_BASE_URL=http://你的公网IP:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- 当前前端直接访问后端 `8000` 端口,所以这里不能留空
|
||||||
|
- 只要改过这个值,就必须重新构建前端
|
||||||
|
|
||||||
|
## 4.3 安装依赖和构建
|
||||||
|
|
||||||
|
宝塔终端执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mindmap/minimap/frontend
|
||||||
|
npm ci
|
||||||
|
rm -rf .next
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
如果构建失败,先看本文末尾的“常见问题”。
|
||||||
|
|
||||||
|
## 4.4 在宝塔中配置 Node 项目
|
||||||
|
|
||||||
|
进入宝塔:
|
||||||
|
|
||||||
|
- `网站`
|
||||||
|
- `Node 项目`
|
||||||
|
- 找到或新建前端项目
|
||||||
|
|
||||||
|
建议配置:
|
||||||
|
|
||||||
|
- 项目名称:`mindmap-frontend`
|
||||||
|
- 项目目录:`/mindmap/minimap/frontend`
|
||||||
|
- Node 版本:`20`
|
||||||
|
- 运行端口:`3000`
|
||||||
|
- 启动方式:`PM2`
|
||||||
|
|
||||||
|
如果界面需要填写启动命令,建议使用:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run start -- -p 3000
|
||||||
|
```
|
||||||
|
|
||||||
|
如果界面要求先执行构建,请先完成:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4.5 前端验证
|
||||||
|
|
||||||
|
浏览器访问:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://公网IP:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
如果首页能打开,说明 Node 项目启动正常。
|
||||||
|
|
||||||
|
## 5. 后端部署到宝塔 Python 项目
|
||||||
|
|
||||||
|
## 5.1 后端目录
|
||||||
|
|
||||||
|
后端目录:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
/mindmap/minimap/backend
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5.2 后端环境变量
|
||||||
|
|
||||||
|
复制配置文件:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mindmap/minimap/backend
|
||||||
|
cp .env.example .env
|
||||||
|
```
|
||||||
|
|
||||||
|
然后将 `.env` 改成类似这样:
|
||||||
|
|
||||||
|
```env
|
||||||
|
BOT_APP_KEY=你的腾讯云智能体AppKey
|
||||||
|
FRONTEND_BASE_URL=http://你的公网IP:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `BOT_APP_KEY` 是腾讯云智能体平台 AppKey
|
||||||
|
- `FRONTEND_BASE_URL` 必须指向前端真实访问地址
|
||||||
|
- 当前前端在 `3000` 端口,所以这里必须带 `:3000`
|
||||||
|
|
||||||
|
## 5.3 在宝塔中配置 Python 项目
|
||||||
|
|
||||||
|
进入宝塔:
|
||||||
|
|
||||||
|
- `Python 项目`
|
||||||
|
- 找到或新建后端项目
|
||||||
|
|
||||||
|
建议配置:
|
||||||
|
|
||||||
|
- 项目名称:`mindmap-backend`
|
||||||
|
- 项目目录:`/mindmap/minimap/backend`
|
||||||
|
- Python 版本:`3.11`
|
||||||
|
- 监听地址:`0.0.0.0`
|
||||||
|
- 监听端口:`8000`
|
||||||
|
|
||||||
|
如果界面支持自定义启动命令,建议填写:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
uvicorn app.main:app --host 0.0.0.0 --port 8000 --proxy-headers
|
||||||
|
```
|
||||||
|
|
||||||
|
如果宝塔 Python 项目需要先准备虚拟环境和依赖,执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mindmap/minimap/backend
|
||||||
|
python3.11 -m venv venv
|
||||||
|
source venv/bin/activate
|
||||||
|
pip install --upgrade pip
|
||||||
|
pip install -r requirements.txt
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5.4 后端验证
|
||||||
|
|
||||||
|
浏览器访问:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://公网IP:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
或在终端执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://公网IP:8000/
|
||||||
|
```
|
||||||
|
|
||||||
|
正常应返回:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{"message":"Interactive Mindmap API is running"}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 6. 宝塔防火墙与云安全组
|
||||||
|
|
||||||
|
当前部署方式需要对公网放行:
|
||||||
|
|
||||||
|
- `3000`
|
||||||
|
- `8000`
|
||||||
|
|
||||||
|
必须同时检查两层:
|
||||||
|
|
||||||
|
- 宝塔防火墙
|
||||||
|
- 云服务器安全组
|
||||||
|
|
||||||
|
只放行其中一层是不够的。
|
||||||
|
|
||||||
|
## 7. 当前部署方式下的验证步骤
|
||||||
|
|
||||||
|
## 7.1 验证首页
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://公网IP:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
## 7.2 验证后端接口
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl http://公网IP:8000/api/mindmaps/not-found
|
||||||
|
```
|
||||||
|
|
||||||
|
这里预期是后端返回 `404` JSON,而不是超时或 HTML 页面。
|
||||||
|
|
||||||
|
## 7.3 创建一条测试思维导图
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST http://公网IP:8000/api/mindmaps \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"session_id": "deploy-test-session",
|
||||||
|
"mindmap_json": {
|
||||||
|
"id": "node_0",
|
||||||
|
"label": "部署测试",
|
||||||
|
"parent_id": null,
|
||||||
|
"level": 0,
|
||||||
|
"is_leaf": false,
|
||||||
|
"children": [
|
||||||
|
{
|
||||||
|
"id": "node_1",
|
||||||
|
"label": "前端",
|
||||||
|
"parent_id": "node_0",
|
||||||
|
"level": 1,
|
||||||
|
"is_leaf": true,
|
||||||
|
"children": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "node_2",
|
||||||
|
"label": "后端",
|
||||||
|
"parent_id": "node_0",
|
||||||
|
"level": 1,
|
||||||
|
"is_leaf": true,
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
成功后返回的 `url` 应类似:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://公网IP:3000/mindmap/xxxxxx
|
||||||
|
```
|
||||||
|
|
||||||
|
直接打开该链接,能正常看到脑图页,说明前后端都通了。
|
||||||
|
|
||||||
|
## 8. 后续如果想切到宝塔网站统一入口
|
||||||
|
|
||||||
|
你当前不需要这一步,但后续可以这样演进:
|
||||||
|
|
||||||
|
1. 前端 Node 项目改为监听 `127.0.0.1:3000`
|
||||||
|
2. 后端 Python 项目改为监听 `127.0.0.1:8000`
|
||||||
|
3. 再使用宝塔 `网站 -> 添加站点`
|
||||||
|
4. 用站点反向代理把 `/` 转到前端,把 `/api/` 转到后端
|
||||||
|
|
||||||
|
如果切到这种模式:
|
||||||
|
|
||||||
|
- 前端 `.env.production` 应改为 `NEXT_PUBLIC_API_BASE_URL=`
|
||||||
|
- 后端 `.env` 中 `FRONTEND_BASE_URL` 应改成统一入口地址
|
||||||
|
- 前端需要重新构建
|
||||||
|
|
||||||
|
## 9. 常见问题
|
||||||
|
|
||||||
|
## 9.1 前端能打开,但接口请求错地址
|
||||||
|
|
||||||
|
检查:
|
||||||
|
|
||||||
|
```env
|
||||||
|
NEXT_PUBLIC_API_BASE_URL=http://你的公网IP:8000
|
||||||
|
```
|
||||||
|
|
||||||
|
只要这个值改过,就重新执行:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /mindmap/minimap/frontend
|
||||||
|
rm -rf .next
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
然后在宝塔里重启 Node 项目。
|
||||||
|
|
||||||
|
## 9.2 后端返回的脑图链接不对
|
||||||
|
|
||||||
|
检查:
|
||||||
|
|
||||||
|
```env
|
||||||
|
FRONTEND_BASE_URL=http://你的公网IP:3000
|
||||||
|
```
|
||||||
|
|
||||||
|
如果这里错了,API 返回的 `url` 就会错。
|
||||||
|
|
||||||
|
## 9.3 前端构建时报 `Can't resolve ../lib/api`
|
||||||
|
|
||||||
|
这通常不是 Next.js 本身问题,而是服务器代码不完整。
|
||||||
|
|
||||||
|
先确认这些文件在服务器上存在:
|
||||||
|
|
||||||
|
```text
|
||||||
|
/mindmap/minimap/frontend/lib/api.ts
|
||||||
|
/mindmap/minimap/frontend/lib/tencentSse.ts
|
||||||
|
/mindmap/minimap/frontend/lib/treeToGraph.ts
|
||||||
|
/mindmap/minimap/frontend/types/mindmap.ts
|
||||||
|
```
|
||||||
|
|
||||||
|
如果缺文件,优先检查 Git 同步是否完整。
|
||||||
|
|
||||||
|
## 9.4 Git push 后服务器代码不完整
|
||||||
|
|
||||||
|
这个项目之前踩过一个典型问题:
|
||||||
|
|
||||||
|
- 根目录 `.gitignore` 中如果写了 `lib/`
|
||||||
|
- 会把 `frontend/lib/` 一起忽略掉
|
||||||
|
|
||||||
|
正确写法应该是:
|
||||||
|
|
||||||
|
```gitignore
|
||||||
|
/lib/
|
||||||
|
/lib64/
|
||||||
|
```
|
||||||
|
|
||||||
|
这样只忽略仓库根目录的 `lib`,不会误伤 `frontend/lib`。
|
||||||
|
|
||||||
|
## 9.5 公网访问不到
|
||||||
|
|
||||||
|
优先检查:
|
||||||
|
|
||||||
|
- 宝塔防火墙是否放行 `3000` 和 `8000`
|
||||||
|
- 云安全组是否放行 `3000` 和 `8000`
|
||||||
|
- Node 项目是否运行中
|
||||||
|
- Python 项目是否运行中
|
||||||
|
|
||||||
|
## 9.6 浏览器提示不安全
|
||||||
|
|
||||||
|
当前是:
|
||||||
|
|
||||||
|
- 公网 IP
|
||||||
|
- HTTP
|
||||||
|
- 非 80/443 端口
|
||||||
|
|
||||||
|
浏览器提示“不安全”是正常现象,不代表服务没有跑起来。
|
||||||
|
|
||||||
|
## 10. 当前最稳的运维方式
|
||||||
|
|
||||||
|
按你现在的实际情况,最稳的方式就是保持:
|
||||||
|
|
||||||
|
1. 前端继续放在宝塔 `Node 项目`
|
||||||
|
2. 后端继续放在宝塔 `Python 项目`
|
||||||
|
3. 继续用 `3000/8000` 直接验证
|
||||||
|
4. 等功能稳定后,再考虑加宝塔网站做统一入口
|
||||||
383
腾讯云工作流说明.md
Normal file
383
腾讯云工作流说明.md
Normal file
@@ -0,0 +1,383 @@
|
|||||||
|
# 腾讯云思维导图工作流说明
|
||||||
|
|
||||||
|
## 1. 工作流目标
|
||||||
|
|
||||||
|
当前工作流的目标是:
|
||||||
|
|
||||||
|
1. 接收用户主题与会话 ID
|
||||||
|
2. 通过大模型生成思维导图内容
|
||||||
|
3. 将内容整理为合法 JSON 字符串
|
||||||
|
4. 对 JSON 做校验和修复
|
||||||
|
5. 将 JSON 字符串反序列化为对象
|
||||||
|
6. 调用后端工具保存脑图
|
||||||
|
7. 将生成后的访问链接回复给用户
|
||||||
|
|
||||||
|
这条链路的关键点是:
|
||||||
|
|
||||||
|
- 大模型节点输出保持为 `string`
|
||||||
|
- 插件节点接收的 `mindmap_json` 必须是 `object`
|
||||||
|
- 所以中间必须经过一次 `变量转换节点 -> JSON 反序列化`
|
||||||
|
|
||||||
|
## 2. 当前工作流节点
|
||||||
|
|
||||||
|
根据当前画布,节点顺序为:
|
||||||
|
|
||||||
|
1. `开始`
|
||||||
|
2. `生成思维导图内容`
|
||||||
|
3. `生成思维导图内容json`
|
||||||
|
4. `检查json`
|
||||||
|
5. `变量转换1`
|
||||||
|
6. `生成前端页面`
|
||||||
|
7. `回复1`
|
||||||
|
8. `结束`
|
||||||
|
|
||||||
|
## 3. 节点职责说明
|
||||||
|
|
||||||
|
## 3.1 开始
|
||||||
|
|
||||||
|
作用:
|
||||||
|
|
||||||
|
- 接收工作流启动输入
|
||||||
|
|
||||||
|
建议输入变量:
|
||||||
|
|
||||||
|
- `topic`
|
||||||
|
- `session_id`
|
||||||
|
|
||||||
|
说明:
|
||||||
|
|
||||||
|
- `topic` 用于告诉大模型本次脑图主题
|
||||||
|
- `session_id` 用于插件调用时传给后端接口
|
||||||
|
|
||||||
|
## 3.2 生成思维导图内容
|
||||||
|
|
||||||
|
节点类型:
|
||||||
|
|
||||||
|
- 大模型节点
|
||||||
|
|
||||||
|
作用:
|
||||||
|
|
||||||
|
- 根据用户主题先生成一版结构化脑图内容
|
||||||
|
|
||||||
|
建议输出:
|
||||||
|
|
||||||
|
- `Output.Content`:文本内容
|
||||||
|
|
||||||
|
建议:
|
||||||
|
|
||||||
|
- 这一层可以先输出脑图提纲或中间描述
|
||||||
|
- 不要求它直接输出最终可落库的 JSON
|
||||||
|
|
||||||
|
## 3.3 生成思维导图内容json
|
||||||
|
|
||||||
|
节点类型:
|
||||||
|
|
||||||
|
- 大模型节点
|
||||||
|
|
||||||
|
作用:
|
||||||
|
|
||||||
|
- 将上一节点的内容转换成思维导图 JSON 字符串
|
||||||
|
|
||||||
|
建议输出:
|
||||||
|
|
||||||
|
- `Output.Content`:JSON 字符串
|
||||||
|
|
||||||
|
这一节点必须满足:
|
||||||
|
|
||||||
|
- 只输出纯 JSON
|
||||||
|
- 不要输出解释文字
|
||||||
|
- 不要输出 markdown 代码块
|
||||||
|
- 不要输出前后缀说明
|
||||||
|
|
||||||
|
建议要求它输出的格式为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "node_0",
|
||||||
|
"label": "主题",
|
||||||
|
"parent_id": null,
|
||||||
|
"level": 0,
|
||||||
|
"is_leaf": false,
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.4 检查json
|
||||||
|
|
||||||
|
节点类型:
|
||||||
|
|
||||||
|
- 大模型节点
|
||||||
|
|
||||||
|
作用:
|
||||||
|
|
||||||
|
- 对上一节点生成的 JSON 字符串做检查和修复
|
||||||
|
|
||||||
|
建议输出:
|
||||||
|
|
||||||
|
- `Output.Content`:修复后的 JSON 字符串
|
||||||
|
|
||||||
|
建议规则:
|
||||||
|
|
||||||
|
1. 如果 JSON 合法且结构正确,原样返回
|
||||||
|
2. 如果不合法,修复后返回
|
||||||
|
3. 始终只输出 JSON 字符串
|
||||||
|
4. 不输出解释文字
|
||||||
|
|
||||||
|
这一节点输出仍然应该是 `string`,不要直接改成 `JSON` 类型输出。
|
||||||
|
|
||||||
|
## 3.5 变量转换1
|
||||||
|
|
||||||
|
节点类型:
|
||||||
|
|
||||||
|
- 变量转换节点
|
||||||
|
|
||||||
|
当前建议配置:
|
||||||
|
|
||||||
|
- 转换类型:`JSON`
|
||||||
|
- 转换方式:`JSON 反序列化`
|
||||||
|
- 转换变量:`检查json.Output.Content`
|
||||||
|
|
||||||
|
作用:
|
||||||
|
|
||||||
|
- 将 `检查json` 节点输出的 JSON 字符串反序列化为对象
|
||||||
|
|
||||||
|
为什么必须有这一步:
|
||||||
|
|
||||||
|
- 大模型节点输出的是字符串
|
||||||
|
- 插件节点里的 `mindmap_json` 入参需要的是 `object`
|
||||||
|
|
||||||
|
该节点默认输出:
|
||||||
|
|
||||||
|
- `Output`
|
||||||
|
- `Error`
|
||||||
|
|
||||||
|
后续插件节点应引用:
|
||||||
|
|
||||||
|
- `变量转换1.Output`
|
||||||
|
|
||||||
|
不要引用:
|
||||||
|
|
||||||
|
- `变量转换1.Error`
|
||||||
|
|
||||||
|
## 3.6 生成前端页面
|
||||||
|
|
||||||
|
节点类型:
|
||||||
|
|
||||||
|
- 插件节点
|
||||||
|
|
||||||
|
作用:
|
||||||
|
|
||||||
|
- 调用后端 API 创建并保存思维导图
|
||||||
|
- 返回脑图访问链接
|
||||||
|
|
||||||
|
当前插件接口建议:
|
||||||
|
|
||||||
|
- 调用地址:`http://公网IP:8000/api/mindmaps`
|
||||||
|
- 请求方式:`POST`
|
||||||
|
- Content-Type:`application/json`
|
||||||
|
|
||||||
|
插件输入参数应配置为:
|
||||||
|
|
||||||
|
- `session_id`
|
||||||
|
- `mindmap_json`
|
||||||
|
|
||||||
|
工作流内的输入映射建议如下:
|
||||||
|
|
||||||
|
### 输入参数 `session_id`
|
||||||
|
|
||||||
|
来源建议:
|
||||||
|
|
||||||
|
- `开始.session_id`
|
||||||
|
|
||||||
|
如果你是通过应用 API 调工作流,也可以使用:
|
||||||
|
|
||||||
|
- `API.session_id`
|
||||||
|
|
||||||
|
### 输入参数 `mindmap_json`
|
||||||
|
|
||||||
|
来源必须是:
|
||||||
|
|
||||||
|
- `变量转换1.Output`
|
||||||
|
|
||||||
|
也就是反序列化后的对象,而不是大模型输出的字符串。
|
||||||
|
|
||||||
|
## 3.7 回复1
|
||||||
|
|
||||||
|
节点类型:
|
||||||
|
|
||||||
|
- 回复节点
|
||||||
|
|
||||||
|
作用:
|
||||||
|
|
||||||
|
- 将生成后的脑图链接返回给用户
|
||||||
|
|
||||||
|
当前文案可以写成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
思维导图已生成!请打开以下链接查看:{{url}}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果你在插件节点里直接暴露的是完整路径变量,也可以写成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
思维导图已生成!请打开以下链接查看:{{生成前端页面.Output.Body.url}}
|
||||||
|
```
|
||||||
|
|
||||||
|
如果还想带标题,可以改成:
|
||||||
|
|
||||||
|
```text
|
||||||
|
思维导图已生成!
|
||||||
|
标题:{{生成前端页面.Output.Body.title}}
|
||||||
|
链接:{{生成前端页面.Output.Body.url}}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 3.8 结束
|
||||||
|
|
||||||
|
作用:
|
||||||
|
|
||||||
|
- 标记工作流结束
|
||||||
|
|
||||||
|
## 4. 当前工作流的数据流
|
||||||
|
|
||||||
|
可以理解为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
开始
|
||||||
|
-> 生成思维导图内容
|
||||||
|
-> 生成思维导图内容json
|
||||||
|
-> 检查json
|
||||||
|
-> 变量转换1(JSON反序列化)
|
||||||
|
-> 生成前端页面(插件/API落库)
|
||||||
|
-> 回复1
|
||||||
|
-> 结束
|
||||||
|
```
|
||||||
|
|
||||||
|
其中最关键的一段是:
|
||||||
|
|
||||||
|
```text
|
||||||
|
检查json.Output.Content (string)
|
||||||
|
-> 变量转换1.Output (object)
|
||||||
|
-> 生成前端页面.mindmap_json
|
||||||
|
```
|
||||||
|
|
||||||
|
## 5. 插件节点推荐配置
|
||||||
|
|
||||||
|
如果你使用当前项目现有后端接口,插件建议如下:
|
||||||
|
|
||||||
|
### 工具基础信息
|
||||||
|
|
||||||
|
- 工具名称:`create_mindmap`
|
||||||
|
- 调用地址:`http://公网IP:8000/api/mindmaps`
|
||||||
|
- 请求方法:`POST`
|
||||||
|
- 请求体格式:`JSON`
|
||||||
|
|
||||||
|
### 输入参数
|
||||||
|
|
||||||
|
- `body.session_id`:`string`
|
||||||
|
- `body.mindmap_json`:`object`
|
||||||
|
|
||||||
|
### 输出参数
|
||||||
|
|
||||||
|
- `url`:`string`
|
||||||
|
- `title`:`string`
|
||||||
|
- `unique_id`:`string`
|
||||||
|
|
||||||
|
如果平台输出变量是以返回体路径显示,也可以直接使用:
|
||||||
|
|
||||||
|
- `Output.Body.url`
|
||||||
|
- `Output.Body.title`
|
||||||
|
- `Output.Body.unique_id`
|
||||||
|
|
||||||
|
## 6. 当前工作流下各节点变量关系
|
||||||
|
|
||||||
|
| 节点 | 关键输入 | 关键输出 | 备注 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 开始 | `topic`、`session_id` | 输入变量本身 | 作为整条链路源头 |
|
||||||
|
| 生成思维导图内容 | `topic` | `Output.Content` | 生成脑图内容 |
|
||||||
|
| 生成思维导图内容json | 上一节点内容 | `Output.Content` | 输出 JSON 字符串 |
|
||||||
|
| 检查json | 上一节点 JSON 字符串 | `Output.Content` | 修复后的 JSON 字符串 |
|
||||||
|
| 变量转换1 | `检查json.Output.Content` | `Output`、`Error` | `Output` 为 object |
|
||||||
|
| 生成前端页面 | `session_id`、`mindmap_json` | `Output.Body.url` 等 | 调用插件/API |
|
||||||
|
| 回复1 | `url` | 文本回复 | 回复用户链接 |
|
||||||
|
|
||||||
|
## 7. 当前工作流的关键注意点
|
||||||
|
|
||||||
|
## 7.1 不要让大模型节点直接输出 object 给插件
|
||||||
|
|
||||||
|
当前这条链里,最稳的方式仍然是:
|
||||||
|
|
||||||
|
- 大模型输出 `string`
|
||||||
|
- 变量转换节点做 `JSON 反序列化`
|
||||||
|
- 插件节点接收 `object`
|
||||||
|
|
||||||
|
## 7.2 插件节点的 `mindmap_json` 必须吃反序列化后的结果
|
||||||
|
|
||||||
|
正确:
|
||||||
|
|
||||||
|
- `变量转换1.Output`
|
||||||
|
|
||||||
|
错误:
|
||||||
|
|
||||||
|
- `检查json.Output.Content`
|
||||||
|
- `生成思维导图内容json.Output.Content`
|
||||||
|
|
||||||
|
因为这两者还是字符串。
|
||||||
|
|
||||||
|
## 7.3 插件节点的 `session_id` 不要写死
|
||||||
|
|
||||||
|
建议引用:
|
||||||
|
|
||||||
|
- `开始.session_id`
|
||||||
|
- 或 `API.session_id`
|
||||||
|
|
||||||
|
这样才能保证每次生成的脑图都能和对应会话关联。
|
||||||
|
|
||||||
|
## 7.4 当前工作流还缺少显式异常分支
|
||||||
|
|
||||||
|
建议后续补一个条件判断:
|
||||||
|
|
||||||
|
1. 如果 `变量转换1.Error` 不为空,直接回复“JSON 解析失败”
|
||||||
|
2. 如果 `生成前端页面.Error` 不为空,直接回复“脑图保存失败”
|
||||||
|
|
||||||
|
当前不加也能跑,但可维护性会差一些。
|
||||||
|
|
||||||
|
## 8. 建议的后续优化
|
||||||
|
|
||||||
|
如果你后面想继续提升稳定性,优先做这几件事:
|
||||||
|
|
||||||
|
1. 在 `检查json` 节点里进一步收紧输出约束,只允许返回纯 JSON 字符串
|
||||||
|
2. 在工作流里增加错误分支,处理反序列化失败和插件调用失败
|
||||||
|
3. 插件输出中除了 `url` 外,再将 `title` 一并回复给用户
|
||||||
|
4. 如果后面模型经常输出不稳定,可以增加代码节点做 schema 校验
|
||||||
|
|
||||||
|
## 9. 当前工作流与项目接口的对应关系
|
||||||
|
|
||||||
|
当前插件节点 `生成前端页面` 对应后端接口:
|
||||||
|
|
||||||
|
- [backend/app/routers/mindmaps.py](./backend/app/routers/mindmaps.py)
|
||||||
|
|
||||||
|
接口实际请求体结构为:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"session_id": "string",
|
||||||
|
"mindmap_json": {
|
||||||
|
"id": "node_0",
|
||||||
|
"label": "主题",
|
||||||
|
"parent_id": null,
|
||||||
|
"level": 0,
|
||||||
|
"is_leaf": false,
|
||||||
|
"children": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
接口实际返回中至少会包含:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"unique_id": "string",
|
||||||
|
"title": "string",
|
||||||
|
"url": "string"
|
||||||
|
}
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user