chore: initialize fullstack starter and team guidelines
This commit is contained in:
+10
@@ -0,0 +1,10 @@
|
||||
.env
|
||||
.venv/
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
node_modules/
|
||||
dist/
|
||||
.vscode/
|
||||
.idea/
|
||||
*.log
|
||||
@@ -0,0 +1,50 @@
|
||||
# 那拉提文旅智能问答:前后端基础壳子
|
||||
|
||||
基础工程骨架:Vue 3 + TypeScript + Vite 前端;FastAPI + Pydantic + SQLAlchemy 后端;PostgreSQL + pgvector 数据库配置。
|
||||
|
||||
## 启动数据库
|
||||
需要 Docker Desktop,在项目根目录运行:
|
||||
```bash
|
||||
docker compose up -d db
|
||||
```
|
||||
开发数据库默认仅绑定 127.0.0.1:5432。请在正式使用前修改默认密码。
|
||||
|
||||
## 启动后端(Windows PowerShell)
|
||||
```powershell
|
||||
cd backend
|
||||
py -3.11 -m venv .venv
|
||||
.\.venv\Scripts\Activate.ps1
|
||||
pip install -r requirements.txt
|
||||
Copy-Item .env.example .env
|
||||
uvicorn app.main:app --reload
|
||||
```
|
||||
API 文档:http://127.0.0.1:8000/docs
|
||||
|
||||
## 启动前端(另开终端)
|
||||
```powershell
|
||||
cd frontend
|
||||
npm install
|
||||
npm run dev
|
||||
```
|
||||
Vite 通常使用 http://localhost:5173。
|
||||
|
||||
## 已有接口
|
||||
- GET `/api/health`
|
||||
- POST `/api/chat`:占位接口,尚未接入检索和 LLM
|
||||
- GET `/api/knowledge`
|
||||
- POST `/api/knowledge`
|
||||
|
||||
前端包括问答、知识库管理、来源与更新、实验评估四个页面。实验评估页面没有虚构数据;所有页面明确标记当前功能状态。
|
||||
|
||||
注意:当前没有实现 Embedding、RAG、网络搜索、后台更新任务、用户认证或生产级数据库迁移。下一步建议先验证前后端联通,再按模块实现。`.env` 不要提交到 Git。
|
||||
|
||||
## 团队协作文档
|
||||
|
||||
- [项目章程](docs/PROJECT_CHARTER.md)
|
||||
- [开发计划](docs/DEVELOPMENT_PLAN.md)
|
||||
- [共同开发规范](docs/CONTRIBUTING.md)
|
||||
- [系统架构](docs/ARCHITECTURE.md)
|
||||
- [任务分工](docs/TASKS.md)
|
||||
- [技术决策](docs/DECISIONS.md)
|
||||
|
||||
目标 Gitea 仓库:`https://gitea.kronecker.cc/saint/border-red-post-nalati`。
|
||||
@@ -0,0 +1,5 @@
|
||||
APP_NAME=Nalati Tourism QA API
|
||||
APP_ENV=development
|
||||
API_PREFIX=/api
|
||||
DATABASE_URL=postgresql+psycopg://nalati:change-this-password@127.0.0.1:5432/nalati_rag
|
||||
CORS_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
||||
@@ -0,0 +1,7 @@
|
||||
from fastapi import APIRouter
|
||||
from app.schemas.chat import ChatRequest, ChatResponse
|
||||
router = APIRouter(tags=["chat"])
|
||||
@router.post("/chat", response_model=ChatResponse)
|
||||
def chat(payload: ChatRequest) -> ChatResponse:
|
||||
# TODO: retrieval -> context assembly -> LLM -> evidence/source checks
|
||||
return ChatResponse(answer=f"已收到问题:{payload.question}。当前为基础壳子,尚未接入知识检索或大语言模型。", status="stub", sources=[])
|
||||
@@ -0,0 +1,5 @@
|
||||
from fastapi import APIRouter
|
||||
router = APIRouter(tags=["health"])
|
||||
@router.get("/health")
|
||||
def health():
|
||||
return {"status": "ok", "service": "nalati-rag-api"}
|
||||
@@ -0,0 +1,15 @@
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.orm import Session
|
||||
from app.db.session import get_db
|
||||
from app.models.knowledge import KnowledgeItem
|
||||
from app.schemas.knowledge import KnowledgeCreate, KnowledgeRead
|
||||
router = APIRouter(tags=["knowledge"])
|
||||
@router.get("/knowledge", response_model=list[KnowledgeRead])
|
||||
def list_knowledge(db: Session = Depends(get_db)):
|
||||
return list(db.scalars(select(KnowledgeItem).order_by(KnowledgeItem.id.desc())).all())
|
||||
@router.post("/knowledge", response_model=KnowledgeRead, status_code=201)
|
||||
def create_knowledge(payload: KnowledgeCreate, db: Session = Depends(get_db)):
|
||||
item = KnowledgeItem(title=payload.title, category=payload.category, content=payload.content, source_url=str(payload.source_url) if payload.source_url else None, verification_status="pending")
|
||||
db.add(item); db.commit(); db.refresh(item)
|
||||
return item
|
||||
@@ -0,0 +1,14 @@
|
||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||
|
||||
class Settings(BaseSettings):
|
||||
app_name: str = "Nalati Tourism QA API"
|
||||
app_env: str = "development"
|
||||
api_prefix: str = "/api"
|
||||
database_url: str = "postgresql+psycopg://nalati:change-this-password@127.0.0.1:5432/nalati_rag"
|
||||
cors_origins: str = "http://localhost:5173,http://127.0.0.1:5173"
|
||||
model_config = SettingsConfigDict(env_file=".env", extra="ignore")
|
||||
@property
|
||||
def cors_origin_list(self) -> list[str]:
|
||||
return [x.strip() for x in self.cors_origins.split(",") if x.strip()]
|
||||
|
||||
settings = Settings()
|
||||
@@ -0,0 +1,16 @@
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import DeclarativeBase, sessionmaker
|
||||
from app.core.config import settings
|
||||
|
||||
class Base(DeclarativeBase):
|
||||
pass
|
||||
|
||||
engine = create_engine(settings.database_url, pool_pre_ping=True)
|
||||
SessionLocal = sessionmaker(bind=engine, autoflush=False, autocommit=False)
|
||||
|
||||
def get_db():
|
||||
db = SessionLocal()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
@@ -0,0 +1,25 @@
|
||||
from contextlib import asynccontextmanager
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from sqlalchemy import text
|
||||
from app.core.config import settings
|
||||
from app.db.session import Base, engine
|
||||
from app.models import KnowledgeItem # noqa: F401
|
||||
from app.api.routes import health, chat, knowledge
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
# Convenience for local bootstrap. Use Alembic migrations before production.
|
||||
with engine.begin() as connection:
|
||||
connection.execute(text("CREATE EXTENSION IF NOT EXISTS vector"))
|
||||
Base.metadata.create_all(bind=engine)
|
||||
yield
|
||||
|
||||
app = FastAPI(title=settings.app_name, version="0.1.0", lifespan=lifespan)
|
||||
app.add_middleware(CORSMiddleware, allow_origins=settings.cors_origin_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"])
|
||||
app.include_router(health.router, prefix=settings.api_prefix)
|
||||
app.include_router(chat.router, prefix=settings.api_prefix)
|
||||
app.include_router(knowledge.router, prefix=settings.api_prefix)
|
||||
@app.get("/")
|
||||
def root():
|
||||
return {"message": "Nalati Tourism QA API", "docs": "/docs"}
|
||||
@@ -0,0 +1 @@
|
||||
from app.models.knowledge import KnowledgeItem
|
||||
@@ -0,0 +1,14 @@
|
||||
from datetime import datetime
|
||||
from sqlalchemy import DateTime, Integer, String, Text, func
|
||||
from sqlalchemy.orm import Mapped, mapped_column
|
||||
from app.db.session import Base
|
||||
|
||||
class KnowledgeItem(Base):
|
||||
__tablename__ = "knowledge_items"
|
||||
id: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
|
||||
title: Mapped[str] = mapped_column(String(250), nullable=False, index=True)
|
||||
category: Mapped[str] = mapped_column(String(80), nullable=False, default="未分类")
|
||||
content: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
source_url: Mapped[str | None] = mapped_column(String(1000), nullable=True)
|
||||
verification_status: Mapped[str] = mapped_column(String(30), nullable=False, default="pending")
|
||||
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
|
||||
@@ -0,0 +1,13 @@
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
question: str = Field(min_length=1, max_length=2000)
|
||||
|
||||
class SourcePreview(BaseModel):
|
||||
title: str
|
||||
source_url: str | None = None
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
answer: str
|
||||
status: str
|
||||
sources: list[SourcePreview] = []
|
||||
@@ -0,0 +1,18 @@
|
||||
from datetime import datetime
|
||||
from pydantic import BaseModel, ConfigDict, Field, HttpUrl
|
||||
|
||||
class KnowledgeCreate(BaseModel):
|
||||
title: str = Field(min_length=1, max_length=250)
|
||||
category: str = Field(default="未分类", max_length=80)
|
||||
content: str = Field(min_length=1)
|
||||
source_url: HttpUrl | None = None
|
||||
|
||||
class KnowledgeRead(BaseModel):
|
||||
model_config = ConfigDict(from_attributes=True)
|
||||
id: int
|
||||
title: str
|
||||
category: str
|
||||
content: str
|
||||
source_url: str | None
|
||||
verification_status: str
|
||||
created_at: datetime
|
||||
@@ -0,0 +1,7 @@
|
||||
fastapi>=0.115,<1.0
|
||||
uvicorn[standard]>=0.30,<1.0
|
||||
pydantic>=2.8,<3.0
|
||||
pydantic-settings>=2.4,<3.0
|
||||
SQLAlchemy>=2.0,<3.0
|
||||
psycopg[binary]>=3.2,<4.0
|
||||
pgvector>=0.3,<1.0
|
||||
@@ -0,0 +1,20 @@
|
||||
services:
|
||||
db:
|
||||
image: pgvector/pgvector:pg16
|
||||
container_name: nalati-postgres
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
POSTGRES_DB: nalati_rag
|
||||
POSTGRES_USER: nalati
|
||||
POSTGRES_PASSWORD: change-this-password
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
volumes:
|
||||
- nalati_pgdata:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U nalati -d nalati_rag"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
volumes:
|
||||
nalati_pgdata:
|
||||
@@ -0,0 +1,20 @@
|
||||
# 系统架构
|
||||
|
||||
```text
|
||||
Vue 3 + TS 前端
|
||||
| HTTP /api
|
||||
FastAPI + Pydantic
|
||||
|-- 知识管理 API -- PostgreSQL
|
||||
|-- 问答服务
|
||||
|-- 关键词检索
|
||||
|-- Embedding 查询向量 -> pgvector 检索
|
||||
|-- 结果融合 -> 证据检查 -> LLM API
|
||||
|-- 答案 + 来源
|
||||
|-- 后台更新任务(后续)
|
||||
|-- 来源获取 -> 人工核验 -> 文本切分 -> 生成向量 -> 发布版本
|
||||
```
|
||||
|
||||
当前工程仅实现健康检查、知识条目新增/列表及问答占位接口;pgvector 扩展已在启动时启用,但未实现向量字段、索引和 RAG。前端来源管理与实验页为静态原型。
|
||||
|
||||
## 安全边界
|
||||
当前无认证与权限控制,仅供本机开发;正式部署前须补充登录鉴权、接口限流、数据库迁移、密钥管理、日志脱敏、CORS 收敛和 HTTPS。
|
||||
@@ -0,0 +1,37 @@
|
||||
# 团队共同开发规范
|
||||
|
||||
## 1. 基本原则
|
||||
所有代码、接口、配置、任务进度均以仓库为准。禁止直接在主分支上试验性开发;禁止提交真实密钥、个人数据、未经授权的数据集。每个任务有负责人、验收条件和关联 Issue。
|
||||
|
||||
## 2. 分支管理
|
||||
- `main`:稳定可演示版本,仅通过合并请求更新。
|
||||
- `dev`:阶段集成分支(创建后使用)。
|
||||
- `feat/<模块>-<描述>`:新功能,如 `feat/backend-knowledge-api`。
|
||||
- `fix/<描述>`:修复;`docs/<描述>`:文档;`test/<描述>`:测试。
|
||||
|
||||
从最新 `dev` 拉分支,完成后发 Pull Request(Gitea 可称合并请求),禁止覆盖他人提交或强制推送共享分支。初次初始化仓库时允许负责人向空仓库推送 `main`。
|
||||
|
||||
## 3. 提交信息
|
||||
使用 `feat: ...`、`fix: ...`、`docs: ...`、`refactor: ...`、`test: ...`、`chore: ...`,例如 `feat: add knowledge CRUD API`。一次提交只处理一个相对独立的主题。
|
||||
|
||||
## 4. 合并请求与代码审查
|
||||
合并前说明:需求/Issue、改动范围、测试命令及结果、截图(界面改动)、数据库变更、风险和回滚方式。至少一位非作者成员审查;无成员可审查时由负责人确认并记录。发现冲突先沟通,不直接覆盖。
|
||||
|
||||
## 5. 前后端协作
|
||||
- 所有接口以 `/api` 开头;Pydantic 定义输入输出模型。
|
||||
- 接口字段命名使用 `snake_case`,日期使用带时区的 ISO 8601;错误响应明确错误原因。
|
||||
- 新增接口须同步更新 OpenAPI 文档和前端调用层。
|
||||
- 前端不直接连接数据库、不保存服务端 API 密钥。
|
||||
- 使用 `.env.example` 记录变量名称,真实 `.env` 不入库。
|
||||
|
||||
## 6. 数据与知识质量
|
||||
- 知识须记录标题、分类、正文、来源、采集/核验时间、审核状态。
|
||||
- 未审核资料不得进入正式检索;网络搜索结果不自动等于可靠事实。
|
||||
- 模型向量按 `model/version/dimension` 管理,更新用后台任务和版本切换。
|
||||
- 测试数据与演示数据明确区分;禁止编造论文实验结果。
|
||||
|
||||
## 7. 完成定义(DoD)
|
||||
代码能启动、主要功能有测试或可复现验收步骤、无明文密钥、文档同步、合并请求审查通过,才视为完成。
|
||||
|
||||
## 8. 沟通与任务
|
||||
优先在 Gitea Issues 记录任务;每周更新已完成/阻塞/下一步。技术选型变更先在 `docs/DECISIONS.md` 留痕。公共接口或数据库结构变更必须提前通知前后端负责人。
|
||||
@@ -0,0 +1,16 @@
|
||||
# 技术决策记录(ADR 简版)
|
||||
|
||||
## ADR-001 前后端分离
|
||||
状态:已决定。前端 Vue 3 + TypeScript + Vite;后端 FastAPI + Pydantic。原因:团队并行开发、API 边界明确、原型可先行。
|
||||
|
||||
## ADR-002 数据库
|
||||
状态:已决定。PostgreSQL + pgvector。原因:结构化业务数据与向量检索可统一管理。首版先实现知识表,再逐步加入版本、向量与检索日志。
|
||||
|
||||
## ADR-003 Embedding
|
||||
状态:待评估。采用统一 Provider 接口,在开源本地模型与云端 API 间选择。严禁混用不同模型生成的向量。
|
||||
|
||||
## ADR-004 网络搜索与 MCP
|
||||
状态:延期。首版默认使用经过审核的知识库;联网搜索作为可选扩展。优先封装已有 API,不因使用 MCP 而自研协议服务。
|
||||
|
||||
## ADR-005 更新策略
|
||||
状态:设计确定、尚未实现。新文档版本后台生成向量,完成后原子发布;失败保留旧索引。
|
||||
@@ -0,0 +1,23 @@
|
||||
# 开发规划与里程碑
|
||||
|
||||
## 技术栈
|
||||
- 前端:Vue 3 + TypeScript + Vite
|
||||
- 后端:FastAPI + Pydantic + SQLAlchemy
|
||||
- 数据库:PostgreSQL + pgvector
|
||||
- Embedding:统一接口,首版选择一种中文/多语言模型;切换模型必须重建向量索引
|
||||
- 检索:关键词基线、向量语义检索、混合检索
|
||||
- LLM:API 接入,密钥仅在服务端环境变量中
|
||||
- 网络搜索:可选扩展;候选资料必须核验后方可正式入库
|
||||
|
||||
## 阶段与验收
|
||||
| 阶段 | 交付物 | 验收标准 |
|
||||
|---|---|---|
|
||||
| 1 工程壳子 | 前后端、数据库、静态页面 | `/api/health` 可访问,页面正常展示,新增知识持久化 |
|
||||
| 2 知识管理 | 来源、文档、审核、版本 | 新增、编辑、审核和查询有日志与来源 |
|
||||
| 3 基础 RAG | 关键词检索、LLM 接口 | 可展示检索依据,知识不足时不强答 |
|
||||
| 4 混合检索 | Embedding、pgvector、融合排序 | 能复现 A/B/C 三种对照方案 |
|
||||
| 5 增量更新 | 后台任务、索引版本切换 | 新索引失败不影响旧版检索 |
|
||||
| 6 实验与交付 | 测试集、指标、结果、软著材料 | 数据真实、过程可复现、文档与代码一致 |
|
||||
|
||||
## 实验设计
|
||||
A:直接大模型;B:关键词 RAG;C:混合检索 RAG。记录正确性、来源可追溯性、知识不足问题无依据回答率、响应时间、调用成本。不得预设实验结果。
|
||||
@@ -0,0 +1,13 @@
|
||||
# 项目章程
|
||||
|
||||
## 项目定位
|
||||
项目:边疆红驿——那拉提文旅智能问答系统。论文研究方向为基于混合检索的那拉提文旅智能问答方法。以红色文化、旅游资源与民宿服务的可信知识问答为核心。
|
||||
|
||||
## 第一阶段目标
|
||||
1. 完成 Vue 3 + TypeScript 静态原型;2. FastAPI + Pydantic 与 PostgreSQL 基础接口联通;3. 建立可追溯知识条目管理;4. 完成关键词检索、向量检索、混合检索;5. 使用真实数据开展对照实验。
|
||||
|
||||
## 边界
|
||||
首版不承诺实时房态、实时价格、自动联网核验、模型训练、知识图谱或自研 MCP。演示数据必须标记,不得当作真实文旅信息或论文实验结果。
|
||||
|
||||
## 成果
|
||||
可运行软件、代码仓库、部署说明、真实测试记录、论文材料及软件著作权申请材料。结项和发表以学校与主管机构要求为准。
|
||||
@@ -0,0 +1,16 @@
|
||||
# 共同开发任务板(初稿)
|
||||
|
||||
> 负责人暂未指定。认领后在 Gitea Issue 中补充负责人、预计完成时间和验收记录。
|
||||
|
||||
| 编号 | 模块 | 任务 | 负责人 | 状态 |
|
||||
|---|---|---|---|---|
|
||||
| T01 | 前端 | 验证 Vue 页面启动、补充截图 | 待分配 | 待办 |
|
||||
| T02 | 后端 | 验证 FastAPI `/docs`、`/api/health` | 待分配 | 待办 |
|
||||
| T03 | 数据库 | 启动 PostgreSQL 并验证知识入库 | 待分配 | 待办 |
|
||||
| T04 | 文档 | 明确接口字段、知识来源审核规则 | 待分配 | 待办 |
|
||||
| T05 | 知识库 | 设计 sources/documents/chunks/version 表 | 待分配 | 待办 |
|
||||
| T06 | 检索 | 实现关键词检索基线 | 待分配 | 待办 |
|
||||
| T07 | 向量 | 评估 Embedding 并实现 pgvector | 待分配 | 待办 |
|
||||
| T08 | RAG | 集成 LLM 与来源展示 | 待分配 | 待办 |
|
||||
| T09 | 实验 | 构建可核验问题集及评分规范 | 待分配 | 待办 |
|
||||
| T10 | 工程 | 补充测试、Alembic、鉴权与部署 | 待分配 | 待办 |
|
||||
@@ -0,0 +1 @@
|
||||
<!doctype html><html lang="zh-CN"><head><meta charset="UTF-8"/><meta name="viewport" content="width=device-width, initial-scale=1.0"/><title>边疆红驿 · 那拉提文旅智能问答</title></head><body><div id="app"></div><script type="module" src="/src/main.ts"></script></body></html>
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "nalati-rag-frontend", "private": true, "version": "0.1.0", "type": "module",
|
||||
"scripts": {"dev": "vite", "build": "vue-tsc -b && vite build", "preview": "vite preview"},
|
||||
"dependencies": {"lucide-vue-next": "^0.468.0", "vue": "^3.5.13"},
|
||||
"devDependencies": {"@types/node": "^22.10.2", "@vitejs/plugin-vue": "^5.2.1", "typescript": "~5.7.2", "vite": "^6.0.5", "vue-tsc": "^2.2.0"}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, ref } from 'vue'
|
||||
import { Activity, BookOpen, Bot, CheckCircle2, ChevronRight, Database, FileText, Globe2, Link2, MessageSquareText, Plus, Search, Send, ShieldCheck, Sparkles, AlertCircle } from 'lucide-vue-next'
|
||||
import { askQuestion, createKnowledge, getHealth, getKnowledge } from './api'
|
||||
type Page='chat'|'knowledge'|'sources'|'evaluation'
|
||||
const page=ref<Page>('chat'), apiStatus=ref('检测中'), question=ref(''), busy=ref(false), search=ref(''), showForm=ref(false), error=ref(''), notice=ref('')
|
||||
const messages=ref([{role:'assistant',content:'你好!我是边疆红驿文旅问答原型。当前后端仅为基础接口,尚未接入知识检索和大语言模型。'}] as {role:string;content:string;sources?:{title:string;source_url?:string}[]}[])
|
||||
const items=ref<any[]>([]), form=ref({title:'',category:'旅游资源',content:'',source_url:''})
|
||||
const nav=[{id:'chat',label:'智能问答',icon:MessageSquareText},{id:'knowledge',label:'知识库管理',icon:Database},{id:'sources',label:'来源与更新',icon:Globe2},{id:'evaluation',label:'实验评估',icon:Activity}] as const
|
||||
const filtered=computed(()=>items.value.filter(x=>`${x.title} ${x.category} ${x.content}`.toLowerCase().includes(search.value.toLowerCase())))
|
||||
async function refresh(){try{items.value=await getKnowledge();error.value=''}catch{error.value='无法读取后端知识库,请确认 API 和 PostgreSQL 已启动。'}}
|
||||
onMounted(async()=>{try{await getHealth();apiStatus.value='已连接'}catch{apiStatus.value='未连接'}await refresh()})
|
||||
async function send(){const q=question.value.trim();if(!q||busy.value)return;messages.value.push({role:'user',content:q});question.value='';busy.value=true;try{const r=await askQuestion(q);messages.value.push({role:'assistant',content:r.answer,sources:r.sources})}catch{messages.value.push({role:'assistant',content:'暂时无法连接后端 API。请确认 FastAPI 已启动。'})}finally{busy.value=false}}
|
||||
async function save(){error.value='';notice.value='';try{await createKnowledge({...form.value,...(form.value.source_url?{source_url:form.value.source_url}:{})});form.value={title:'',category:'旅游资源',content:'',source_url:''};showForm.value=false;notice.value='已保存为待审核条目,尚未进入检索索引。';await refresh()}catch(e){error.value=e instanceof Error?e.message:'保存失败'}}
|
||||
</script>
|
||||
<template><div class="shell"><aside class="sidebar"><div class="brand"><div class="mark"><Bot :size="23"/></div><div><b>边疆红驿</b><small>NALATI KNOWLEDGE HUB</small></div></div><p class="muted label">工作空间</p><nav><button v-for="n in nav" :key="n.id" :class="{active:page===n.id}" @click="page=n.id"><component :is="n.icon" :size="18"/>{{n.label}}<ChevronRight v-if="page===n.id" class="chev" :size="15"/></button></nav><div class="side-bottom"><div class="tip"><ShieldCheck :size="17"/><div><b>可信知识服务</b><p>优先展示可核验来源,信息不足时明确提示。</p></div></div><p class="api">● API {{apiStatus}} <span>v0.1.0</span></p></div></aside><main><header><span>边疆红驿 / {{nav.find(n=>n.id===page)?.label}}</span><div><span class="pill">● 原型阶段</span><i>研</i></div></header><section class="content">
|
||||
<div v-if="page==='chat'"><div class="heading"><div><small class="eyebrow"><Sparkles :size="14"/> LOCAL KNOWLEDGE ASSISTANT</small><h1>文旅知识,问得明白</h1><p>面向那拉提文旅与民宿服务的知识问答原型</p></div><span class="chip"><ShieldCheck :size="15"/> 来源可追溯 · 设计目标</span></div><div class="chat-layout"><div class="card chat"><div class="chat-head"><div class="mark"><Bot :size="20"/></div><div><b>边疆红驿助手</b><small>问答服务原型</small></div></div><div class="messages"><div v-for="(m,i) in messages" :key="i" class="message" :class="m.role"><small>{{m.role==='assistant'?'智能助手':'你'}}</small><p>{{m.content}}</p><div v-if="m.sources?.length" class="sources"><b><Link2 :size="14"/> 参考来源</b><a v-for="s in m.sources" :key="s.title" :href="s.source_url" target="_blank">{{s.title}}</a></div></div><p v-if="busy">正在请求后端…</p></div><form class="composer" @submit.prevent="send"><textarea v-model="question" placeholder="输入你想了解的文旅问题…" rows="2" @keydown.enter.exact.prevent="send"/><div><small>当前为基础接口,尚未接入真实检索</small><button class="primary" :disabled="busy||!question.trim()"><Send :size="15"/> 发送</button></div></form></div><aside class="suggest"><b><Sparkles :size="16"/> 你可以这样问</b><button @click="question='那拉提有哪些值得了解的旅游资源?'"><Globe2/>旅游资源 <ChevronRight/></button><button @click="question='有哪些经过核验的红色文化资料?'"><BookOpen/>红色文化 <ChevronRight/></button><button @click="question='民宿服务信息应如何核验?'"><ShieldCheck/>民宿服务 <ChevronRight/></button><div class="notice"><b><AlertCircle :size="15"/> 信息说明</b><p>本项目仍处于原型阶段。演示内容不代表已核验的地方文旅事实。</p></div></aside></div></div>
|
||||
<div v-else-if="page==='knowledge'"><div class="heading"><div><small class="eyebrow"><Database :size="14"/> KNOWLEDGE MANAGEMENT</small><h1>知识库管理</h1><p>集中管理领域知识、来源和审核状态。</p></div><button class="primary" @click="showForm=!showForm"><Plus :size="16"/> 新增知识</button></div><div v-if="showForm" class="card form-card"><b>新增知识条目</b><form class="knowledge-form" @submit.prevent="save"><label>标题<input v-model="form.title" required maxlength="250"/></label><label>分类<select v-model="form.category"><option>红色文化</option><option>旅游资源</option><option>民宿服务</option><option>交通出行</option><option>其他</option></select></label><label class="wide">正文<textarea v-model="form.content" required rows="3"/></label><label class="wide">来源网址(可选)<input v-model="form.source_url" type="url" placeholder="https://..."/></label><div class="wide actions"><button type="button" class="secondary" @click="showForm=false">取消</button><button class="primary">保存条目</button></div></form></div><div class="stats"><div class="card stat"><small>知识条目</small><strong>{{items.length}}</strong><span>来自当前数据库</span></div><div class="card stat"><small>已核验</small><strong>{{items.filter(x=>x.verification_status==='verified').length}}</strong><span>需经过真实审核</span></div><div class="card stat"><small>待审核</small><strong>{{items.filter(x=>x.verification_status==='pending').length}}</strong><span>尚未进入检索索引</span></div></div><p v-if="notice" class="success">{{notice}}</p><p v-if="error" class="error">{{error}}</p><div class="card table-card"><div class="table-head"><div><b>知识条目</b><p>列表来自后端接口,不含预设真实资料。</p></div><label class="search"><Search :size="15"/><input v-model="search" placeholder="搜索知识"/></label></div><div class="scroll"><table><thead><tr><th>标题</th><th>分类</th><th>来源</th><th>状态</th><th>创建时间</th></tr></thead><tbody><tr v-for="x in filtered" :key="x.id"><td><b>{{x.title}}</b><small>{{x.content.slice(0,45)}}</small></td><td>{{x.category}}</td><td><a v-if="x.source_url" :href="x.source_url" target="_blank">打开来源</a><span v-else>未填写</span></td><td><em>{{x.verification_status==='pending'?'待审核':x.verification_status}}</em></td><td>{{new Date(x.created_at).toLocaleDateString()}}</td></tr><tr v-if="!filtered.length"><td colspan="5" class="empty">暂无条目,点击“新增知识”添加。</td></tr></tbody></table></div></div></div>
|
||||
<div v-else-if="page==='sources'"><div class="heading"><div><small class="eyebrow"><Globe2 :size="14"/> SOURCE OPERATIONS</small><h1>来源与更新</h1><p>为后续网页采集、审核、向量生成和版本切换预留界面。</p></div><span class="pill">● 功能规划中</span></div><div class="notice wide"><b><AlertCircle :size="16"/> 当前状态</b><p>此页面为静态原型。尚未连接网页抓取、定时任务或 Embedding 流程,不会自动更新知识。</p></div><div class="stats"><div class="card stat"><small>来源总数</small><strong>—</strong><span>待接入</span></div><div class="card stat"><small>待处理任务</small><strong>—</strong><span>后台队列尚未实现</span></div><div class="card stat"><small>最近更新</small><strong>—</strong><span>待接入更新记录</span></div></div><div class="card roadmap"><b>后续更新流程</b><div><span>01 获取来源</span><ChevronRight/><span>02 核验内容</span><ChevronRight/><span>03 生成向量</span><ChevronRight/><span>04 发布版本</span></div></div></div>
|
||||
<div v-else><div class="heading"><div><small class="eyebrow"><Activity :size="14"/> EVALUATION WORKSPACE</small><h1>实验评估</h1><p>展示实验方案与指标规划;当前不显示虚构实验结果。</p></div><span class="pill">● 尚未开展实验</span></div><div class="notice wide"><b><ShieldCheck :size="16"/> 实验原则</b><p>所有指标应来自真实测试问题、运行日志和明确的人工评分规则。下方仅列评价维度,不包含实验数值。</p></div><div class="stats"><div class="card stat"><small>回答正确性</small><strong class="pending">待测试</strong><span>依据人工评分规范</span></div><div class="card stat"><small>来源可追溯性</small><strong class="pending">待测试</strong><span>核对来源是否支持答案</span></div><div class="card stat"><small>响应时间与成本</small><strong class="pending">待测试</strong><span>记录耗时与实际费用</span></div></div><div class="card roadmap"><b>计划中的对照方案</b><div class="methods"><p><b>A 基础大语言模型</b><span>不提供项目知识库检索结果</span></p><p><b>B 关键词检索 RAG</b><span>关键词检索后生成答案</span></p><p><b>C 混合检索 RAG</b><span>融合关键词与向量语义检索</span></p></div></div></div><footer>边疆红驿 · 那拉提文旅智能问答 <span>工程原型 v0.1.0 · 尚非完整系统</span></footer></section></main></div></template>
|
||||
@@ -0,0 +1,4 @@
|
||||
export async function getHealth(){const r=await fetch('/api/health');if(!r.ok)throw new Error('health failed');return r.json()}
|
||||
export async function askQuestion(question:string){const r=await fetch('/api/chat',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({question})});if(!r.ok)throw new Error('chat failed');return r.json() as Promise<{answer:string;status:string;sources:{title:string;source_url?:string|null}[]}>}
|
||||
export async function getKnowledge(){const r=await fetch('/api/knowledge');if(!r.ok)throw new Error('knowledge failed');return r.json() as Promise<any[]>}
|
||||
export async function createKnowledge(payload:{title:string;category:string;content:string;source_url?:string}){const r=await fetch('/api/knowledge',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(payload)});if(!r.ok)throw new Error('保存失败,请检查网址格式及后端状态');return r.json()}
|
||||
@@ -0,0 +1,4 @@
|
||||
import { createApp } from 'vue'
|
||||
import './style.css'
|
||||
import App from './App.vue'
|
||||
createApp(App).mount('#app')
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
||||
{"compilerOptions":{"tsBuildInfoFile":"./node_modules/.tmp/tsconfig.app.tsbuildinfo","target":"ES2022","useDefineForClassFields":true,"module":"ESNext","lib":["ES2022","DOM","DOM.Iterable"],"skipLibCheck":true,"moduleResolution":"Bundler","allowImportingTsExtensions":true,"resolveJsonModule":true,"isolatedModules":true,"verbatimModuleSyntax":true,"moduleDetection":"force","noEmit":true,"jsx":"preserve","strict":true},"include":["src/**/*.ts","src/**/*.vue"]}
|
||||
@@ -0,0 +1 @@
|
||||
{"files":[],"references":[{"path":"./tsconfig.app.json"},{"path":"./tsconfig.node.json"}]}
|
||||
@@ -0,0 +1 @@
|
||||
{"compilerOptions":{"tsBuildInfoFile":"./node_modules/.tmp/tsconfig.node.tsbuildinfo","target":"ES2023","lib":["ES2023"],"module":"ESNext","skipLibCheck":true,"moduleResolution":"Bundler","allowImportingTsExtensions":true,"verbatimModuleSyntax":true,"moduleDetection":"force","noEmit":true,"strict":true},"include":["vite.config.ts"]}
|
||||
@@ -0,0 +1,3 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
export default defineConfig({plugins:[vue()],server:{port:5173,proxy:{'/api':{target:'http://127.0.0.1:8000',changeOrigin:true}}}})
|
||||
Reference in New Issue
Block a user