From 768c1c3ce95d13bc1c59af9088aee0856668975a Mon Sep 17 00:00:00 2001
From: jhy <184825448@qq.com>
Date: Thu, 15 Oct 2026 13:05:10 +0800
Subject: [PATCH] chore: initialize fullstack starter and team guidelines
---
.gitignore | 10 ++++++
README.md | 50 +++++++++++++++++++++++++++++
backend/.env.example | 5 +++
backend/app/__init__.py | 0
backend/app/api/__init__.py | 0
backend/app/api/routes/__init__.py | 0
backend/app/api/routes/chat.py | 7 ++++
backend/app/api/routes/health.py | 5 +++
backend/app/api/routes/knowledge.py | 15 +++++++++
backend/app/core/__init__.py | 0
backend/app/core/config.py | 14 ++++++++
backend/app/db/__init__.py | 0
backend/app/db/session.py | 16 +++++++++
backend/app/main.py | 25 +++++++++++++++
backend/app/models/__init__.py | 1 +
backend/app/models/knowledge.py | 14 ++++++++
backend/app/schemas/__init__.py | 0
backend/app/schemas/chat.py | 13 ++++++++
backend/app/schemas/knowledge.py | 18 +++++++++++
backend/requirements.txt | 7 ++++
docker-compose.yml | 20 ++++++++++++
docs/ARCHITECTURE.md | 20 ++++++++++++
docs/CONTRIBUTING.md | 37 +++++++++++++++++++++
docs/DECISIONS.md | 16 +++++++++
docs/DEVELOPMENT_PLAN.md | 23 +++++++++++++
docs/PROJECT_CHARTER.md | 13 ++++++++
docs/TASKS.md | 16 +++++++++
frontend/index.html | 1 +
frontend/package.json | 6 ++++
frontend/src/App.vue | 20 ++++++++++++
frontend/src/api.ts | 4 +++
frontend/src/main.ts | 4 +++
frontend/src/style.css | 2 ++
frontend/tsconfig.app.json | 1 +
frontend/tsconfig.json | 1 +
frontend/tsconfig.node.json | 1 +
frontend/vite.config.ts | 3 ++
37 files changed, 388 insertions(+)
create mode 100644 .gitignore
create mode 100644 README.md
create mode 100644 backend/.env.example
create mode 100644 backend/app/__init__.py
create mode 100644 backend/app/api/__init__.py
create mode 100644 backend/app/api/routes/__init__.py
create mode 100644 backend/app/api/routes/chat.py
create mode 100644 backend/app/api/routes/health.py
create mode 100644 backend/app/api/routes/knowledge.py
create mode 100644 backend/app/core/__init__.py
create mode 100644 backend/app/core/config.py
create mode 100644 backend/app/db/__init__.py
create mode 100644 backend/app/db/session.py
create mode 100644 backend/app/main.py
create mode 100644 backend/app/models/__init__.py
create mode 100644 backend/app/models/knowledge.py
create mode 100644 backend/app/schemas/__init__.py
create mode 100644 backend/app/schemas/chat.py
create mode 100644 backend/app/schemas/knowledge.py
create mode 100644 backend/requirements.txt
create mode 100644 docker-compose.yml
create mode 100644 docs/ARCHITECTURE.md
create mode 100644 docs/CONTRIBUTING.md
create mode 100644 docs/DECISIONS.md
create mode 100644 docs/DEVELOPMENT_PLAN.md
create mode 100644 docs/PROJECT_CHARTER.md
create mode 100644 docs/TASKS.md
create mode 100644 frontend/index.html
create mode 100644 frontend/package.json
create mode 100644 frontend/src/App.vue
create mode 100644 frontend/src/api.ts
create mode 100644 frontend/src/main.ts
create mode 100644 frontend/src/style.css
create mode 100644 frontend/tsconfig.app.json
create mode 100644 frontend/tsconfig.json
create mode 100644 frontend/tsconfig.node.json
create mode 100644 frontend/vite.config.ts
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..2b2678b
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,10 @@
+.env
+.venv/
+__pycache__/
+*.py[cod]
+.pytest_cache/
+node_modules/
+dist/
+.vscode/
+.idea/
+*.log
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f55a393
--- /dev/null
+++ b/README.md
@@ -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`。
diff --git a/backend/.env.example b/backend/.env.example
new file mode 100644
index 0000000..aa41b80
--- /dev/null
+++ b/backend/.env.example
@@ -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
diff --git a/backend/app/__init__.py b/backend/app/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/api/__init__.py b/backend/app/api/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/api/routes/__init__.py b/backend/app/api/routes/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/api/routes/chat.py b/backend/app/api/routes/chat.py
new file mode 100644
index 0000000..c601685
--- /dev/null
+++ b/backend/app/api/routes/chat.py
@@ -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=[])
diff --git a/backend/app/api/routes/health.py b/backend/app/api/routes/health.py
new file mode 100644
index 0000000..cbfebf6
--- /dev/null
+++ b/backend/app/api/routes/health.py
@@ -0,0 +1,5 @@
+from fastapi import APIRouter
+router = APIRouter(tags=["health"])
+@router.get("/health")
+def health():
+ return {"status": "ok", "service": "nalati-rag-api"}
diff --git a/backend/app/api/routes/knowledge.py b/backend/app/api/routes/knowledge.py
new file mode 100644
index 0000000..e97bb4a
--- /dev/null
+++ b/backend/app/api/routes/knowledge.py
@@ -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
diff --git a/backend/app/core/__init__.py b/backend/app/core/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/core/config.py b/backend/app/core/config.py
new file mode 100644
index 0000000..8aef48f
--- /dev/null
+++ b/backend/app/core/config.py
@@ -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()
diff --git a/backend/app/db/__init__.py b/backend/app/db/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/db/session.py b/backend/app/db/session.py
new file mode 100644
index 0000000..12aea8d
--- /dev/null
+++ b/backend/app/db/session.py
@@ -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()
diff --git a/backend/app/main.py b/backend/app/main.py
new file mode 100644
index 0000000..0e8f89e
--- /dev/null
+++ b/backend/app/main.py
@@ -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"}
diff --git a/backend/app/models/__init__.py b/backend/app/models/__init__.py
new file mode 100644
index 0000000..03a2db7
--- /dev/null
+++ b/backend/app/models/__init__.py
@@ -0,0 +1 @@
+from app.models.knowledge import KnowledgeItem
diff --git a/backend/app/models/knowledge.py b/backend/app/models/knowledge.py
new file mode 100644
index 0000000..d825dec
--- /dev/null
+++ b/backend/app/models/knowledge.py
@@ -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())
diff --git a/backend/app/schemas/__init__.py b/backend/app/schemas/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/backend/app/schemas/chat.py b/backend/app/schemas/chat.py
new file mode 100644
index 0000000..40ccb80
--- /dev/null
+++ b/backend/app/schemas/chat.py
@@ -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] = []
diff --git a/backend/app/schemas/knowledge.py b/backend/app/schemas/knowledge.py
new file mode 100644
index 0000000..69a3260
--- /dev/null
+++ b/backend/app/schemas/knowledge.py
@@ -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
diff --git a/backend/requirements.txt b/backend/requirements.txt
new file mode 100644
index 0000000..eb450d8
--- /dev/null
+++ b/backend/requirements.txt
@@ -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
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..d311e93
--- /dev/null
+++ b/docker-compose.yml
@@ -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:
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
new file mode 100644
index 0000000..5fb5d97
--- /dev/null
+++ b/docs/ARCHITECTURE.md
@@ -0,0 +1,20 @@
+# 系统架构
+
+```text
+Vue 3 + TS 前端
+ | HTTP /api
+FastAPI + Pydantic
+ |-- 知识管理 API -- PostgreSQL
+ |-- 问答服务
+ |-- 关键词检索
+ |-- Embedding 查询向量 -> pgvector 检索
+ |-- 结果融合 -> 证据检查 -> LLM API
+ |-- 答案 + 来源
+ |-- 后台更新任务(后续)
+ |-- 来源获取 -> 人工核验 -> 文本切分 -> 生成向量 -> 发布版本
+```
+
+当前工程仅实现健康检查、知识条目新增/列表及问答占位接口;pgvector 扩展已在启动时启用,但未实现向量字段、索引和 RAG。前端来源管理与实验页为静态原型。
+
+## 安全边界
+当前无认证与权限控制,仅供本机开发;正式部署前须补充登录鉴权、接口限流、数据库迁移、密钥管理、日志脱敏、CORS 收敛和 HTTPS。
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 0000000..024a546
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -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` 留痕。公共接口或数据库结构变更必须提前通知前后端负责人。
diff --git a/docs/DECISIONS.md b/docs/DECISIONS.md
new file mode 100644
index 0000000..92395b5
--- /dev/null
+++ b/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 更新策略
+状态:设计确定、尚未实现。新文档版本后台生成向量,完成后原子发布;失败保留旧索引。
diff --git a/docs/DEVELOPMENT_PLAN.md b/docs/DEVELOPMENT_PLAN.md
new file mode 100644
index 0000000..61a9f0b
--- /dev/null
+++ b/docs/DEVELOPMENT_PLAN.md
@@ -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。记录正确性、来源可追溯性、知识不足问题无依据回答率、响应时间、调用成本。不得预设实验结果。
diff --git a/docs/PROJECT_CHARTER.md b/docs/PROJECT_CHARTER.md
new file mode 100644
index 0000000..623beda
--- /dev/null
+++ b/docs/PROJECT_CHARTER.md
@@ -0,0 +1,13 @@
+# 项目章程
+
+## 项目定位
+项目:边疆红驿——那拉提文旅智能问答系统。论文研究方向为基于混合检索的那拉提文旅智能问答方法。以红色文化、旅游资源与民宿服务的可信知识问答为核心。
+
+## 第一阶段目标
+1. 完成 Vue 3 + TypeScript 静态原型;2. FastAPI + Pydantic 与 PostgreSQL 基础接口联通;3. 建立可追溯知识条目管理;4. 完成关键词检索、向量检索、混合检索;5. 使用真实数据开展对照实验。
+
+## 边界
+首版不承诺实时房态、实时价格、自动联网核验、模型训练、知识图谱或自研 MCP。演示数据必须标记,不得当作真实文旅信息或论文实验结果。
+
+## 成果
+可运行软件、代码仓库、部署说明、真实测试记录、论文材料及软件著作权申请材料。结项和发表以学校与主管机构要求为准。
diff --git a/docs/TASKS.md b/docs/TASKS.md
new file mode 100644
index 0000000..238a5a2
--- /dev/null
+++ b/docs/TASKS.md
@@ -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、鉴权与部署 | 待分配 | 待办 |
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..f438f5f
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1 @@
+
边疆红驿 · 那拉提文旅智能问答
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..4ad8c9b
--- /dev/null
+++ b/frontend/package.json
@@ -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"}
+}
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 0000000..ec1259d
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,20 @@
+
+边疆红驿 / {{nav.find(n=>n.id===page)?.label}}● 原型阶段研
+ LOCAL KNOWLEDGE ASSISTANT文旅知识,问得明白
面向那拉提文旅与民宿服务的知识问答原型
来源可追溯 · 设计目标{{m.role==='assistant'?'智能助手':'你'}}{{m.content}}
正在请求后端…
+ KNOWLEDGE MANAGEMENT知识库管理
集中管理领域知识、来源和审核状态。
知识条目{{items.length}}来自当前数据库
已核验{{items.filter(x=>x.verification_status==='verified').length}}需经过真实审核
待审核{{items.filter(x=>x.verification_status==='pending').length}}尚未进入检索索引
{{notice}}
{{error}}
+ SOURCE OPERATIONS来源与更新
为后续网页采集、审核、向量生成和版本切换预留界面。
● 功能规划中 当前状态此页面为静态原型。尚未连接网页抓取、定时任务或 Embedding 流程,不会自动更新知识。
来源总数—待接入
待处理任务—后台队列尚未实现
最近更新—待接入更新记录
后续更新流程01 获取来源02 核验内容03 生成向量04 发布版本
+ EVALUATION WORKSPACE实验评估
展示实验方案与指标规划;当前不显示虚构实验结果。
● 尚未开展实验 实验原则所有指标应来自真实测试问题、运行日志和明确的人工评分规则。下方仅列评价维度,不包含实验数值。
回答正确性待测试依据人工评分规范
来源可追溯性待测试核对来源是否支持答案
响应时间与成本待测试记录耗时与实际费用
计划中的对照方案A 基础大语言模型不提供项目知识库检索结果
B 关键词检索 RAG关键词检索后生成答案
C 混合检索 RAG融合关键词与向量语义检索
diff --git a/frontend/src/api.ts b/frontend/src/api.ts
new file mode 100644
index 0000000..ae4cfda
--- /dev/null
+++ b/frontend/src/api.ts
@@ -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}
+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()}
diff --git a/frontend/src/main.ts b/frontend/src/main.ts
new file mode 100644
index 0000000..d47787a
--- /dev/null
+++ b/frontend/src/main.ts
@@ -0,0 +1,4 @@
+import { createApp } from 'vue'
+import './style.css'
+import App from './App.vue'
+createApp(App).mount('#app')
diff --git a/frontend/src/style.css b/frontend/src/style.css
new file mode 100644
index 0000000..8d51e37
--- /dev/null
+++ b/frontend/src/style.css
@@ -0,0 +1,2 @@
+@import url('https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=Noto+Sans+SC:wght@400;500;600;700&display=swap');
+:root{font-family:'DM Sans','Noto Sans SC',sans-serif;color:#252a25;background:#f7f6f1;font-synthesis:none;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;--green:#355c48;--green-dark:#274735;--soft:#e9f0e9;--line:#e8e7df;--surface:#fffefa}*{box-sizing:border-box}body{margin:0;min-width:320px;min-height:100vh}button,input,textarea,select{font:inherit}button{cursor:pointer}button:disabled{opacity:.5;cursor:not-allowed}a{color:var(--green);text-decoration:none}a:hover{text-decoration:underline}.shell{min-height:100vh;display:flex}.sidebar{width:245px;flex:0 0 245px;background:#fbfaf6;border-right:1px solid var(--line);padding:24px 15px 17px;display:flex;flex-direction:column}.brand{display:flex;align-items:center;gap:11px;padding:0 9px 30px}.mark{width:42px;height:42px;border-radius:13px;background:var(--green);color:white;display:grid;place-items:center;flex-shrink:0}.brand b{font-size:17px}.brand small{display:block;font-size:9px;letter-spacing:.12em;color:#8b9088;margin-top:4px}.muted,.label{color:#9a9d94}.label{font-size:11px;padding:0 12px}.sidebar nav{display:grid;gap:5px}.sidebar nav button{display:flex;align-items:center;gap:11px;text-align:left;border:0;border-radius:9px;padding:12px;background:transparent;color:#727a71;font-size:12px}.sidebar nav button.active{background:var(--soft);color:var(--green-dark);font-weight:600}.chev{margin-left:auto}.side-bottom{margin-top:auto}.tip{display:flex;gap:9px;padding:12px;background:#f1f2e9;border:1px solid #e7e9dc;border-radius:11px;color:var(--green)}.tip b{font-size:11px}.tip p{font-size:10px;line-height:1.7;color:#7c8277;margin:5px 0 0}.api{font-size:10px;color:#858980;padding:10px 7px}.api span{float:right;color:#a4a79f}main{flex:1;min-width:0}header{height:66px;border-bottom:1px solid var(--line);display:flex;align-items:center;justify-content:space-between;padding:0 35px;background:#fbfaf6aa;color:#858a81;font-size:12px}header>div{display:flex;align-items:center;gap:14px}header i{font-style:normal;background:#e5e9dc;color:var(--green);border-radius:50%;width:30px;height:30px;display:grid;place-items:center}.pill{border:1px solid #e5e5d9;background:#f5f4ec;border-radius:99px;padding:6px 10px;font-size:10px;color:#7a7d6e}.content{max-width:1450px;margin:auto;padding:32px 36px 20px}.heading{display:flex;align-items:flex-end;justify-content:space-between;gap:18px;margin-bottom:25px}.eyebrow{display:flex;align-items:center;gap:7px;color:#84917f;font-size:10px;letter-spacing:.1em;font-weight:700;margin-bottom:10px}h1{font-size:clamp(25px,2.5vw,33px);letter-spacing:-.04em;margin:0;line-height:1.25}.heading p{font-size:12px;color:#898e85;margin:10px 0 0}.chip{display:flex;align-items:center;gap:7px;background:#eaf0e8;color:var(--green);padding:9px 12px;border-radius:8px;font-size:11px;white-space:nowrap}.chat-layout{display:grid;grid-template-columns:minmax(0,1.65fr) minmax(245px,.8fr);gap:19px;align-items:start}.card{background:var(--surface);border:1px solid var(--line);border-radius:13px;box-shadow:0 4px 18px #273b2a05}.chat{min-height:570px;display:flex;flex-direction:column;overflow:hidden}.chat-head{display:flex;align-items:center;gap:10px;padding:16px 19px;border-bottom:1px solid #efeee8}.chat-head b{font-size:12px}.chat-head small{display:block;font-size:10px;color:#92968d;margin-top:4px}.messages{padding:22px 19px;display:flex;flex-direction:column;gap:18px;min-height:340px;max-height:450px;overflow:auto;flex:1}.message{max-width:90%}.message.user{align-self:flex-end}.message small{font-size:10px;color:#92968d}.message p{font-size:12px;line-height:1.9;padding:11px 13px;background:#f5f5ef;border:1px solid #efefe6;border-radius:4px 12px 12px 12px;color:#4e554d;white-space:pre-wrap;margin:7px 0 0}.message.user p{background:var(--green);color:white;border-color:var(--green);border-radius:12px 4px 12px 12px}.sources{display:grid;gap:7px;border:1px solid var(--line);padding:10px;border-radius:8px;font-size:11px}.sources b{display:flex;align-items:center;gap:6px;color:var(--green)}.composer{padding:13px 17px;border-top:1px solid #efeee8}.composer textarea{width:100%;border:0;outline:0;resize:vertical;background:transparent;font-size:12px;line-height:1.7}.composer>div{display:flex;align-items:center;justify-content:space-between;gap:8px;margin-top:8px}.composer small{font-size:10px;color:#a0a399}.primary{display:inline-flex;align-items:center;justify-content:center;gap:7px;border:0;border-radius:8px;background:var(--green);color:white;padding:10px 14px;font-size:11px;font-weight:600;white-space:nowrap}.primary:hover{background:var(--green-dark)}.suggest{display:grid;gap:10px}.suggest>b{display:flex;align-items:center;gap:8px;font-size:12px;margin-bottom:3px}.suggest>button{display:flex;align-items:center;gap:10px;padding:13px 11px;border:1px solid var(--line);border-radius:10px;background:var(--surface);color:#4b554a;text-align:left;font-size:11px}.suggest>button svg:first-child{color:var(--green)}.suggest>button svg:last-child{margin-left:auto;color:#9b9f95}.notice{background:#f4f0e5;border:1px solid #eee6d3;border-radius:10px;padding:14px;margin-top:7px;color:#8c6e43}.notice b{display:flex;align-items:center;gap:7px;font-size:11px}.notice p{font-size:11px;line-height:1.8;color:#8a8375;margin:8px 0 0}.notice.wide{margin:0 0 18px;padding:16px}.notice.wide p{font-size:12px}.stats{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:13px;margin-bottom:19px}.stat{padding:17px 18px;min-width:0}.stat small{display:block;color:#858b81;font-size:11px}.stat strong{display:block;font-size:27px;margin-top:9px;color:#313c31}.stat span{display:block;color:#a1a49c;font-size:10px;margin-top:7px}.stat strong.pending{font-size:15px;color:#9a804e;margin-top:18px}.table-card{overflow:hidden}.table-head{display:flex;justify-content:space-between;align-items:center;gap:15px;padding:18px 20px}.table-head b{font-size:13px}.table-head p{font-size:10px;color:#9b9e96;margin:6px 0 0}.search{display:flex;align-items:center;gap:7px;border:1px solid var(--line);border-radius:8px;padding:8px 10px;color:#9b9f95}.search input{border:0;outline:0;font-size:11px;min-width:0}.scroll{overflow:auto}table{width:100%;border-collapse:collapse;text-align:left;font-size:11px}th{background:#f8f7f1;color:#8b9087;font-weight:500;white-space:nowrap}th,td{padding:13px 17px;border-top:1px solid #efeee8}td{color:#73796f}td b{display:block;color:#434a41}td small{display:block;color:#a2a59c;font-size:10px;margin-top:5px;max-width:250px}em{font-style:normal;background:#f4f0e3;color:#9a804e;padding:5px 7px;border-radius:5px}.empty{text-align:center;padding:35px;color:#9b9f95}.form-card{padding:20px;margin-bottom:17px}.knowledge-form{display:grid;grid-template-columns:1fr 1fr;gap:13px;margin-top:15px}.knowledge-form label{display:grid;gap:7px;font-size:11px;color:#6f776d}.knowledge-form input,.knowledge-form select,.knowledge-form textarea{width:100%;min-width:0;border:1px solid #e6e6dc;border-radius:8px;padding:10px;background:#fffefa;font-size:12px;outline-color:#93ad93}.wide{grid-column:1/-1}.actions{display:flex;justify-content:flex-end;gap:9px}.secondary{border:1px solid var(--line);border-radius:8px;background:white;padding:10px 14px;font-size:11px}.success,.error{padding:10px 12px;border-radius:8px;font-size:11px}.success{background:#eaf2e8;color:#4b7954}.error{background:#f9eae5;color:#a04d3a}.roadmap{padding:21px;margin-top:18px}.roadmap>b{font-size:13px}.roadmap>div:not(.methods){display:flex;align-items:center;justify-content:space-between;gap:10px;margin-top:22px;font-size:11px;color:var(--green)}.roadmap svg{width:15px;color:#c5c8bd}.methods{margin-top:12px}.methods p{display:flex;flex-direction:column;gap:6px;padding:13px 0;border-top:1px solid #efeee8;font-size:11px}.methods span{color:#9b9f95;font-size:10px}footer{display:flex;justify-content:space-between;gap:10px;padding:24px 2px 0;font-size:10px;color:#a4a69e}footer span{color:#a4a69e}@media(max-width:1000px){.sidebar{width:210px;flex-basis:210px}.content{padding:26px 22px}.chat-layout{grid-template-columns:1fr}.suggest{grid-template-columns:repeat(3,minmax(0,1fr))}.suggest>b,.suggest .notice{grid-column:1/-1}.suggest>button svg:last-child{display:none}}@media(max-width:680px){.shell{display:block}.sidebar{width:100%;padding:12px;border-right:0;border-bottom:1px solid var(--line)}.brand{padding:0 5px 12px}.mark{width:35px;height:35px}.label,.side-bottom{display:none}.sidebar nav{display:flex;overflow:auto}.sidebar nav button{white-space:nowrap;padding:9px;gap:6px}.sidebar nav button svg{width:15px}.chev{display:none}header{height:48px;padding:0 15px}.content{padding:23px 14px} .heading{align-items:flex-start;flex-direction:column}.chip{display:none}.suggest{grid-template-columns:1fr}.suggest>b,.suggest .notice{grid-column:auto}.stats{grid-template-columns:1fr;gap:8px}.stat{padding:13px}.stat strong{margin-top:5px}.knowledge-form{grid-template-columns:1fr}.wide{grid-column:auto}.table-head{align-items:flex-start;flex-direction:column}.search{width:100%}.roadmap>div:not(.methods){align-items:flex-start;flex-direction:column}footer{flex-direction:column}.pill{display:none}}
diff --git a/frontend/tsconfig.app.json b/frontend/tsconfig.app.json
new file mode 100644
index 0000000..070ad40
--- /dev/null
+++ b/frontend/tsconfig.app.json
@@ -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"]}
diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json
new file mode 100644
index 0000000..ff0aea3
--- /dev/null
+++ b/frontend/tsconfig.json
@@ -0,0 +1 @@
+{"files":[],"references":[{"path":"./tsconfig.app.json"},{"path":"./tsconfig.node.json"}]}
diff --git a/frontend/tsconfig.node.json b/frontend/tsconfig.node.json
new file mode 100644
index 0000000..c1b4011
--- /dev/null
+++ b/frontend/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"]}
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
new file mode 100644
index 0000000..4e59ccd
--- /dev/null
+++ b/frontend/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}}}})