chore: initialize fullstack starter and team guidelines

This commit is contained in:
2026-10-15 13:05:10 +08:00
commit 768c1c3ce9
37 changed files with 388 additions and 0 deletions
+10
View File
@@ -0,0 +1,10 @@
.env
.venv/
__pycache__/
*.py[cod]
.pytest_cache/
node_modules/
dist/
.vscode/
.idea/
*.log
+50
View File
@@ -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`。
+5
View File
@@ -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
View File
View File
View File
+7
View File
@@ -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=[])
+5
View File
@@ -0,0 +1,5 @@
from fastapi import APIRouter
router = APIRouter(tags=["health"])
@router.get("/health")
def health():
return {"status": "ok", "service": "nalati-rag-api"}
+15
View File
@@ -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
View File
+14
View File
@@ -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()
View File
+16
View File
@@ -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()
+25
View File
@@ -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"}
+1
View File
@@ -0,0 +1 @@
from app.models.knowledge import KnowledgeItem
+14
View File
@@ -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())
View File
+13
View File
@@ -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] = []
+18
View File
@@ -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
+7
View File
@@ -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
+20
View File
@@ -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:
+20
View File
@@ -0,0 +1,20 @@
# 系统架构
```text
Vue 3 + TS 前端
| HTTP /api
FastAPI + Pydantic
|-- 知识管理 API -- PostgreSQL
|-- 问答服务
|-- 关键词检索
|-- Embedding 查询向量 -> pgvector 检索
|-- 结果融合 -> 证据检查 -> LLM API
|-- 答案 + 来源
|-- 后台更新任务(后续)
|-- 来源获取 -> 人工核验 -> 文本切分 -> 生成向量 -> 发布版本
```
当前工程仅实现健康检查、知识条目新增/列表及问答占位接口;pgvector 扩展已在启动时启用,但未实现向量字段、索引和 RAG。前端来源管理与实验页为静态原型。
## 安全边界
当前无认证与权限控制,仅供本机开发;正式部署前须补充登录鉴权、接口限流、数据库迁移、密钥管理、日志脱敏、CORS 收敛和 HTTPS。
+37
View File
@@ -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` 留痕。公共接口或数据库结构变更必须提前通知前后端负责人。
+16
View File
@@ -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 更新策略
状态:设计确定、尚未实现。新文档版本后台生成向量,完成后原子发布;失败保留旧索引。
+23
View File
@@ -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。记录正确性、来源可追溯性、知识不足问题无依据回答率、响应时间、调用成本。不得预设实验结果。
+13
View File
@@ -0,0 +1,13 @@
# 项目章程
## 项目定位
项目:边疆红驿——那拉提文旅智能问答系统。论文研究方向为基于混合检索的那拉提文旅智能问答方法。以红色文化、旅游资源与民宿服务的可信知识问答为核心。
## 第一阶段目标
1. 完成 Vue 3 + TypeScript 静态原型;2. FastAPI + Pydantic 与 PostgreSQL 基础接口联通;3. 建立可追溯知识条目管理;4. 完成关键词检索、向量检索、混合检索;5. 使用真实数据开展对照实验。
## 边界
首版不承诺实时房态、实时价格、自动联网核验、模型训练、知识图谱或自研 MCP。演示数据必须标记,不得当作真实文旅信息或论文实验结果。
## 成果
可运行软件、代码仓库、部署说明、真实测试记录、论文材料及软件著作权申请材料。结项和发表以学校与主管机构要求为准。
+16
View File
@@ -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、鉴权与部署 | 待分配 | 待办 |
+1
View File
@@ -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>
+6
View File
@@ -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"}
}
+20
View File
@@ -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>
+4
View File
@@ -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()}
+4
View File
@@ -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
+1
View File
@@ -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"]}
+1
View File
@@ -0,0 +1 @@
{"files":[],"references":[{"path":"./tsconfig.app.json"},{"path":"./tsconfig.node.json"}]}
+1
View File
@@ -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"]}
+3
View File
@@ -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}}}})