1
0

2 Коммитууд 17cdff13d9 ... 23432c6788

Эзэн SHA1 Мессеж Огноо
  anhuiqiang 23432c6788 clean install bugfix 22 цаг өмнө
  anhuiqiang bf839a88e4 doc fix 1 өдөр өмнө

+ 119 - 0
README_ZH.md

@@ -0,0 +1,119 @@
+# AuraK:企业级全栈智能 AI 知识平台
+
+AuraK 是一个基于 **React 19** 与 **NestJS** 构建的现代化企业级 AI 知识库与人才评价系统。它不仅提供了高度可扩展的 RAG(检索增强生成)能力,还深度集成了多租户管理、交互式评价工作流及飞书办公生态。
+
+---
+
+## ✨ 核心特性
+
+### 🔐 企业级多租户与权限
+- **租户隔离**:严格的数据与资源租户级物理隔离,支持独立域名/子域名挂载。
+- **RBAC 权限管理**:预置超级管理员、租户管理员、普通用户等多种角色。
+- **成员管理**:支持租户内成员邀请、权限分配与配额限制。
+
+### 📚 智能知识路由与管理
+- **层级化分组**:支持知识库文件的文件夹式层级管理(Knowledge Groups),轻松应对海量文档。
+- **双模式处理流水线**:
+    - **快速模式 (Fast)**:基于 Apache Tika,极速提取海量纯文本。
+    - **高精度模式 (High-Precision)**:集成了 **Vision Pipeline**,利用多模态模型识别复杂 PDF/图片中的图文混合内容。
+- **格式全支持**:原生支持 PDF, Word, PPT, Excel, TXT, Markdown 以及各类图片格式。
+
+### 📊 交互式人才评价 (Assessment)
+- **LangGraph 工作流**:基于图结构的 AI 对话逻辑,实现逻辑严密的自动化面试与素质评价。
+- **落地式出题 (Grounded Q&A)**:基于 RAG 技术,从自有知识库中根据关键词精准提取素材生成专业题目。
+- **加权智能评分**:支持 Standard (1.0), Advanced (1.5), Specialist (2.0) 三级难度权重的自动化综合评分。
+- **多语言评价**:支持中、英、日三语同步测评。
+
+### 🤖 深度飞书办公集成
+- **免公网 WebSocket 机器人**:支持通过飞书长连接(WebSocket)直接接入企业内网,无需公网 IP 或域名映射。
+- **互动消息卡片**:在飞书中实时展示 AI 思考过程、检索来源及测评进度。
+- **移动端评价**:用户可直接在飞书聊天窗口完成完整的人才评价流程。
+
+### 🚀 高级 RAG 性能优化
+- **混合检索 (Hybrid Search)**:结合 Elasticsearch 的 BM25 关键词检索与高维度向量检索,大幅提升首选片段准确率。
+- **智能重排序 (Rerank)**:内置 Rerank 模型二次校验,确保生成内容的真实性与相关性。
+- **SSE 流式响应**:秒级首屏响应,实时展示知识检索状态与生成进度。
+
+### 🛠️ 生产力增强工具
+- **播客生成 (Podcasts)**:一键将长文档转化为播客形式的音频摘要。
+- **智能笔记 (Notes)**:支持对知识库内容记录分类笔记。
+- **搜索历史溯源**:完整的聊天历史记录与引用文档回溯。
+
+---
+
+## 🏗️ 技术架构
+
+### 前端 (Web)
+- **核心**:React 19 + TypeScript + Vite
+- **UI/样式**:Tailwind CSS + Lucide React
+- **交互**:React Context + SSE Streaming + Framer Motion (微动画)
+
+### 后端 (Server)
+- **框架**:NestJS (Node.js) + TypeScript
+- **AI 引擎**:LangChain + **LangGraph** (评价工作流)
+- **存储**:SQLite (元数据) + **Elasticsearch** (向量与全文检索)
+- **处理层**:Apache Tika + Vision Pipeline + LibreOffice (文档转换)
+- **通信**:Feishu WebSocket Manager + SSE
+
+---
+
+## 🏢 内网部署支持
+
+AuraK 专为私有化部署设计:
+- **资源本地化**:KaTeX、字体等静态资源完全本地化,无需访问 CDN。
+- **私有模型接入**:支持接入各类 OpenAI 兼容格式的内网私有化模型服务。
+- **容器化部署**:提供完整的 Docker Compose 一键启动方案,支持私有镜像仓库。
+
+详细指南请参考 [内网部署手册](INTERNAL_DEPLOYMENT_GUIDE.md)。
+
+---
+
+## 🚀 快速开始
+
+### 1. 准备工作
+- Node.js 18+
+- Yarn
+- Docker & Docker Compose
+
+### 2. 克隆与安装
+```bash
+git clone <repository-url>
+cd auraAuraK
+yarn install
+```
+
+### 3. 启动周边服务
+```bash
+docker-compose up -d elasticsearch tika libreoffice
+```
+
+### 4. 环境配置
+分别修改 `server/.env` 和 `web/.env`。
+
+### 5. 启动项目
+```bash
+yarn dev
+```
+访问 `http://localhost:5173` 开始体验!
+
+---
+
+## 📁 项目目录
+```
+auraAuraK/
+├── web/                    # 前端 React 应用
+├── server/                 # 后端 NestJS 应用
+│   ├── src/
+│   │   ├── tenant/         # 多租户管理
+│   │   ├── assessment/     # 合才评价 (LangGraph)
+│   │   ├── feishu/         # 飞书集成
+│   │   ├── knowledge-group/# 知识库分组
+│   │   └── chat/           # RAG 核心逻辑
+├── docs/                   # 技术方案与 API 文档
+└── docker-compose.yml      # 全栈部署配置
+```
+
+---
+
+## 📄 开源协议
+本项目采用 MIT 协议。详见 [LICENSE](LICENSE) 文件。

+ 831 - 0
docs/military-simulation-ai-solution.html

@@ -0,0 +1,831 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>军事仿真 AI 原生解决方案</title>
+  <style>
+    :root {
+      --bg: #0f1117;
+      --surface: #1a1d27;
+      --surface2: #232736;
+      --border: #2e3348;
+      --text: #e2e4ed;
+      --text-dim: #8b8fa3;
+      --accent: #6c8cff;
+      --accent2: #a78bfa;
+      --green: #34d399;
+      --orange: #fb923c;
+      --red: #f87171;
+      --cyan: #22d3ee;
+      --yellow: #fbbf24;
+    }
+    * { margin: 0; padding: 0; box-sizing: border-box; }
+    body {
+      font-family: -apple-system, "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif;
+      background: var(--bg);
+      color: var(--text);
+      line-height: 1.7;
+      padding: 0;
+    }
+
+    /* Header */
+    .hero {
+      background: linear-gradient(135deg, #1e1b4b 0%, #0f172a 50%, #0c1220 100%);
+      border-bottom: 1px solid var(--border);
+      padding: 80px 40px 60px;
+      text-align: center;
+      position: relative;
+      overflow: hidden;
+    }
+    .hero::before {
+      content: '';
+      position: absolute;
+      top: -50%;
+      left: -50%;
+      width: 200%;
+      height: 200%;
+      background: radial-gradient(circle at 30% 50%, rgba(108,140,255,0.08) 0%, transparent 50%),
+                  radial-gradient(circle at 70% 50%, rgba(167,139,250,0.06) 0%, transparent 50%);
+    }
+    .hero h1 {
+      font-size: 2.6em;
+      font-weight: 700;
+      background: linear-gradient(135deg, var(--accent), var(--accent2));
+      -webkit-background-clip: text;
+      -webkit-text-fill-color: transparent;
+      position: relative;
+      margin-bottom: 12px;
+    }
+    .hero .subtitle {
+      color: var(--text-dim);
+      font-size: 1.15em;
+      position: relative;
+    }
+
+    /* Layout */
+    .container {
+      max-width: 1200px;
+      margin: 0 auto;
+      padding: 0 32px;
+    }
+
+    /* Pipeline overview */
+    .pipeline {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 0;
+      padding: 48px 0 24px;
+      flex-wrap: wrap;
+    }
+    .pipeline-node {
+      background: var(--surface);
+      border: 1px solid var(--border);
+      border-radius: 12px;
+      padding: 18px 28px;
+      text-align: center;
+      min-width: 140px;
+      transition: border-color 0.2s;
+    }
+    .pipeline-node:hover { border-color: var(--accent); }
+    .pipeline-node .step-num {
+      display: inline-block;
+      width: 28px; height: 28px;
+      border-radius: 50%;
+      background: var(--accent);
+      color: #fff;
+      font-size: 0.8em;
+      font-weight: 700;
+      line-height: 28px;
+      margin-bottom: 6px;
+    }
+    .pipeline-node .label { font-weight: 600; font-size: 1em; }
+    .pipeline-node .sub { color: var(--text-dim); font-size: 0.82em; }
+    .pipeline-arrow {
+      color: var(--text-dim);
+      font-size: 1.4em;
+      padding: 0 8px;
+      user-select: none;
+    }
+
+    /* Sections */
+    section {
+      margin: 48px 0;
+    }
+    .section-header {
+      display: flex;
+      align-items: center;
+      gap: 14px;
+      margin-bottom: 24px;
+      padding-bottom: 12px;
+      border-bottom: 1px solid var(--border);
+    }
+    .section-header .icon {
+      width: 42px; height: 42px;
+      border-radius: 10px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 1.3em;
+      flex-shrink: 0;
+    }
+    .section-header h2 {
+      font-size: 1.55em;
+      font-weight: 700;
+    }
+    .section-header .tag {
+      font-size: 0.78em;
+      padding: 3px 10px;
+      border-radius: 20px;
+      font-weight: 600;
+    }
+
+    /* Cards grid */
+    .cards {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
+      gap: 16px;
+    }
+    .card {
+      background: var(--surface);
+      border: 1px solid var(--border);
+      border-radius: 12px;
+      padding: 22px;
+      transition: border-color 0.2s, transform 0.15s;
+    }
+    .card:hover {
+      border-color: var(--accent);
+      transform: translateY(-2px);
+    }
+    .card h4 {
+      font-size: 0.95em;
+      margin-bottom: 6px;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+    }
+    .card p {
+      color: var(--text-dim);
+      font-size: 0.88em;
+      line-height: 1.6;
+    }
+
+    /* Diagram box */
+    .diagram-box {
+      background: var(--surface2);
+      border: 1px solid var(--border);
+      border-radius: 12px;
+      padding: 32px;
+      margin-top: 20px;
+      overflow-x: auto;
+    }
+    .diagram-box pre {
+      font-family: "JetBrains Mono", "Fira Code", "Cascadia Code", monospace;
+      font-size: 0.82em;
+      line-height: 1.65;
+      color: var(--cyan);
+      white-space: pre;
+      margin: 0;
+    }
+
+    /* Table */
+    .data-table {
+      width: 100%;
+      border-collapse: collapse;
+      margin-top: 16px;
+    }
+    .data-table th {
+      text-align: left;
+      padding: 12px 16px;
+      background: var(--surface2);
+      border-bottom: 1px solid var(--border);
+      font-size: 0.85em;
+      color: var(--text-dim);
+      font-weight: 600;
+      text-transform: uppercase;
+      letter-spacing: 0.5px;
+    }
+    .data-table td {
+      padding: 12px 16px;
+      border-bottom: 1px solid var(--border);
+      font-size: 0.9em;
+    }
+    .data-table tr:hover td { background: rgba(108,140,255,0.04); }
+
+    /* Tags */
+    .tag-blue { background: rgba(108,140,255,0.15); color: var(--accent); }
+    .tag-purple { background: rgba(167,139,250,0.15); color: var(--accent2); }
+    .tag-green { background: rgba(52,211,153,0.15); color: var(--green); }
+    .tag-orange { background: rgba(251,146,60,0.15); color: var(--orange); }
+    .tag-cyan { background: rgba(34,211,238,0.15); color: var(--cyan); }
+    .tag-yellow { background: rgba(251,191,36,0.15); color: var(--yellow); }
+
+    .icon-blue { background: rgba(108,140,255,0.12); }
+    .icon-purple { background: rgba(167,139,250,0.12); }
+    .icon-green { background: rgba(52,211,153,0.12); }
+    .icon-orange { background: rgba(251,146,60,0.12); }
+    .icon-cyan { background: rgba(34,211,238,0.12); }
+    .icon-yellow { background: rgba(251,191,36,0.12); }
+    .icon-red { background: rgba(248,113,113,0.12); }
+
+    /* Code block */
+    code {
+      font-family: "JetBrains Mono", "Fira Code", monospace;
+      background: var(--surface2);
+      padding: 2px 7px;
+      border-radius: 4px;
+      font-size: 0.88em;
+      color: var(--cyan);
+    }
+
+    /* Callout */
+    .callout {
+      background: rgba(108,140,255,0.06);
+      border-left: 3px solid var(--accent);
+      border-radius: 0 10px 10px 0;
+      padding: 20px 24px;
+      margin-top: 20px;
+    }
+    .callout strong { color: var(--accent); }
+    .callout p { color: var(--text-dim); font-size: 0.92em; margin-top: 6px; }
+
+    /* Roadmap */
+    .roadmap {
+      display: grid;
+      grid-template-columns: repeat(3, 1fr);
+      gap: 20px;
+      margin-top: 16px;
+    }
+    .roadmap-item {
+      background: var(--surface);
+      border: 1px solid var(--border);
+      border-radius: 12px;
+      padding: 24px;
+      position: relative;
+      overflow: hidden;
+    }
+    .roadmap-item::before {
+      content: attr(data-phase);
+      position: absolute;
+      top: 0; left: 0; right: 0;
+      height: 3px;
+    }
+    .roadmap-item h4 {
+      font-size: 1.05em;
+      margin-bottom: 8px;
+    }
+    .roadmap-item ul {
+      list-style: none;
+      padding: 0;
+    }
+    .roadmap-item li {
+      color: var(--text-dim);
+      font-size: 0.88em;
+      padding: 3px 0;
+      padding-left: 16px;
+      position: relative;
+    }
+    .roadmap-item li::before {
+      content: '›';
+      position: absolute;
+      left: 0;
+      color: var(--accent);
+      font-weight: 700;
+    }
+
+    /* Flow diagram with SVG-like CSS */
+    .flow-section {
+      background: var(--surface);
+      border: 1px solid var(--border);
+      border-radius: 12px;
+      padding: 28px;
+      margin-top: 16px;
+    }
+    .flow-row {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      gap: 12px;
+      flex-wrap: wrap;
+    }
+    .flow-box {
+      border-radius: 10px;
+      padding: 14px 22px;
+      text-align: center;
+      font-size: 0.88em;
+      font-weight: 600;
+      min-width: 120px;
+      border: 1px solid var(--border);
+    }
+    .flow-arrow {
+      color: var(--text-dim);
+      font-size: 1.2em;
+    }
+
+    /* Footer */
+    .footer {
+      text-align: center;
+      padding: 40px;
+      color: var(--text-dim);
+      font-size: 0.82em;
+      border-top: 1px solid var(--border);
+      margin-top: 60px;
+    }
+
+    /* Responsive */
+    @media (max-width: 768px) {
+      .hero { padding: 48px 20px 36px; }
+      .hero h1 { font-size: 1.8em; }
+      .container { padding: 0 16px; }
+      .pipeline { gap: 4px; }
+      .pipeline-node { min-width: 100px; padding: 12px 14px; }
+      .pipeline-arrow { font-size: 1em; padding: 0 4px; }
+      .roadmap { grid-template-columns: 1fr; }
+      .diagram-box pre { font-size: 0.72em; }
+    }
+  </style>
+</head>
+<body>
+
+  <!-- Hero -->
+  <div class="hero">
+    <h1>军事仿真 AI 原生解决方案</h1>
+    <div class="subtitle">场景 → 想定 → 方案 → 推演 → 评估 · 端到端闭环智能体系</div>
+  </div>
+
+  <div class="container">
+
+    <!-- Pipeline Overview -->
+    <div class="pipeline">
+      <div class="pipeline-node">
+        <div class="step-num">1</div>
+        <div class="label">场景构建</div>
+        <div class="sub">Scene Construction</div>
+      </div>
+      <span class="pipeline-arrow">→</span>
+      <div class="pipeline-node">
+        <div class="step-num">2</div>
+        <div class="label">想定生成</div>
+        <div class="sub">Wargaming Scenario</div>
+      </div>
+      <span class="pipeline-arrow">→</span>
+      <div class="pipeline-node">
+        <div class="step-num">3</div>
+        <div class="label">方案生成</div>
+        <div class="sub">Plan Generation</div>
+      </div>
+      <span class="pipeline-arrow">→</span>
+      <div class="pipeline-node">
+        <div class="step-num">4</div>
+        <div class="label">仿真推演</div>
+        <div class="sub">Simulation Run</div>
+      </div>
+      <span class="pipeline-arrow">→</span>
+      <div class="pipeline-node">
+        <div class="step-num">5</div>
+        <div class="label">仿真评估</div>
+        <div class="sub">Assessment</div>
+      </div>
+    </div>
+
+    <!-- ========== 1. 场景构建 ========== -->
+    <section>
+      <div class="section-header">
+        <div class="icon icon-blue"> </div>
+        <h2>一、场景构建</h2>
+        <span class="tag tag-blue">Scene Construction</span>
+      </div>
+      <p style="color:var(--text-dim);margin-bottom:16px;">构建战场环境,涵盖地理、气象、电磁、基础设施等多维态势。核心目标:<strong>用自然语言快速生成结构化战场场景</strong>。</p>
+
+      <div class="cards">
+        <div class="card">
+          <h4><span class="tag tag-blue">NLP</span> 自然语言 → 场景</h4>
+          <p>LLM 将 "红蓝双方在东海某海域对峙,气象条件 XX" 解析为结构化场景参数 (JSON / DSL)</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-purple">Vision</span> 地理环境生成</h4>
+          <p>多模态模型从卫星图 / 地图自动提取地形特征、道路网络、关键地标</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-green">RAG</span> 场景模板推荐</h4>
+          <p>基于历史场景库做 RAG 检索,推荐相似场景作为起点,降低构建成本</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-orange">Validate</span> 一致性校验</h4>
+          <p>LLM + 规则引擎联合检查参数矛盾,如海拔与水深冲突、时间线逻辑错误</p>
+        </div>
+      </div>
+
+      <div class="diagram-box">
+<pre>
+  [自然语言输入]
+       │
+       ▼
+  [场景 DSL 生成器 (LLM)]
+       │
+       ▼
+  [场景参数 JSON Schema] ──→ [场景渲染引擎 (3D / GIS)]
+       │
+       ▼
+  [一致性校验 (规则 + LLM)]
+       │
+       ▼
+  [结构化场景定义]
+</pre>
+      </div>
+    </section>
+
+    <!-- ========== 2. 想定生成 ========== -->
+    <section>
+      <div class="section-header">
+        <div class="icon icon-purple">⚔️</div>
+        <h2>二、想定生成</h2>
+        <span class="tag tag-purple">Wargaming Scenario</span>
+      </div>
+      <p style="color:var(--text-dim);margin-bottom:16px;">在场景基础上定义双方作战目标、兵力编成、交战规则 (ROE)、时间线。核心创新:<strong>红蓝对抗博弈 Agent 自动迭代生成想定</strong>。</p>
+
+      <div class="cards">
+        <div class="card">
+          <h4><span class="tag tag-blue">Generate</span> 想定自动生成</h4>
+          <p>LLM 基于场景 + 作战意图,生成结构化想定:兵力部署、阶段划分、ROE 规则</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-red">Game</span> 对抗性博弈</h4>
+          <p>红蓝两个 Agent 各自独立生成意图,通过多轮对抗迭代完善想定</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-green">Review</span> 合理性评估</h4>
+          <p>战术知识库 RAG + LLM 审查,标注兵力不足、后勤断链等风险点</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-cyan">Version</span> 版本管理</h4>
+          <p>每次修改生成 diff,支持分支 / 合并,类 Git 的想定版本管理</p>
+        </div>
+      </div>
+
+      <div class="diagram-box">
+<pre>
+        场景描述 + 红方目标
+              │
+              ▼
+  ┌─────────────────────────┐
+  │    Red Agent (LLM)       │ ← 战术知识库 (RAG)
+  │   生成红方兵力部署方案     │
+  └───────────┬─────────────┘
+              ▼
+  ┌─────────────────────────┐
+  │    Blue Agent (LLM)      │ ← 战术知识库 (RAG)
+  │   针对红方部署制定对策     │
+  └───────────┬─────────────┘
+              ▼
+  ┌─────────────────────────┐
+  │    Referee Agent         │
+  │   平衡性审查 + 冲突标注    │
+  └───────────┬─────────────┘
+              ▼
+         最终想定文档
+</pre>
+      </div>
+    </section>
+
+    <!-- ========== 3. 方案生成 ========== -->
+    <section>
+      <div class="section-header">
+        <div class="icon icon-green"> </div>
+        <h2>三、方案生成</h2>
+        <span class="tag tag-green">Plan Generation</span>
+      </div>
+      <p style="color:var(--text-dim);margin-bottom:16px;">基于想定为各作战单元生成具体行动计划。核心价值:<strong>多方案并行生成 + 量化比较,突破人工只能覆盖少数方案的局限</strong>。</p>
+
+      <div class="cards">
+        <div class="card">
+          <h4><span class="tag tag-blue">Ensemble</span> 多方案并行</h4>
+          <p>LLM + 规划算法 (HTN / MCTS) 并行输出 3-5 个差异化方案,覆盖不同战术风格</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-yellow">Score</span> 多维评价打分</h4>
+          <p>风险、时效、资源消耗、达成概率等维度自动评分,附 LLM 解释</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-purple">Merge</span> 方案对比与融合</h4>
+          <p>LLM 对多方案做优劣势分析,可融合不同方案的优点生成复合方案</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-cyan">HITL</span> 人在回路修正</h4>
+          <p>指挥员用自然语言修改方案,LLM 实时调整并反馈级联影响</p>
+        </div>
+      </div>
+
+      <div class="diagram-box">
+<pre>
+  想定 + 约束条件
+       │
+       ▼
+  ┌────────────────────────────────────────┐
+  │          Plan Generator Ensemble        │
+  │  ┌──────────┐ ┌──────────┐ ┌─────────┐│
+  │  │ 方案 A    │ │ 方案 B    │ │ 方案 C   ││
+  │  │(激进突防) │ │(稳健推进) │ │(迂回包围)││
+  │  └─────┬────┘ └─────┬────┘ └────┬────┘│
+  │        └────────────┼───────────┘     │
+  │                     ▼                  │
+  │           Plan Evaluator               │
+  │      (多维度打分 + LLM 解释)            │
+  └─────────────────────┬──────────────────┘
+                        ▼
+           排序推荐 + 可视化对比
+</pre>
+      </div>
+
+      <div class="callout">
+        <strong>方案 DSL 示例</strong>
+        <p>每个方案以结构化 YAML 描述,包含阶段、兵力、行动指令,便于 LLM 生成、校验和推演引擎执行。</p>
+      </div>
+
+      <div class="diagram-box" style="margin-top:12px;">
+<pre>
+plan_id: PLAN-2026-0320-A
+name: "东海封锁作战方案 A — 激进突防"
+phases:
+  - phase: 1
+    name: "电子压制"
+    start_time: T+0
+    units: [EW-01, EW-02]
+    actions:
+      - type: jamming
+        target: enemy_radar_net
+        power: high
+  - phase: 2
+    name: "第一波突击"
+    start_time: T+00:30
+    units: [AIR-01, AIR-02, NAV-01]
+    actions:
+      - type: strike
+        target: port_facility_A
+        weapon: cruise_missile
+        quantity: 12
+</pre>
+      </div>
+    </section>
+
+    <!-- ========== 4. 仿真推演 ========== -->
+    <section>
+      <div class="section-header">
+        <div class="icon icon-orange"> </div>
+        <h2>四、仿真推演</h2>
+        <span class="tag tag-orange">Simulation Execution</span>
+      </div>
+      <p style="color:var(--text-dim);margin-bottom:16px;">按方案驱动各作战单元在仿真环境中执行,产生时序态势数据。核心创新:<strong>每个作战单元挂载 LLM Agent,自主感知-决策-行动</strong>。</p>
+
+      <div class="cards">
+        <div class="card">
+          <h4><span class="tag tag-blue">Agent</span> 智能体行为决策</h4>
+          <p>每个作战单元挂载 LLM Agent,根据实时态势自主决策,非脚本化行为树</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-green">Adaptive</span> 自适应仿真步长</h4>
+          <p>战斗激烈区域用小步长,平静区域用大步长,AI 动态调度提升仿真效率</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-yellow">Inject</span> 意外事件注入</h4>
+          <p>LLM 作为 "战局导演",在关键节点注入意外事件,考验方案鲁棒性</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-cyan">Monitor</span> 实时态势理解</h4>
+          <p>推演过程中 LLM 持续解读态势,自动生成态势报告与异常告警</p>
+        </div>
+      </div>
+
+      <div class="diagram-box">
+<pre>
+  ┌──────────────────────────────────────────────────┐
+  │               Simulation Engine                   │
+  │                                                    │
+  │   ┌────────────┐  ┌────────────┐  ┌────────────┐ │
+  │   │ Unit Agent  │  │ Unit Agent  │  │ Unit Agent  ││
+  │   │  (驱逐舰)   │  │  (战斗机)   │  │  (雷达站)   ││
+  │   │            │  │            │  │            │ │
+  │   │  观测→推理  │  │  观测→推理  │  │  观测→推理  │ │
+  │   │  →决策→行动 │  │  →决策→行动 │  │  →决策→行动 │ │
+  │   └──────┬─────┘  └──────┬─────┘  └──────┬─────┘ │
+  │          └───────────────┼───────────────┘        │
+  │                          ▼                         │
+  │               态势融合与冲突仲裁                      │
+  │                          ▼                         │
+  │               仿真时间推进                           │
+  └──────────────────────────┬─────────────────────────┘
+                             ▼
+                  时序态势数据流 (SSE)
+</pre>
+      </div>
+
+      <div class="callout">
+        <strong>Unit Agent 决策循环</strong>
+        <p>每个 Agent 遵循 <code>观测 (Observe) → 推理 (Reason) → 决策 (Decide) → 行动 (Act)</code> 循环,输出可审计的决策链,支持事后复盘分析。</p>
+      </div>
+
+      <div class="diagram-box" style="margin-top:12px;">
+<pre>
+UNIT_AGENT_SYSTEM_PROMPT = """
+  你是 {unit_type} {unit_id} 的指挥 AI。
+  当前态势:{current_situation}
+  你的任务:{mission_objective}
+  ROE 规则:{rules_of_engagement}
+  可用武器/资源:{available_resources}
+
+  输出格式:
+    1. 态势判断 — 你观察到了什么
+    2. 决策依据 — 为什么这么做
+    3. 行动指令 — 具体做什么
+    4. 预期结果 — 预计产生什么效果
+"""
+</pre>
+      </div>
+    </section>
+
+    <!-- ========== 5. 仿真评估 ========== -->
+    <section>
+      <div class="section-header">
+        <div class="icon icon-yellow"> </div>
+        <h2>五、仿真评估</h2>
+        <span class="tag tag-yellow">Assessment</span>
+      </div>
+      <p style="color:var(--text-dim);margin-bottom:16px;">对推演结果进行多维度评估,输出报告,支持方案迭代优化。核心价值:<strong>LLM 自动将海量推演数据转化为可读评估报告 + 可执行的改进建议</strong>。</p>
+
+      <div class="cards">
+        <div class="card">
+          <h4><span class="tag tag-blue">Metrics</span> 多维自动评估</h4>
+          <p>从推演数据自动计算战损比、时间达成率、资源效率、态势控制面积等指标</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-green">Report</span> 自然语言报告</h4>
+          <p>LLM 将指标数据转化为结构化评估报告,含关键节点叙事与图表引用</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-purple">Root Cause</span> 根因分析</h4>
+          <p>LLM 分析关键转折点,构建因果链,追溯胜负根本原因</p>
+        </div>
+        <div class="card">
+          <h4><span class="tag tag-orange">Loop</span> 方案迭代闭环</h4>
+          <p>基于评估结果自动提出改进建议,闭环反馈至方案生成阶段</p>
+        </div>
+      </div>
+
+      <div class="diagram-box">
+<pre>
+  推演时序数据
+       │
+       ▼
+  ┌─────────────────────────┐
+  │   评估指标计算引擎        │
+  │  (战损 / 时间 / 资源 态势)│
+  └───────────┬─────────────┘
+              ▼
+  ┌─────────────────────────┐
+  │   LLM 评估报告生成器     │
+  │   指标 → 叙事 → 图表 → 建议│
+  └───────────┬─────────────┘
+              ▼
+  ┌─────────────────────────┐
+  │   根因分析 Agent         │
+  │   关键节点回溯 + 因果链   │
+  └───────────┬─────────────┘
+              ▼
+  ┌─────────────────────────┐
+  │   方案优化建议           │
+  │   → 反馈至方案生成阶段    │
+  └─────────────────────────┘
+</pre>
+      </div>
+    </section>
+
+    <!-- ========== 端到端架构 ========== -->
+    <section>
+      <div class="section-header">
+        <div class="icon icon-cyan"> </div>
+        <h2>六、端到端闭环架构</h2>
+        <span class="tag tag-cyan">Full Loop</span>
+      </div>
+
+      <div class="flow-section">
+        <div class="flow-row" style="margin-bottom:20px;">
+          <div class="flow-box" style="border-color:var(--accent);background:rgba(108,140,255,0.08);">场景构建<br><small style="color:var(--text-dim)">LLM 解析 + RAG 模板</small></div>
+          <span class="flow-arrow">→</span>
+          <div class="flow-box" style="border-color:var(--accent2);background:rgba(167,139,250,0.08);">想定生成<br><small style="color:var(--text-dim)">对抗博弈 Agent</small></div>
+          <span class="flow-arrow">→</span>
+          <div class="flow-box" style="border-color:var(--green);background:rgba(52,211,153,0.08);">方案生成<br><small style="color:var(--text-dim)">多方案并行生成</small></div>
+          <span class="flow-arrow">→</span>
+          <div class="flow-box" style="border-color:var(--orange);background:rgba(251,146,60,0.08);">仿真推演<br><small style="color:var(--text-dim)">Agent 化推演引擎</small></div>
+          <span class="flow-arrow">→</span>
+          <div class="flow-box" style="border-color:var(--yellow);background:rgba(251,191,36,0.08);">仿真评估<br><small style="color:var(--text-dim)">LLM 报告 + 根因分析</small></div>
+        </div>
+        <div style="text-align:center;color:var(--text-dim);font-size:0.88em;">
+          <span style="border:1px dashed var(--border);padding:6px 16px;border-radius:20px;">  闭环迭代:评估结果 → 优化建议 → 回到方案生成</span>
+        </div>
+      </div>
+
+      <div class="callout" style="margin-top:24px;">
+        <strong>核心设计哲学</strong>
+        <p>让 LLM 承担 <strong>"认知"</strong> 工作(理解、推理、生成、评估),传统算法承担 <strong>"计算"</strong> 工作(物理仿真、路径规划、优化求解),两者通过结构化 DSL 桥接。这一分层保证了 AI 的创造性与仿真的精确性互不干扰。</p>
+      </div>
+    </section>
+
+    <!-- ========== 技术选型 ========== -->
+    <section>
+      <div class="section-header">
+        <div class="icon icon-green"> </div>
+        <h2>七、关键技术选型</h2>
+        <span class="tag tag-green">Tech Stack</span>
+      </div>
+
+      <table class="data-table">
+        <thead>
+          <tr>
+            <th>层级</th>
+            <th>技术</th>
+            <th>说明</th>
+          </tr>
+        </thead>
+        <tbody>
+          <tr>
+            <td><span class="tag tag-blue">LLM 编排</span></td>
+            <td>LangGraph / 自研 Agent 框架</td>
+            <td>管理多 Agent 协作、状态流转、对话历史</td>
+          </tr>
+          <tr>
+            <td><span class="tag tag-purple">知识库</span></td>
+            <td>Elasticsearch + 向量检索</td>
+            <td>作战条令、战术案例、历史战例的 RAG 索引</td>
+          </tr>
+          <tr>
+            <td><span class="tag tag-orange">仿真引擎</span></td>
+            <td>离散事件仿真 (DES) + Agent 调度</td>
+            <td>战场事件驱动,Agent 自主决策,支持多时间尺度</td>
+          </tr>
+          <tr>
+            <td><span class="tag tag-green">态势存储</span></td>
+            <td>时序 DB (InfluxDB / TDengine)</td>
+            <td>高频态势数据写入与时间范围查询</td>
+          </tr>
+          <tr>
+            <td><span class="tag tag-cyan">可视化</span></td>
+            <td>Cesium (3D GIS) + WebSocket</td>
+            <td>实时态势可视化,支持 2D/3D 切换</td>
+          </tr>
+          <tr>
+            <td><span class="tag tag-yellow">方案 DSL</span></td>
+            <td>YAML / JSON Schema</td>
+            <td>结构化方案描述,便于 LLM 生成、校验和引擎执行</td>
+          </tr>
+        </tbody>
+      </table>
+    </section>
+
+    <!-- ========== 落地路径 ========== -->
+    <section>
+      <div class="section-header">
+        <div class="icon icon-red"> </div>
+        <h2>八、落地路径</h2>
+        <span class="tag tag-orange">Roadmap</span>
+      </div>
+
+      <div class="roadmap">
+        <div class="roadmap-item" data-phase="" style="border-top: 3px solid var(--green);">
+          <h4 style="color:var(--green);">Phase 1 · MVP</h4>
+          <ul>
+            <li>跑通场景→想定→单方案→简单推演→评估的最小闭环</li>
+            <li>LLM 做自然语言到 DSL 的转换</li>
+            <li>基础知识库 RAG 索引(作战条令)</li>
+            <li>脚本化仿真引擎 + 基础指标评估</li>
+          </ul>
+        </div>
+        <div class="roadmap-item" data-phase="" style="border-top: 3px solid var(--accent);">
+          <h4 style="color:var(--accent);">Phase 2 · 增强</h4>
+          <ul>
+            <li>引入红蓝对抗博弈 Agent</li>
+            <li>多方案并行生成 + 量化对比</li>
+            <li>Agent 化推演引擎(自主决策)</li>
+            <li>实时态势可视化 + SSE 推送</li>
+          </ul>
+        </div>
+        <div class="roadmap-item" data-phase="" style="border-top: 3px solid var(--accent2);">
+          <h4 style="color:var(--accent2);">Phase 3 · 成熟</h4>
+          <ul>
+            <li>自适应仿真步长 + 异常检测</li>
+            <li>方案自动迭代优化(评估→改进闭环)</li>
+            <li>跨域多兵种联合作战仿真</li>
+            <li>历史战例深度挖掘与战术知识图谱</li>
+          </ul>
+        </div>
+      </div>
+    </section>
+
+  </div>
+
+  <div class="footer">
+    军事仿真 AI 原生解决方案 · 2026-03-20
+  </div>
+
+</body>
+</html>

+ 779 - 0
docs/system-overview-zh.html

@@ -0,0 +1,779 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Simple Knowledge Base - 系统概览</title>
+  <style>
+    :root {
+      --primary: #6366f1;
+      --primary-light: #818cf8;
+      --primary-dark: #4f46e5;
+      --bg: #0f172a;
+      --bg-card: #1e293b;
+      --bg-code: #0d1117;
+      --text: #e2e8f0;
+      --text-muted: #94a3b8;
+      --border: #334155;
+      --accent-green: #34d399;
+      --accent-blue: #38bdf8;
+      --accent-orange: #fb923c;
+      --accent-pink: #f472b6;
+    }
+
+    * { margin: 0; padding: 0; box-sizing: border-box; }
+
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans SC', 'PingFang SC', 'Microsoft YaHei', sans-serif;
+      background: var(--bg);
+      color: var(--text);
+      line-height: 1.8;
+    }
+
+    .hero {
+      background: linear-gradient(135deg, #1e1b4b 0%, #312e81 30%, #0f172a 70%, #0c0a09 100%);
+      padding: 80px 20px 60px;
+      text-align: center;
+      position: relative;
+      overflow: hidden;
+    }
+
+    .hero::before {
+      content: '';
+      position: absolute;
+      top: -50%;
+      left: -50%;
+      width: 200%;
+      height: 200%;
+      background: radial-gradient(circle at 30% 50%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
+                  radial-gradient(circle at 70% 50%, rgba(56, 189, 248, 0.1) 0%, transparent 50%);
+      animation: pulse 8s ease-in-out infinite;
+    }
+
+    @keyframes pulse {
+      0%, 100% { opacity: 0.6; }
+      50% { opacity: 1; }
+    }
+
+    .hero h1 {
+      font-size: 2.8rem;
+      font-weight: 800;
+      background: linear-gradient(135deg, #c7d2fe, #818cf8, #38bdf8);
+      -webkit-background-clip: text;
+      -webkit-text-fill-color: transparent;
+      background-clip: text;
+      position: relative;
+      margin-bottom: 12px;
+    }
+
+    .hero .subtitle {
+      font-size: 1.15rem;
+      color: var(--text-muted);
+      position: relative;
+      max-width: 700px;
+      margin: 0 auto;
+    }
+
+    .badge-row {
+      display: flex;
+      gap: 10px;
+      justify-content: center;
+      flex-wrap: wrap;
+      margin-top: 24px;
+      position: relative;
+    }
+
+    .badge {
+      display: inline-flex;
+      align-items: center;
+      gap: 6px;
+      padding: 5px 14px;
+      border-radius: 999px;
+      font-size: 0.82rem;
+      font-weight: 600;
+      border: 1px solid;
+    }
+
+    .badge.react { color: #61dafb; border-color: #61dafb33; background: #61dafb10; }
+    .badge.nest { color: #e0234e; border-color: #e0234e33; background: #e0234e10; }
+    .badge.rag { color: var(--accent-green); border-color: #34d39933; background: #34d39910; }
+    .badge.ts { color: #3178c6; border-color: #3178c633; background: #3178c610; }
+
+    .container {
+      max-width: 1100px;
+      margin: 0 auto;
+      padding: 0 20px;
+    }
+
+    .features-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+      gap: 20px;
+      margin: -40px auto 60px;
+      position: relative;
+      z-index: 1;
+    }
+
+    .feature-card {
+      background: var(--bg-card);
+      border: 1px solid var(--border);
+      border-radius: 16px;
+      padding: 28px;
+      transition: transform 0.2s, border-color 0.2s;
+    }
+
+    .feature-card:hover {
+      transform: translateY(-3px);
+      border-color: var(--primary);
+    }
+
+    .feature-card .icon {
+      width: 44px;
+      height: 44px;
+      border-radius: 12px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 1.3rem;
+      margin-bottom: 16px;
+    }
+
+    .feature-card h3 {
+      font-size: 1.05rem;
+      margin-bottom: 8px;
+      color: #f1f5f9;
+    }
+
+    .feature-card p {
+      font-size: 0.9rem;
+      color: var(--text-muted);
+    }
+
+    .icon-blue { background: #38bdf818; }
+    .icon-green { background: #34d39918; }
+    .icon-orange { background: #fb923c18; }
+    .icon-pink { background: #f472b618; }
+    .icon-purple { background: #a78bfa18; }
+    .icon-yellow { background: #fbbf2418; }
+
+    section {
+      margin-bottom: 56px;
+    }
+
+    .section-title {
+      font-size: 1.5rem;
+      font-weight: 700;
+      margin-bottom: 24px;
+      padding-bottom: 12px;
+      border-bottom: 2px solid var(--border);
+      display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+
+    .section-title .num {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 32px;
+      height: 32px;
+      border-radius: 8px;
+      background: var(--primary);
+      font-size: 0.85rem;
+      font-weight: 700;
+      flex-shrink: 0;
+    }
+
+    /* Architecture Diagram */
+    .arch-diagram {
+      background: var(--bg-card);
+      border: 1px solid var(--border);
+      border-radius: 16px;
+      padding: 32px;
+      overflow-x: auto;
+    }
+
+    .arch-row {
+      display: flex;
+      justify-content: center;
+      gap: 16px;
+      margin-bottom: 16px;
+      flex-wrap: wrap;
+    }
+
+    .arch-box {
+      padding: 14px 22px;
+      border-radius: 10px;
+      font-size: 0.85rem;
+      font-weight: 600;
+      text-align: center;
+      min-width: 140px;
+      border: 1px solid;
+    }
+
+    .arch-box.frontend { background: #61dafb12; border-color: #61dafb33; color: #61dafb; }
+    .arch-box.backend { background: #e0234e12; border-color: #e0234e33; color: #e0234e; }
+    .arch-box.infra { background: #34d39912; border-color: #34d39933; color: #34d399; }
+    .arch-box.ai { background: #a78bfa12; border-color: #a78bfa33; color: #a78bfa; }
+    .arch-box.data { background: #fb923c12; border-color: #fb923c33; color: #fb923c; }
+
+    .arch-arrow {
+      text-align: center;
+      color: var(--text-muted);
+      font-size: 1.2rem;
+      margin-bottom: 16px;
+    }
+
+    .arch-label {
+      text-align: center;
+      font-size: 0.78rem;
+      color: var(--text-muted);
+      letter-spacing: 1.5px;
+      margin-bottom: 10px;
+    }
+
+    /* File Tree */
+    .file-tree {
+      background: var(--bg-code);
+      border: 1px solid var(--border);
+      border-radius: 12px;
+      padding: 24px 28px;
+      font-family: 'Cascadia Code', 'Fira Code', monospace;
+      font-size: 0.85rem;
+      line-height: 1.9;
+      overflow-x: auto;
+    }
+
+    .file-tree .dir { color: var(--accent-blue); font-weight: 600; }
+    .file-tree .comment { color: #64748b; font-style: italic; }
+    .file-tree .file { color: var(--text); }
+
+    /* Pipeline */
+    .pipeline {
+      display: flex;
+      align-items: center;
+      gap: 0;
+      flex-wrap: wrap;
+      justify-content: center;
+      margin: 20px 0;
+    }
+
+    .pipeline-step {
+      padding: 12px 20px;
+      border-radius: 10px;
+      font-size: 0.85rem;
+      font-weight: 600;
+      text-align: center;
+      border: 1px solid var(--border);
+      background: var(--bg-card);
+    }
+
+    .pipeline-arrow {
+      color: var(--text-muted);
+      font-size: 1.1rem;
+      padding: 0 6px;
+    }
+
+    .pipeline-step.active {
+      border-color: var(--primary);
+      background: #6366f118;
+      color: var(--primary-light);
+    }
+
+    /* Tables */
+    .info-table {
+      width: 100%;
+      border-collapse: collapse;
+      margin: 12px 0;
+    }
+
+    .info-table th, .info-table td {
+      padding: 10px 16px;
+      text-align: left;
+      border-bottom: 1px solid var(--border);
+      font-size: 0.9rem;
+    }
+
+    .info-table th {
+      color: var(--text-muted);
+      font-weight: 600;
+      font-size: 0.82rem;
+      letter-spacing: 0.5px;
+    }
+
+    .info-table .port {
+      font-family: 'Cascadia Code', monospace;
+      color: var(--accent-orange);
+      font-weight: 600;
+    }
+
+    .info-table .env-key {
+      font-family: 'Cascadia Code', monospace;
+      color: var(--accent-green);
+      font-size: 0.83rem;
+    }
+
+    /* Code Block */
+    .code-block {
+      background: var(--bg-code);
+      border: 1px solid var(--border);
+      border-radius: 10px;
+      padding: 18px 22px;
+      font-family: 'Cascadia Code', 'Fira Code', monospace;
+      font-size: 0.83rem;
+      line-height: 1.8;
+      overflow-x: auto;
+      margin: 12px 0;
+    }
+
+    .code-block .cmd { color: var(--accent-green); }
+    .code-block .comment { color: #64748b; }
+
+    /* Two Column Layout */
+    .two-col {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 24px;
+    }
+
+    @media (max-width: 768px) {
+      .two-col { grid-template-columns: 1fr; }
+      .hero h1 { font-size: 2rem; }
+      .features-grid { grid-template-columns: 1fr; }
+    }
+
+    .card {
+      background: var(--bg-card);
+      border: 1px solid var(--border);
+      border-radius: 14px;
+      padding: 24px;
+    }
+
+    .card h4 {
+      font-size: 1rem;
+      margin-bottom: 14px;
+      color: #f1f5f9;
+    }
+
+    .card ul {
+      list-style: none;
+      padding: 0;
+    }
+
+    .card ul li {
+      padding: 6px 0;
+      font-size: 0.88rem;
+      color: var(--text-muted);
+      display: flex;
+      align-items: flex-start;
+      gap: 8px;
+    }
+
+    .card ul li::before {
+      content: '›';
+      color: var(--primary-light);
+      font-weight: 700;
+      flex-shrink: 0;
+    }
+
+    .step-list {
+      list-style: none;
+      padding: 0;
+      counter-reset: step;
+    }
+
+    .step-list li {
+      counter-increment: step;
+      padding: 8px 0;
+      font-size: 0.88rem;
+      color: var(--text-muted);
+      display: flex;
+      align-items: flex-start;
+      gap: 10px;
+    }
+
+    .step-list li::before {
+      content: counter(step);
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 22px;
+      height: 22px;
+      border-radius: 6px;
+      background: var(--primary);
+      color: white;
+      font-size: 0.72rem;
+      font-weight: 700;
+      flex-shrink: 0;
+    }
+
+    .troubleshoot-item {
+      background: var(--bg-card);
+      border: 1px solid var(--border);
+      border-radius: 10px;
+      padding: 16px 20px;
+      margin-bottom: 10px;
+    }
+
+    .troubleshoot-item strong {
+      color: #f1f5f9;
+      font-size: 0.92rem;
+    }
+
+    .troubleshoot-item p {
+      color: var(--text-muted);
+      font-size: 0.85rem;
+      margin-top: 4px;
+    }
+
+    footer {
+      text-align: center;
+      padding: 40px 20px;
+      color: var(--text-muted);
+      font-size: 0.82rem;
+      border-top: 1px solid var(--border);
+    }
+  </style>
+</head>
+<body>
+
+  <!-- Hero -->
+  <div class="hero">
+    <h1>Simple Knowledge Base</h1>
+    <p class="subtitle">全栈 RAG 问答系统 &mdash; 基于 React 19 + NestJS 的检索增强生成技术</p>
+    <div class="badge-row">
+      <span class="badge react">React 19</span>
+      <span class="badge nest">NestJS</span>
+      <span class="badge rag">RAG 系统</span>
+      <span class="badge ts">TypeScript</span>
+    </div>
+  </div>
+
+  <div class="container">
+
+    <!-- 核心特性 -->
+    <div class="features-grid">
+      <div class="feature-card">
+        <div class="icon icon-purple"> </div>
+        <h3>多模型支持</h3>
+        <p>兼容 OpenAI API(OpenAI、DeepSeek、Claude 等)+ Google Gemini 原生 SDK,可配置 LLM、Embedding 和 Rerank 模型。</p>
+      </div>
+      <div class="feature-card">
+        <div class="icon icon-blue">⚡</div>
+        <h3>双处理模式</h3>
+        <p>快速模式通过 Apache Tika 提取文本,高精度模式通过视觉管线处理图文混合文档。</p>
+      </div>
+      <div class="feature-card">
+        <div class="icon icon-green"> </div>
+        <h3>混合检索</h3>
+        <p>基于 Elasticsearch 的向量 + 关键词混合搜索,支持来源引用、相似度评分,可配置分块大小与重叠。</p>
+      </div>
+      <div class="feature-card">
+        <div class="icon icon-orange"> </div>
+        <h3>用户隔离</h3>
+        <p>JWT 认证 + 每用户独立知识库,数据与配置完全隔离,保障隐私安全。</p>
+      </div>
+      <div class="feature-card">
+        <div class="icon icon-pink"> </div>
+        <h3>流式响应</h3>
+        <p>基于 Server-Sent Events (SSE) 的实时流式输出,提供流畅低延迟的对话体验。</p>
+      </div>
+      <div class="feature-card">
+        <div class="icon icon-yellow"> </div>
+        <h3>多语言支持</h3>
+        <p>界面支持日语、中文和英文,错误信息和 API 响应消息全面国际化。</p>
+      </div>
+    </div>
+
+    <!-- 系统架构 -->
+    <section>
+      <h2 class="section-title"><span class="num">1</span> 系统架构概览</h2>
+
+      <div class="arch-diagram">
+        <div class="arch-label">前端层</div>
+        <div class="arch-row">
+          <div class="arch-box frontend">React 19 + Vite<br><small style="opacity:0.7">端口 13001(开发)/ 80(生产)</small></div>
+        </div>
+        <div class="arch-arrow">↕</div>
+        <div class="arch-label">后端层</div>
+        <div class="arch-row">
+          <div class="arch-box backend">NestJS API<br><small style="opacity:0.7">端口 3001</small></div>
+          <div class="arch-box backend">JWT 认证</div>
+          <div class="arch-box backend">对话 / RAG</div>
+          <div class="arch-box backend">视觉管线</div>
+        </div>
+        <div class="arch-arrow">↕</div>
+        <div class="arch-label">AI 与数据层</div>
+        <div class="arch-row">
+          <div class="arch-box ai">OpenAI / Gemini<br><small style="opacity:0.7">LLM + 向量嵌入</small></div>
+          <div class="arch-box infra">Elasticsearch<br><small style="opacity:0.7">端口 9200</small></div>
+          <div class="arch-box infra">Apache Tika<br><small style="opacity:0.7">端口 9998</small></div>
+          <div class="arch-box infra">LibreOffice<br><small style="opacity:0.7">端口 8100</small></div>
+          <div class="arch-box data">SQLite<br><small style="opacity:0.7">元数据存储</small></div>
+        </div>
+      </div>
+
+      <!-- 处理管线 -->
+      <h4 style="margin-top:28px; margin-bottom:14px; color:#f1f5f9;">双处理管线</h4>
+      <div class="two-col">
+        <div class="card">
+          <h4>快速模式(Tika)</h4>
+          <div class="pipeline">
+            <div class="pipeline-step active">上传</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step active">Tika 提取</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step active">向量化</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step active">存储</div>
+          </div>
+          <p style="font-size:0.85rem; color:var(--text-muted); text-align:center;">快速文本提取,无 API 调用成本</p>
+        </div>
+        <div class="card">
+          <h4>高精度模式(视觉管线)</h4>
+          <div class="pipeline">
+            <div class="pipeline-step active">上传</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step">LibreOffice</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step">PDF 转图片</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step">视觉模型</div>
+          </div>
+          <p style="font-size:0.85rem; color:var(--text-muted); text-align:center;">保留原始排版、图表和图片信息</p>
+        </div>
+      </div>
+    </section>
+
+    <!-- 项目结构 -->
+    <section>
+      <h2 class="section-title"><span class="num">2</span> 项目结构</h2>
+      <div class="file-tree">
+        <span class="dir">simple-kb/</span><br>
+        ├── <span class="dir">web/</span>                      <span class="comment"># React 前端(Vite)</span><br>
+        │   ├── <span class="dir">components/</span>              <span class="comment"># UI 组件(ChatInterface, ConfigPanel 等)</span><br>
+        │   ├── <span class="dir">contexts/</span>                <span class="comment"># React Context 提供者</span><br>
+        │   ├── <span class="dir">services/</span>                <span class="comment"># API 客户端服务</span><br>
+        │   └── <span class="dir">utils/</span>                   <span class="comment"># 工具函数</span><br>
+        ├── <span class="dir">server/</span>                     <span class="comment"># NestJS 后端</span><br>
+        │   ├── <span class="dir">src/</span><br>
+        │   │   ├── <span class="dir">ai/</span>                  <span class="comment"># AI 服务(向量嵌入等)</span><br>
+        │   │   ├── <span class="dir">api/</span>                 <span class="comment"># API 模块</span><br>
+        │   │   ├── <span class="dir">auth/</span>                <span class="comment"># JWT 认证</span><br>
+        │   │   ├── <span class="dir">chat/</span>                <span class="comment"># 对话 / RAG 模块</span><br>
+        │   │   ├── <span class="dir">elasticsearch/</span>        <span class="comment"># Elasticsearch 集成</span><br>
+        │   │   ├── <span class="dir">import-task/</span>          <span class="comment"># 导入任务管理</span><br>
+        │   │   ├── <span class="dir">knowledge-base/</span>       <span class="comment"># 知识库管理</span><br>
+        │   │   ├── <span class="dir">libreoffice/</span>          <span class="comment"># LibreOffice 集成</span><br>
+        │   │   ├── <span class="dir">model-config/</span>         <span class="comment"># 模型配置管理</span><br>
+        │   │   ├── <span class="dir">vision/</span>              <span class="comment"># 视觉模型集成</span><br>
+        │   │   └── <span class="dir">vision-pipeline/</span>      <span class="comment"># 视觉管线编排</span><br>
+        │   ├── <span class="dir">data/</span>                    <span class="comment"># SQLite 数据库存储</span><br>
+        │   ├── <span class="dir">uploads/</span>                 <span class="comment"># 上传文件存储</span><br>
+        │   └── <span class="dir">temp/</span>                    <span class="comment"># 临时文件</span><br>
+        ├── <span class="dir">docs/</span>                        <span class="comment"># 项目文档</span><br>
+        ├── <span class="dir">nginx/</span>                       <span class="comment"># Nginx 配置</span><br>
+        ├── <span class="dir">libreoffice-server/</span>           <span class="comment"># LibreOffice 转换服务(Python/FastAPI)</span><br>
+        └── <span class="file">docker-compose.yml</span>            <span class="comment"># Docker 编排配置</span>
+      </div>
+    </section>
+
+    <!-- 开发环境搭建 -->
+    <section>
+      <h2 class="section-title"><span class="num">3</span> 开发环境搭建</h2>
+
+      <h4 style="margin-bottom:10px; color:#f1f5f9;">前置条件</h4>
+      <div class="card" style="margin-bottom:20px;">
+        <ul>
+          <li>Node.js 18+</li>
+          <li>Yarn 包管理器</li>
+          <li>Docker &amp; Docker Compose</li>
+        </ul>
+      </div>
+
+      <h4 style="margin-bottom:10px; color:#f1f5f9;">快速启动</h4>
+      <div class="code-block">
+        <span class="comment"># 安装依赖</span><br>
+        <span class="cmd">yarn install</span><br><br>
+        <span class="comment"># 启动基础设施服务</span><br>
+        <span class="cmd">docker-compose up -d elasticsearch tika libreoffice</span><br><br>
+        <span class="comment"># 配置环境变量</span><br>
+        <span class="cmd">cp server/.env.sample server/.env</span><br><br>
+        <span class="comment"># 同时启动前后端</span><br>
+        <span class="cmd">yarn dev</span>
+      </div>
+
+      <h4 style="margin:20px 0 10px; color:#f1f5f9;">开发命令</h4>
+      <div class="code-block">
+        <span class="comment"># 仅启动前端(端口 13001)</span><br>
+        <span class="cmd">cd web && yarn dev</span><br><br>
+        <span class="comment"># 仅启动后端(端口 3001)</span><br>
+        <span class="cmd">cd server && yarn start:dev</span><br><br>
+        <span class="comment"># 运行测试</span><br>
+        <span class="cmd">cd server && yarn test</span><br>
+        <span class="cmd">cd server && yarn test:e2e</span><br><br>
+        <span class="comment"># 代码检查和格式化</span><br>
+        <span class="cmd">cd server && yarn lint</span><br>
+        <span class="cmd">cd server && yarn format</span>
+      </div>
+    </section>
+
+    <!-- Docker 服务 -->
+    <section>
+      <h2 class="section-title"><span class="num">4</span> Docker 服务与端口</h2>
+      <div class="card">
+        <table class="info-table">
+          <thead>
+            <tr><th>服务</th><th>端口</th><th>用途</th></tr>
+          </thead>
+          <tbody>
+            <tr><td>Elasticsearch</td><td class="port">9200</td><td>向量存储与混合检索</td></tr>
+            <tr><td>Apache Tika</td><td class="port">9998</td><td>文档文本提取</td></tr>
+            <tr><td>LibreOffice Server</td><td class="port">8100</td><td>文档格式转换</td></tr>
+            <tr><td>后端 API</td><td class="port">3001</td><td>NestJS REST API</td></tr>
+            <tr><td>前端(开发)</td><td class="port">13001</td><td>Vite 开发服务器</td></tr>
+            <tr><td>前端(生产)</td><td class="port">80 / 443</td><td>Nginx 反向代理</td></tr>
+          </tbody>
+        </table>
+      </div>
+    </section>
+
+    <!-- 环境配置 -->
+    <section>
+      <h2 class="section-title"><span class="num">5</span> 环境配置</h2>
+      <div class="card">
+        <p style="font-size:0.88rem; color:var(--text-muted); margin-bottom:14px;">关键环境变量,配置于 <code style="color:var(--accent-green); background:#0d1117; padding:2px 8px; border-radius:4px; font-size:0.83rem;">server/.env</code></p>
+        <table class="info-table">
+          <thead>
+            <tr><th>变量名</th><th>默认值</th><th>说明</th></tr>
+          </thead>
+          <tbody>
+            <tr><td class="env-key">OPENAI_API_KEY</td><td>&mdash;</td><td>OpenAI 兼容 API 密钥</td></tr>
+            <tr><td class="env-key">GEMINI_API_KEY</td><td>&mdash;</td><td>Google Gemini API 密钥</td></tr>
+            <tr><td class="env-key">ELASTICSEARCH_HOST</td><td>http://localhost:9200</td><td>Elasticsearch 地址</td></tr>
+            <tr><td class="env-key">TIKA_HOST</td><td>http://localhost:9998</td><td>Apache Tika 地址</td></tr>
+            <tr><td class="env-key">LIBREOFFICE_URL</td><td>http://localhost:8100</td><td>LibreOffice 服务器地址</td></tr>
+            <tr><td class="env-key">JWT_SECRET</td><td>&mdash;</td><td>JWT 签名密钥</td></tr>
+          </tbody>
+        </table>
+      </div>
+    </section>
+
+    <!-- 代码规范 -->
+    <section>
+      <h2 class="section-title"><span class="num">6</span> 代码规范</h2>
+      <div class="two-col">
+        <div class="card">
+          <h4>语言要求</h4>
+          <ul>
+            <li>代码注释必须使用英文</li>
+            <li>日志消息必须使用英文</li>
+            <li>错误消息必须支持国际化</li>
+            <li>API 响应消息必须支持国际化</li>
+            <li>界面支持日语、中文和英文</li>
+          </ul>
+        </div>
+        <div class="card">
+          <h4>代码质量</h4>
+          <ul>
+            <li>后端使用 Jest 进行单元测试和端到端测试</li>
+            <li>后端已配置 ESLint 和 Prettier</li>
+            <li>格式化:<code style="color:var(--accent-green);">cd server && yarn format</code></li>
+            <li>检查:<code style="color:var(--accent-green);">cd server && yarn lint</code></li>
+          </ul>
+        </div>
+      </div>
+    </section>
+
+    <!-- 常见开发任务 -->
+    <section>
+      <h2 class="section-title"><span class="num">7</span> 常见开发任务</h2>
+      <div class="two-col">
+        <div class="card">
+          <h4>添加新的 API 端点</h4>
+          <ol class="step-list">
+            <li>在 <code style="color:var(--accent-blue);">server/src/</code> 对应模块下创建 Controller</li>
+            <li>添加 Service 方法,使用英文注释</li>
+            <li>更新 DTO 和参数校验</li>
+            <li>在 <code style="color:var(--accent-blue);">*.spec.ts</code> 文件中添加测试</li>
+          </ol>
+        </div>
+        <div class="card">
+          <h4>添加新的前端组件</h4>
+          <ol class="step-list">
+            <li>在 <code style="color:var(--accent-blue);">web/components/</code> 下创建组件</li>
+            <li>在 <code style="color:var(--accent-blue);">web/types.ts</code> 中添加 TypeScript 接口</li>
+            <li>使用 Tailwind CSS 进行样式开发</li>
+            <li>在 <code style="color:var(--accent-blue);">web/services/</code> 中连接后端服务</li>
+          </ol>
+        </div>
+      </div>
+    </section>
+
+    <!-- 部署 -->
+    <section>
+      <h2 class="section-title"><span class="num">8</span> 部署</h2>
+      <div class="two-col">
+        <div class="card">
+          <h4>开发环境</h4>
+          <div class="code-block">
+            <span class="cmd">docker-compose up -d elasticsearch tika libreoffice</span><br>
+            <span class="cmd">yarn dev</span>
+          </div>
+        </div>
+        <div class="card">
+          <h4>生产环境</h4>
+          <div class="code-block">
+            <span class="comment"># 构建并启动所有服务</span><br>
+            <span class="cmd">docker-compose up -d</span>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <!-- 故障排查 -->
+    <section>
+      <h2 class="section-title"><span class="num">9</span> 故障排查</h2>
+      <div class="troubleshoot-item">
+        <strong>Elasticsearch 无法启动</strong>
+        <p>检查 docker-compose.yml 中的内存限制配置</p>
+      </div>
+      <div class="troubleshoot-item">
+        <strong>文件上传失败</strong>
+        <p>确保 <code style="color:var(--accent-green);">uploads/</code> 和 <code style="color:var(--accent-green);">temp/</code> 目录存在且具有正确的读写权限</p>
+      </div>
+      <div class="troubleshoot-item">
+        <strong>视觉管线报错</strong>
+        <p>确认 LibreOffice 服务正在运行且 8100 端口可访问</p>
+      </div>
+      <div class="troubleshoot-item">
+        <strong>API 密钥错误</strong>
+        <p>检查 <code style="color:var(--accent-green);">server/.env</code> 中的环境变量配置</p>
+      </div>
+      <div class="troubleshoot-item">
+        <strong>数据库重置</strong>
+        <p>删除 <code style="color:var(--accent-green);">server/data/metadata.db</code> 及 Elasticsearch 数据卷</p>
+      </div>
+    </section>
+
+    <!-- 调试 -->
+    <section>
+      <h2 class="section-title"><span class="num">10</span> 调试与健康检查</h2>
+      <div class="code-block">
+        <span class="comment"># 检查 Elasticsearch 状态</span><br>
+        <span class="cmd">curl http://localhost:9200/_cat/indices</span><br><br>
+        <span class="comment"># 检查 Tika 状态</span><br>
+        <span class="cmd">curl http://localhost:9998/tika</span><br><br>
+        <span class="comment"># 检查 LibreOffice 状态</span><br>
+        <span class="cmd">curl http://localhost:8100/health</span>
+      </div>
+    </section>
+
+  </div>
+
+  <footer>
+    Simple Knowledge Base &mdash; 全栈 RAG 问答系统 &mdash; React 19 + NestJS + Elasticsearch
+  </footer>
+
+</body>
+</html>

+ 781 - 0
docs/system-overview.html

@@ -0,0 +1,781 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+  <meta charset="UTF-8">
+  <meta name="viewport" content="width=device-width, initial-scale=1.0">
+  <title>Simple Knowledge Base - System Overview</title>
+  <style>
+    :root {
+      --primary: #6366f1;
+      --primary-light: #818cf8;
+      --primary-dark: #4f46e5;
+      --bg: #0f172a;
+      --bg-card: #1e293b;
+      --bg-code: #0d1117;
+      --text: #e2e8f0;
+      --text-muted: #94a3b8;
+      --border: #334155;
+      --accent-green: #34d399;
+      --accent-blue: #38bdf8;
+      --accent-orange: #fb923c;
+      --accent-pink: #f472b6;
+    }
+
+    * { margin: 0; padding: 0; box-sizing: border-box; }
+
+    body {
+      font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Noto Sans SC', sans-serif;
+      background: var(--bg);
+      color: var(--text);
+      line-height: 1.7;
+    }
+
+    .hero {
+      background: linear-gradient(135deg, #1e1b4b 0%, #312e81 30%, #0f172a 70%, #0c0a09 100%);
+      padding: 80px 20px 60px;
+      text-align: center;
+      position: relative;
+      overflow: hidden;
+    }
+
+    .hero::before {
+      content: '';
+      position: absolute;
+      top: -50%;
+      left: -50%;
+      width: 200%;
+      height: 200%;
+      background: radial-gradient(circle at 30% 50%, rgba(99, 102, 241, 0.15) 0%, transparent 50%),
+                  radial-gradient(circle at 70% 50%, rgba(56, 189, 248, 0.1) 0%, transparent 50%);
+      animation: pulse 8s ease-in-out infinite;
+    }
+
+    @keyframes pulse {
+      0%, 100% { opacity: 0.6; }
+      50% { opacity: 1; }
+    }
+
+    .hero h1 {
+      font-size: 2.8rem;
+      font-weight: 800;
+      background: linear-gradient(135deg, #c7d2fe, #818cf8, #38bdf8);
+      -webkit-background-clip: text;
+      -webkit-text-fill-color: transparent;
+      background-clip: text;
+      position: relative;
+      margin-bottom: 12px;
+    }
+
+    .hero .subtitle {
+      font-size: 1.15rem;
+      color: var(--text-muted);
+      position: relative;
+      max-width: 700px;
+      margin: 0 auto;
+    }
+
+    .badge-row {
+      display: flex;
+      gap: 10px;
+      justify-content: center;
+      flex-wrap: wrap;
+      margin-top: 24px;
+      position: relative;
+    }
+
+    .badge {
+      display: inline-flex;
+      align-items: center;
+      gap: 6px;
+      padding: 5px 14px;
+      border-radius: 999px;
+      font-size: 0.82rem;
+      font-weight: 600;
+      border: 1px solid;
+    }
+
+    .badge.react { color: #61dafb; border-color: #61dafb33; background: #61dafb10; }
+    .badge.nest { color: #e0234e; border-color: #e0234e33; background: #e0234e10; }
+    .badge.rag { color: var(--accent-green); border-color: #34d39933; background: #34d39910; }
+    .badge.ts { color: #3178c6; border-color: #3178c633; background: #3178c610; }
+
+    .container {
+      max-width: 1100px;
+      margin: 0 auto;
+      padding: 0 20px;
+    }
+
+    .features-grid {
+      display: grid;
+      grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
+      gap: 20px;
+      margin: -40px auto 60px;
+      position: relative;
+      z-index: 1;
+    }
+
+    .feature-card {
+      background: var(--bg-card);
+      border: 1px solid var(--border);
+      border-radius: 16px;
+      padding: 28px;
+      transition: transform 0.2s, border-color 0.2s;
+    }
+
+    .feature-card:hover {
+      transform: translateY(-3px);
+      border-color: var(--primary);
+    }
+
+    .feature-card .icon {
+      width: 44px;
+      height: 44px;
+      border-radius: 12px;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      font-size: 1.3rem;
+      margin-bottom: 16px;
+    }
+
+    .feature-card h3 {
+      font-size: 1.05rem;
+      margin-bottom: 8px;
+      color: #f1f5f9;
+    }
+
+    .feature-card p {
+      font-size: 0.9rem;
+      color: var(--text-muted);
+    }
+
+    .icon-blue { background: #38bdf818; }
+    .icon-green { background: #34d39918; }
+    .icon-orange { background: #fb923c18; }
+    .icon-pink { background: #f472b618; }
+    .icon-purple { background: #a78bfa18; }
+    .icon-yellow { background: #fbbf2418; }
+
+    section {
+      margin-bottom: 56px;
+    }
+
+    .section-title {
+      font-size: 1.5rem;
+      font-weight: 700;
+      margin-bottom: 24px;
+      padding-bottom: 12px;
+      border-bottom: 2px solid var(--border);
+      display: flex;
+      align-items: center;
+      gap: 10px;
+    }
+
+    .section-title .num {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 32px;
+      height: 32px;
+      border-radius: 8px;
+      background: var(--primary);
+      font-size: 0.85rem;
+      font-weight: 700;
+      flex-shrink: 0;
+    }
+
+    /* Architecture Diagram */
+    .arch-diagram {
+      background: var(--bg-card);
+      border: 1px solid var(--border);
+      border-radius: 16px;
+      padding: 32px;
+      overflow-x: auto;
+    }
+
+    .arch-row {
+      display: flex;
+      justify-content: center;
+      gap: 16px;
+      margin-bottom: 16px;
+      flex-wrap: wrap;
+    }
+
+    .arch-box {
+      padding: 14px 22px;
+      border-radius: 10px;
+      font-size: 0.85rem;
+      font-weight: 600;
+      text-align: center;
+      min-width: 140px;
+      border: 1px solid;
+    }
+
+    .arch-box.frontend { background: #61dafb12; border-color: #61dafb33; color: #61dafb; }
+    .arch-box.backend { background: #e0234e12; border-color: #e0234e33; color: #e0234e; }
+    .arch-box.infra { background: #34d39912; border-color: #34d39933; color: #34d399; }
+    .arch-box.ai { background: #a78bfa12; border-color: #a78bfa33; color: #a78bfa; }
+    .arch-box.data { background: #fb923c12; border-color: #fb923c33; color: #fb923c; }
+
+    .arch-arrow {
+      text-align: center;
+      color: var(--text-muted);
+      font-size: 1.2rem;
+      margin-bottom: 16px;
+    }
+
+    .arch-label {
+      text-align: center;
+      font-size: 0.78rem;
+      color: var(--text-muted);
+      text-transform: uppercase;
+      letter-spacing: 1.5px;
+      margin-bottom: 10px;
+    }
+
+    /* File Tree */
+    .file-tree {
+      background: var(--bg-code);
+      border: 1px solid var(--border);
+      border-radius: 12px;
+      padding: 24px 28px;
+      font-family: 'Cascadia Code', 'Fira Code', monospace;
+      font-size: 0.85rem;
+      line-height: 1.9;
+      overflow-x: auto;
+    }
+
+    .file-tree .dir { color: var(--accent-blue); font-weight: 600; }
+    .file-tree .comment { color: #64748b; font-style: italic; }
+    .file-tree .file { color: var(--text); }
+
+    /* Pipeline */
+    .pipeline {
+      display: flex;
+      align-items: center;
+      gap: 0;
+      flex-wrap: wrap;
+      justify-content: center;
+      margin: 20px 0;
+    }
+
+    .pipeline-step {
+      padding: 12px 20px;
+      border-radius: 10px;
+      font-size: 0.85rem;
+      font-weight: 600;
+      text-align: center;
+      border: 1px solid var(--border);
+      background: var(--bg-card);
+    }
+
+    .pipeline-arrow {
+      color: var(--text-muted);
+      font-size: 1.1rem;
+      padding: 0 6px;
+    }
+
+    .pipeline-step.active {
+      border-color: var(--primary);
+      background: #6366f118;
+      color: var(--primary-light);
+    }
+
+    /* Tables */
+    .info-table {
+      width: 100%;
+      border-collapse: collapse;
+      margin: 12px 0;
+    }
+
+    .info-table th, .info-table td {
+      padding: 10px 16px;
+      text-align: left;
+      border-bottom: 1px solid var(--border);
+      font-size: 0.9rem;
+    }
+
+    .info-table th {
+      color: var(--text-muted);
+      font-weight: 600;
+      font-size: 0.82rem;
+      text-transform: uppercase;
+      letter-spacing: 0.5px;
+    }
+
+    .info-table .port {
+      font-family: 'Cascadia Code', monospace;
+      color: var(--accent-orange);
+      font-weight: 600;
+    }
+
+    .info-table .env-key {
+      font-family: 'Cascadia Code', monospace;
+      color: var(--accent-green);
+      font-size: 0.83rem;
+    }
+
+    /* Code Block */
+    .code-block {
+      background: var(--bg-code);
+      border: 1px solid var(--border);
+      border-radius: 10px;
+      padding: 18px 22px;
+      font-family: 'Cascadia Code', 'Fira Code', monospace;
+      font-size: 0.83rem;
+      line-height: 1.8;
+      overflow-x: auto;
+      margin: 12px 0;
+    }
+
+    .code-block .cmd { color: var(--accent-green); }
+    .code-block .comment { color: #64748b; }
+
+    /* Two Column Layout */
+    .two-col {
+      display: grid;
+      grid-template-columns: 1fr 1fr;
+      gap: 24px;
+    }
+
+    @media (max-width: 768px) {
+      .two-col { grid-template-columns: 1fr; }
+      .hero h1 { font-size: 2rem; }
+      .features-grid { grid-template-columns: 1fr; }
+    }
+
+    .card {
+      background: var(--bg-card);
+      border: 1px solid var(--border);
+      border-radius: 14px;
+      padding: 24px;
+    }
+
+    .card h4 {
+      font-size: 1rem;
+      margin-bottom: 14px;
+      color: #f1f5f9;
+    }
+
+    .card ul {
+      list-style: none;
+      padding: 0;
+    }
+
+    .card ul li {
+      padding: 6px 0;
+      font-size: 0.88rem;
+      color: var(--text-muted);
+      display: flex;
+      align-items: flex-start;
+      gap: 8px;
+    }
+
+    .card ul li::before {
+      content: '›';
+      color: var(--primary-light);
+      font-weight: 700;
+      flex-shrink: 0;
+    }
+
+    .step-list {
+      list-style: none;
+      padding: 0;
+      counter-reset: step;
+    }
+
+    .step-list li {
+      counter-increment: step;
+      padding: 8px 0;
+      font-size: 0.88rem;
+      color: var(--text-muted);
+      display: flex;
+      align-items: flex-start;
+      gap: 10px;
+    }
+
+    .step-list li::before {
+      content: counter(step);
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      width: 22px;
+      height: 22px;
+      border-radius: 6px;
+      background: var(--primary);
+      color: white;
+      font-size: 0.72rem;
+      font-weight: 700;
+      flex-shrink: 0;
+    }
+
+    .troubleshoot-item {
+      background: var(--bg-card);
+      border: 1px solid var(--border);
+      border-radius: 10px;
+      padding: 16px 20px;
+      margin-bottom: 10px;
+    }
+
+    .troubleshoot-item strong {
+      color: #f1f5f9;
+      font-size: 0.92rem;
+    }
+
+    .troubleshoot-item p {
+      color: var(--text-muted);
+      font-size: 0.85rem;
+      margin-top: 4px;
+    }
+
+    footer {
+      text-align: center;
+      padding: 40px 20px;
+      color: var(--text-muted);
+      font-size: 0.82rem;
+      border-top: 1px solid var(--border);
+    }
+  </style>
+</head>
+<body>
+
+  <!-- Hero -->
+  <div class="hero">
+    <h1>Simple Knowledge Base</h1>
+    <p class="subtitle">Full-stack RAG Q&A System &mdash; Retrieval-Augmented Generation powered by React 19 + NestJS</p>
+    <div class="badge-row">
+      <span class="badge react">React 19</span>
+      <span class="badge nest">NestJS</span>
+      <span class="badge rag">RAG System</span>
+      <span class="badge ts">TypeScript</span>
+    </div>
+  </div>
+
+  <div class="container">
+
+    <!-- Key Features -->
+    <div class="features-grid">
+      <div class="feature-card">
+        <div class="icon icon-purple"> </div>
+        <h3>Multi-Model Support</h3>
+        <p>OpenAI-compatible APIs (OpenAI, DeepSeek, Claude) + Google Gemini native SDK with configurable LLM, Embedding, and Rerank models.</p>
+      </div>
+      <div class="feature-card">
+        <div class="icon icon-blue">⚡</div>
+        <h3>Dual Processing Modes</h3>
+        <p>Fast Mode via Apache Tika for text extraction, and High-Precision Mode via Vision Pipeline for mixed image/text documents.</p>
+      </div>
+      <div class="feature-card">
+        <div class="icon icon-green"> </div>
+        <h3>Hybrid Search</h3>
+        <p>Vector + keyword search with Elasticsearch, source citation, similarity scoring, and configurable chunk size &amp; overlap.</p>
+      </div>
+      <div class="feature-card">
+        <div class="icon icon-orange"> </div>
+        <h3>User Isolation</h3>
+        <p>JWT authentication with per-user knowledge bases. Each user has isolated data and configurations.</p>
+      </div>
+      <div class="feature-card">
+        <div class="icon icon-pink"> </div>
+        <h3>Streaming Responses</h3>
+        <p>Real-time streaming via Server-Sent Events (SSE) for smooth, low-latency chat interactions.</p>
+      </div>
+      <div class="feature-card">
+        <div class="icon icon-yellow"> </div>
+        <h3>Multi-Language</h3>
+        <p>Interface supports Japanese, Chinese, and English with full internationalization for error and API response messages.</p>
+      </div>
+    </div>
+
+    <!-- Architecture -->
+    <section>
+      <h2 class="section-title"><span class="num">1</span> Architecture Overview</h2>
+
+      <div class="arch-diagram">
+        <div class="arch-label">Frontend Layer</div>
+        <div class="arch-row">
+          <div class="arch-box frontend">React 19 + Vite<br><small style="opacity:0.7">Port 13001 (dev) / 80 (prod)</small></div>
+        </div>
+        <div class="arch-arrow">↕</div>
+        <div class="arch-label">Backend Layer</div>
+        <div class="arch-row">
+          <div class="arch-box backend">NestJS API<br><small style="opacity:0.7">Port 3001</small></div>
+          <div class="arch-box backend">JWT Auth</div>
+          <div class="arch-box backend">Chat / RAG</div>
+          <div class="arch-box backend">Vision Pipeline</div>
+        </div>
+        <div class="arch-arrow">↕</div>
+        <div class="arch-label">AI &amp; Data Layer</div>
+        <div class="arch-row">
+          <div class="arch-box ai">OpenAI / Gemini<br><small style="opacity:0.7">LLM + Embedding</small></div>
+          <div class="arch-box infra">Elasticsearch<br><small style="opacity:0.7">Port 9200</small></div>
+          <div class="arch-box infra">Apache Tika<br><small style="opacity:0.7">Port 9998</small></div>
+          <div class="arch-box infra">LibreOffice<br><small style="opacity:0.7">Port 8100</small></div>
+          <div class="arch-box data">SQLite<br><small style="opacity:0.7">Metadata</small></div>
+        </div>
+      </div>
+
+      <!-- Processing Pipeline -->
+      <h4 style="margin-top:28px; margin-bottom:14px; color:#f1f5f9;">Dual Processing Pipeline</h4>
+      <div class="two-col">
+        <div class="card">
+          <h4>Fast Mode (Tika)</h4>
+          <div class="pipeline">
+            <div class="pipeline-step active">Upload</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step active">Tika Extract</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step active">Embed</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step active">Store</div>
+          </div>
+          <p style="font-size:0.85rem; color:var(--text-muted); text-align:center;">Quick text extraction, no API cost</p>
+        </div>
+        <div class="card">
+          <h4>High-Precision Mode (Vision)</h4>
+          <div class="pipeline">
+            <div class="pipeline-step active">Upload</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step">LibreOffice</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step">PDF→Image</div>
+            <span class="pipeline-arrow">→</span>
+            <div class="pipeline-step">Vision Model</div>
+          </div>
+          <p style="font-size:0.85rem; color:var(--text-muted); text-align:center;">Preserves layout, charts, and images</p>
+        </div>
+      </div>
+    </section>
+
+    <!-- Project Structure -->
+    <section>
+      <h2 class="section-title"><span class="num">2</span> Project Structure</h2>
+      <div class="file-tree">
+        <span class="dir">simple-kb/</span><br>
+        ├── <span class="dir">web/</span>                      <span class="comment"># React frontend (Vite)</span><br>
+        │   ├── <span class="dir">components/</span>              <span class="comment"># UI components (ChatInterface, ConfigPanel, etc.)</span><br>
+        │   ├── <span class="dir">contexts/</span>                <span class="comment"># React Context providers</span><br>
+        │   ├── <span class="dir">services/</span>                <span class="comment"># API client services</span><br>
+        │   └── <span class="dir">utils/</span>                   <span class="comment"># Utility functions</span><br>
+        ├── <span class="dir">server/</span>                     <span class="comment"># NestJS backend</span><br>
+        │   ├── <span class="dir">src/</span><br>
+        │   │   ├── <span class="dir">ai/</span>                  <span class="comment"># AI services (embedding, etc.)</span><br>
+        │   │   ├── <span class="dir">api/</span>                 <span class="comment"># API module</span><br>
+        │   │   ├── <span class="dir">auth/</span>                <span class="comment"># JWT authentication</span><br>
+        │   │   ├── <span class="dir">chat/</span>                <span class="comment"># Chat / RAG module</span><br>
+        │   │   ├── <span class="dir">elasticsearch/</span>        <span class="comment"># Elasticsearch integration</span><br>
+        │   │   ├── <span class="dir">import-task/</span>          <span class="comment"># Import task management</span><br>
+        │   │   ├── <span class="dir">knowledge-base/</span>       <span class="comment"># Knowledge base management</span><br>
+        │   │   ├── <span class="dir">libreoffice/</span>          <span class="comment"># LibreOffice integration</span><br>
+        │   │   ├── <span class="dir">model-config/</span>         <span class="comment"># Model configuration management</span><br>
+        │   │   ├── <span class="dir">vision/</span>              <span class="comment"># Vision model integration</span><br>
+        │   │   └── <span class="dir">vision-pipeline/</span>      <span class="comment"># Vision pipeline orchestration</span><br>
+        │   ├── <span class="dir">data/</span>                    <span class="comment"># SQLite database storage</span><br>
+        │   ├── <span class="dir">uploads/</span>                 <span class="comment"># Uploaded files storage</span><br>
+        │   └── <span class="dir">temp/</span>                    <span class="comment"># Temporary files</span><br>
+        ├── <span class="dir">docs/</span>                        <span class="comment"># Documentation (Japanese/Chinese)</span><br>
+        ├── <span class="dir">nginx/</span>                       <span class="comment"># Nginx configuration</span><br>
+        ├── <span class="dir">libreoffice-server/</span>           <span class="comment"># LibreOffice conversion service (Python/FastAPI)</span><br>
+        └── <span class="file">docker-compose.yml</span>            <span class="comment"># Docker orchestration</span>
+      </div>
+    </section>
+
+    <!-- Development Setup -->
+    <section>
+      <h2 class="section-title"><span class="num">3</span> Development Setup</h2>
+
+      <h4 style="margin-bottom:10px; color:#f1f5f9;">Prerequisites</h4>
+      <div class="card" style="margin-bottom:20px;">
+        <ul>
+          <li>Node.js 18+</li>
+          <li>Yarn package manager</li>
+          <li>Docker &amp; Docker Compose</li>
+        </ul>
+      </div>
+
+      <h4 style="margin-bottom:10px; color:#f1f5f9;">Quick Start</h4>
+      <div class="code-block">
+        <span class="comment"># Install dependencies</span><br>
+        <span class="cmd">yarn install</span><br><br>
+        <span class="comment"># Start infrastructure services</span><br>
+        <span class="cmd">docker-compose up -d elasticsearch tika libreoffice</span><br><br>
+        <span class="comment"># Configure environment</span><br>
+        <span class="cmd">cp server/.env.sample server/.env</span><br><br>
+        <span class="comment"># Start both frontend and backend</span><br>
+        <span class="cmd">yarn dev</span>
+      </div>
+
+      <h4 style="margin:20px 0 10px; color:#f1f5f9;">Development Commands</h4>
+      <div class="code-block">
+        <span class="comment"># Frontend only (port 13001)</span><br>
+        <span class="cmd">cd web && yarn dev</span><br><br>
+        <span class="comment"># Backend only (port 3001)</span><br>
+        <span class="cmd">cd server && yarn start:dev</span><br><br>
+        <span class="comment"># Run tests</span><br>
+        <span class="cmd">cd server && yarn test</span><br>
+        <span class="cmd">cd server && yarn test:e2e</span><br><br>
+        <span class="comment"># Lint and format</span><br>
+        <span class="cmd">cd server && yarn lint</span><br>
+        <span class="cmd">cd server && yarn format</span>
+      </div>
+    </section>
+
+    <!-- Docker Services -->
+    <section>
+      <h2 class="section-title"><span class="num">4</span> Docker Services &amp; Ports</h2>
+      <div class="card">
+        <table class="info-table">
+          <thead>
+            <tr><th>Service</th><th>Port</th><th>Purpose</th></tr>
+          </thead>
+          <tbody>
+            <tr><td>Elasticsearch</td><td class="port">9200</td><td>Vector storage &amp; hybrid search</td></tr>
+            <tr><td>Apache Tika</td><td class="port">9998</td><td>Document text extraction</td></tr>
+            <tr><td>LibreOffice Server</td><td class="port">8100</td><td>Document format conversion</td></tr>
+            <tr><td>Backend API</td><td class="port">3001</td><td>NestJS REST API</td></tr>
+            <tr><td>Frontend (dev)</td><td class="port">13001</td><td>Vite dev server</td></tr>
+            <tr><td>Frontend (prod)</td><td class="port">80 / 443</td><td>Nginx reverse proxy</td></tr>
+          </tbody>
+        </table>
+      </div>
+    </section>
+
+    <!-- Environment Configuration -->
+    <section>
+      <h2 class="section-title"><span class="num">5</span> Environment Configuration</h2>
+      <div class="card">
+        <p style="font-size:0.88rem; color:var(--text-muted); margin-bottom:14px;">Key environment variables in <code style="color:var(--accent-green); background:#0d1117; padding:2px 8px; border-radius:4px; font-size:0.83rem;">server/.env</code></p>
+        <table class="info-table">
+          <thead>
+            <tr><th>Variable</th><th>Default</th><th>Description</th></tr>
+          </thead>
+          <tbody>
+            <tr><td class="env-key">OPENAI_API_KEY</td><td>&mdash;</td><td>OpenAI-compatible API key</td></tr>
+            <tr><td class="env-key">GEMINI_API_KEY</td><td>&mdash;</td><td>Google Gemini API key</td></tr>
+            <tr><td class="env-key">ELASTICSEARCH_HOST</td><td>http://localhost:9200</td><td>Elasticsearch URL</td></tr>
+            <tr><td class="env-key">TIKA_HOST</td><td>http://localhost:9998</td><td>Apache Tika URL</td></tr>
+            <tr><td class="env-key">LIBREOFFICE_URL</td><td>http://localhost:8100</td><td>LibreOffice server URL</td></tr>
+            <tr><td class="env-key">JWT_SECRET</td><td>&mdash;</td><td>JWT signing secret</td></tr>
+          </tbody>
+        </table>
+      </div>
+    </section>
+
+    <!-- Code Standards -->
+    <section>
+      <h2 class="section-title"><span class="num">6</span> Code Standards</h2>
+      <div class="two-col">
+        <div class="card">
+          <h4>Language Requirements</h4>
+          <ul>
+            <li>Code comments must be in English</li>
+            <li>Log messages must be in English</li>
+            <li>Error messages must support internationalization</li>
+            <li>API response messages must support i18n</li>
+            <li>Interface supports Japanese, Chinese, and English</li>
+          </ul>
+        </div>
+        <div class="card">
+          <h4>Code Quality</h4>
+          <ul>
+            <li>Backend uses Jest for unit and e2e tests</li>
+            <li>ESLint and Prettier configured for backend</li>
+            <li>Format: <code style="color:var(--accent-green);">cd server && yarn format</code></li>
+            <li>Lint: <code style="color:var(--accent-green);">cd server && yarn lint</code></li>
+          </ul>
+        </div>
+      </div>
+    </section>
+
+    <!-- Common Tasks -->
+    <section>
+      <h2 class="section-title"><span class="num">7</span> Common Development Tasks</h2>
+      <div class="two-col">
+        <div class="card">
+          <h4>Adding a New API Endpoint</h4>
+          <ol class="step-list">
+            <li>Create controller in appropriate module under <code style="color:var(--accent-blue);">server/src/</code></li>
+            <li>Add service methods with English comments</li>
+            <li>Update DTOs and validation</li>
+            <li>Add tests in <code style="color:var(--accent-blue);">*.spec.ts</code> files</li>
+          </ol>
+        </div>
+        <div class="card">
+          <h4>Adding a New Frontend Component</h4>
+          <ol class="step-list">
+            <li>Create component in <code style="color:var(--accent-blue);">web/components/</code></li>
+            <li>Add TypeScript interfaces in <code style="color:var(--accent-blue);">web/types.ts</code></li>
+            <li>Use Tailwind CSS for styling</li>
+            <li>Connect to backend services in <code style="color:var(--accent-blue);">web/services/</code></li>
+          </ol>
+        </div>
+      </div>
+    </section>
+
+    <!-- Deployment -->
+    <section>
+      <h2 class="section-title"><span class="num">8</span> Deployment</h2>
+      <div class="two-col">
+        <div class="card">
+          <h4>Development</h4>
+          <div class="code-block">
+            <span class="cmd">docker-compose up -d elasticsearch tika libreoffice</span><br>
+            <span class="cmd">yarn dev</span>
+          </div>
+        </div>
+        <div class="card">
+          <h4>Production</h4>
+          <div class="code-block">
+            <span class="comment"># Build and start all services</span><br>
+            <span class="cmd">docker-compose up -d</span>
+          </div>
+        </div>
+      </div>
+    </section>
+
+    <!-- Troubleshooting -->
+    <section>
+      <h2 class="section-title"><span class="num">9</span> Troubleshooting</h2>
+      <div class="troubleshoot-item">
+        <strong>Elasticsearch not starting</strong>
+        <p>Check memory limits in docker-compose.yml</p>
+      </div>
+      <div class="troubleshoot-item">
+        <strong>File upload failures</strong>
+        <p>Ensure <code style="color:var(--accent-green);">uploads/</code> and <code style="color:var(--accent-green);">temp/</code> directories exist with proper permissions</p>
+      </div>
+      <div class="troubleshoot-item">
+        <strong>Vision pipeline errors</strong>
+        <p>Verify LibreOffice server is running and accessible at port 8100</p>
+      </div>
+      <div class="troubleshoot-item">
+        <strong>API key errors</strong>
+        <p>Check environment variables in <code style="color:var(--accent-green);">server/.env</code></p>
+      </div>
+      <div class="troubleshoot-item">
+        <strong>Database reset</strong>
+        <p>Delete <code style="color:var(--accent-green);">server/data/metadata.db</code> and Elasticsearch data volume</p>
+      </div>
+    </section>
+
+    <!-- Debugging -->
+    <section>
+      <h2 class="section-title"><span class="num">10</span> Debugging &amp; Health Checks</h2>
+      <div class="code-block">
+        <span class="comment"># Check Elasticsearch</span><br>
+        <span class="cmd">curl http://localhost:9200/_cat/indices</span><br><br>
+        <span class="comment"># Check Tika</span><br>
+        <span class="cmd">curl http://localhost:9998/tika</span><br><br>
+        <span class="comment"># Check LibreOffice</span><br>
+        <span class="cmd">curl http://localhost:8100/health</span>
+      </div>
+    </section>
+
+  </div>
+
+  <footer>
+    Simple Knowledge Base &mdash; Full-stack RAG Q&A System &mdash; React 19 + NestJS + Elasticsearch
+  </footer>
+
+</body>
+</html>

+ 74 - 0
fix_kb_service.py

@@ -0,0 +1,74 @@
+import os
+
+file_path = r'd:\aura\AuraK\server\src\knowledge-base\knowledge-base.service.ts'
+
+with open(file_path, 'r', encoding='utf-8') as f:
+    lines = f.readlines()
+
+new_lines = []
+changed = False
+for line in lines:
+    # Match the specific pattern: userId, followed by kb.embeddingModelId
+    if 'userId,' in line and 'kb.embeddingModelId' in line:
+        # Check if it's the specific call site with three arguments
+        # Example: [chunk.content], userId, kb.embeddingModelId
+        # Or: batchTexts, userId, kb.embeddingModelId
+        if 'getEmbeddings' not in line: # It's probably a multi-line call
+             pass# Handled below
+        
+        new_line = line.replace('userId,', '').replace('  ', ' ') # Simple fix for potential double spaces
+        # But wait, let's be more precise
+        if 'userId,' in line:
+             new_line = line.replace('userId,', '').strip()
+             # Re-add leading whitespace
+             leading = line[:line.find(line.lstrip())]
+             new_line = leading + new_line + '\n'
+             new_lines.append(new_line)
+             changed = True
+             continue
+
+    new_lines.append(line)
+
+# Let's try a simpler approach if the above is too complex
+with open(file_path, 'r', encoding='utf-8') as f:
+    content = f.read()
+
+# Pattern 1: [chunk.content], userId, kb.embeddingModelId
+target1 = """                    [chunk.content], // Single text
+                    userId,
+                    kb.embeddingModelId,"""
+replacement1 = """                    [chunk.content], // Single text
+                    kb.embeddingModelId,"""
+
+# Pattern 2: chunkTexts, userId, kb.embeddingModelId
+target2 = """                chunkTexts,
+                userId,
+                kb.embeddingModelId,"""
+replacement2 = """                chunkTexts,
+                kb.embeddingModelId,"""
+
+# Pattern 3: batchTexts, userId, kb.embeddingModelId
+target3 = """                batchTexts,
+                userId,
+                kb.embeddingModelId,"""
+replacement3 = """                batchTexts,
+                kb.embeddingModelId,"""
+
+# Pattern 4 (Precise results): texts, userId, embeddingModelId
+target4 = """          texts,
+          userId,
+          embeddingModelId,"""
+replacement4 = """          texts,
+          embeddingModelId,"""
+
+fixed_content = content.replace(target1, replacement1)
+fixed_content = fixed_content.replace(target2, replacement2)
+fixed_content = fixed_content.replace(target3, replacement3)
+fixed_content = fixed_content.replace(target4, replacement4)
+
+if fixed_content != content:
+    with open(file_path, 'w', encoding='utf-8') as f:
+        f.write(fixed_content)
+    print("Successfully replaced patterns.")
+else:
+    print("No patterns found to replace.")

+ 1 - 1
server/Dockerfile

@@ -23,4 +23,4 @@ RUN yarn build
 EXPOSE 3001
 
 # Start application
-CMD ["node", "/app/dist/src/main.js"]
+CMD ["node", "/app/dist/main.js"]

+ 1 - 4
server/src/api/api-v1.controller.ts

@@ -63,10 +63,7 @@ export class ApiV1Controller {
     // Get organization settings and model configuration
     const tenantSettings = await this.tenantService.getSettings(user.tenantId);
     const userSetting = await this.userSettingService.getByUser(user.id);
-    const models = await this.modelConfigService.findAll(
-      user.id,
-      user.tenantId,
-    );
+    const models = await this.modelConfigService.findAll();
     const llmModel =
       models.find((m) => m.id === tenantSettings?.selectedLLMId) ??
       models.find((m) => m.type === 'llm' && m.isDefault);

+ 1 - 4
server/src/api/api.controller.ts

@@ -41,10 +41,7 @@ export class ApiController {
 
     try {
       // ユーザーの LLM モデル設定を取得
-      const models = await this.modelConfigService.findAll(
-        req.user.id,
-        req.user.tenantId,
-      );
+      const models = await this.modelConfigService.findAll();
       const llmModel = models.find((m) => m.type === 'llm');
       if (!llmModel) {
         throw new Error(this.i18nService.getMessage('addLLMConfig'));

+ 2 - 6
server/src/chat/chat.controller.ts

@@ -92,7 +92,7 @@ export class ChatController {
       const tenantId = req.user.tenantId;
 
       // 获取用户的LLM模型配置
-      let models = await this.modelConfigService.findAll(userId, tenantId);
+      let models = await this.modelConfigService.findAll();
 
       if (role !== 'SUPER_ADMIN') {
         const tenantSettings = await this.tenantService.getSettings(tenantId);
@@ -104,11 +104,7 @@ export class ChatController {
       let llmModel;
       if (selectedLLMId) {
         // Find specifically selected model
-        llmModel = await this.modelConfigService.findOne(
-          selectedLLMId,
-          userId,
-          tenantId,
-        );
+        llmModel = await this.modelConfigService.findOne(selectedLLMId);
         console.log('使用选中的LLM模型:', llmModel.name);
       } else {
         // Use organization's default LLM from Index Chat Config (strict)

+ 2 - 6
server/src/chat/chat.service.ts

@@ -133,11 +133,8 @@ export class ChatService {
 
       if (selectedEmbeddingId) {
         // Find specifically selected model
-        embeddingModel = await this.modelConfigService.findOne(
-          selectedEmbeddingId,
-          userId,
-          tenantId || 'default',
-        );
+        embeddingModel =
+          await this.modelConfigService.findOne(selectedEmbeddingId);
       } else {
         // Use organization's default from Index Chat Config (strict)
         embeddingModel = await this.modelConfigService.findDefaultByType(
@@ -486,7 +483,6 @@ ${instruction}`;
       );
       const queryEmbedding = await this.embeddingService.getEmbeddings(
         [combinedQuery],
-        userId,
         embeddingModelId,
       );
       const queryVector = queryEmbedding[0];

+ 8 - 28
server/src/knowledge-base/chunk-config.service.ts

@@ -65,22 +65,14 @@ export class ChunkConfigService {
   /**
    * Get model limit settings (read from database)
    */
-  async getModelLimits(
-    modelId: string,
-    userId: string,
-    tenantId?: string,
-  ): Promise<{
+  async getModelLimits(modelId: string): Promise<{
     maxInputTokens: number;
     maxBatchSize: number;
     expectedDimensions: number;
     providerName: string;
     isVectorModel: boolean;
   }> {
-    const modelConfig = await this.modelConfigService.findOne(
-      modelId,
-      userId,
-      tenantId || '',
-    );
+    const modelConfig = await this.modelConfigService.findOne(modelId);
 
     if (!modelConfig || modelConfig.type !== 'embedding') {
       throw new BadRequestException(
@@ -134,8 +126,6 @@ export class ChunkConfigService {
     chunkSize: number,
     chunkOverlap: number,
     modelId: string,
-    userId: string,
-    tenantId?: string,
   ): Promise<{
     chunkSize: number;
     chunkOverlap: number;
@@ -144,7 +134,7 @@ export class ChunkConfigService {
     effectiveMaxOverlapSize: number;
   }> {
     const warnings: string[] = [];
-    const limits = await this.getModelLimits(modelId, userId, tenantId);
+    const limits = await this.getModelLimits(modelId);
 
     // 1. Calculate final limits (choose smaller of env var and model limit)
     const effectiveMaxChunkSize = Math.min(
@@ -263,11 +253,9 @@ export class ChunkConfigService {
    */
   async getRecommendedBatchSize(
     modelId: string,
-    userId: string,
-    tenantId?: string,
     currentBatchSize: number = 100,
   ): Promise<number> {
-    const limits = await this.getModelLimits(modelId, userId, tenantId);
+    const limits = await this.getModelLimits(modelId);
 
     // Choose smaller of configured value and model limit
     const recommended = Math.min(
@@ -302,11 +290,9 @@ export class ChunkConfigService {
    */
   async validateDimensions(
     modelId: string,
-    userId: string,
     actualDimensions: number,
-    tenantId?: string,
   ): Promise<boolean> {
-    const limits = await this.getModelLimits(modelId, userId, tenantId);
+    const limits = await this.getModelLimits(modelId);
 
     if (actualDimensions !== limits.expectedDimensions) {
       this.logger.warn(
@@ -329,10 +315,8 @@ export class ChunkConfigService {
     chunkSize: number,
     chunkOverlap: number,
     modelId: string,
-    userId: string,
-    tenantId?: string,
   ): Promise<string> {
-    const limits = await this.getModelLimits(modelId, userId, tenantId);
+    const limits = await this.getModelLimits(modelId);
 
     return [
       `Model: ${modelId}`,
@@ -364,7 +348,7 @@ export class ChunkConfigService {
       expectedDimensions: number;
     };
   }> {
-    const limits = await this.getModelLimits(modelId, userId, tenantId);
+    const limits = await this.getModelLimits(modelId);
 
     // Calculate final limits (choose smaller of env var and model limit)
     const maxChunkSize = Math.min(this.envMaxChunkSize, limits.maxInputTokens);
@@ -374,11 +358,7 @@ export class ChunkConfigService {
     );
 
     // Get model config name
-    const modelConfig = await this.modelConfigService.findOne(
-      modelId,
-      userId,
-      tenantId || '',
-    );
+    const modelConfig = await this.modelConfigService.findOne(modelId);
     const modelName = modelConfig?.name || 'Unknown';
 
     // Get defaults from tenant or user settings

+ 1 - 12
server/src/knowledge-base/embedding.service.ts

@@ -35,16 +35,12 @@ export class EmbeddingService {
 
   async getEmbeddings(
     texts: string[],
-    userId: string,
     embeddingModelConfigId: string,
-    tenantId?: string,
   ): Promise<number[][]> {
     this.logger.log(`Generating embeddings for ${texts.length} texts`);
 
     const modelConfig = await this.modelConfigService.findOne(
       embeddingModelConfigId,
-      userId,
-      tenantId || 'default',
     );
     if (!modelConfig || modelConfig.type !== 'embedding') {
       throw new Error(
@@ -60,8 +56,6 @@ export class EmbeddingService {
       );
     }
 
-    // API key is optional - allows local models
-
     if (!modelConfig.baseUrl) {
       throw new Error(
         `Model ${modelConfig.name} does not have baseUrl configured`,
@@ -86,7 +80,6 @@ export class EmbeddingService {
         const batch = texts.slice(i, i + maxBatchSize);
         const batchEmbeddings = await this.getEmbeddingsForBatch(
           batch,
-          userId,
           modelConfig,
           maxBatchSize,
         );
@@ -104,7 +97,6 @@ export class EmbeddingService {
       // Normal processing (within batch size)
       return await this.getEmbeddingsForBatch(
         texts,
-        userId,
         modelConfig,
         maxBatchSize,
       );
@@ -141,7 +133,6 @@ export class EmbeddingService {
    */
   private async getEmbeddingsForBatch(
     texts: string[],
-    userId: string,
     modelConfig: any,
     maxBatchSize: number,
   ): Promise<number[][]> {
@@ -161,7 +152,7 @@ export class EmbeddingService {
         }, 60000); // 60s timeout
 
         this.logger.log(
-          `[Model call] Type: Embedding, Model: ${modelConfig.name} (${modelConfig.modelId}), User: ${userId}, Text count: ${texts.length}`,
+          `[Model call] Type: Embedding, Model: ${modelConfig.name} (${modelConfig.modelId}), Text count: ${texts.length}`,
         );
         this.logger.log(
           `Calling embedding API (attempt ${attempt}/${MAX_RETRIES}): ${apiUrl}`,
@@ -208,13 +199,11 @@ export class EmbeddingService {
 
               const firstResult = await this.getEmbeddingsForBatch(
                 firstHalf,
-                userId,
                 modelConfig,
                 Math.floor(maxBatchSize / 2),
               );
               const secondResult = await this.getEmbeddingsForBatch(
                 secondHalf,
-                userId,
                 modelConfig,
                 Math.floor(maxBatchSize / 2),
               );

+ 8 - 41
server/src/knowledge-base/knowledge-base.service.ts

@@ -515,11 +515,8 @@ export class KnowledgeBaseService {
       );
       const visionModelId = settings?.selectedVisionId;
       if (visionModelId) {
-        const visionModel = await this.modelConfigService.findOne(
-          visionModelId,
-          userId,
-          tenantId,
-        );
+        const visionModel =
+          await this.modelConfigService.findOne(visionModelId);
         if (
           visionModel &&
           visionModel.type === 'vision' &&
@@ -613,11 +610,7 @@ export class KnowledgeBaseService {
       return this.processFastMode(kb, userId, tenantId, config);
     }
 
-    const visionModel = await this.modelConfigService.findOne(
-      visionModelId,
-      userId,
-      tenantId,
-    );
+    const visionModel = await this.modelConfigService.findOne(visionModelId);
     if (
       !visionModel ||
       visionModel.type !== 'vision' ||
@@ -719,8 +712,6 @@ export class KnowledgeBaseService {
     // Check index existence - get actual model dimensions
     const actualDimensions = await this.getActualModelDimensions(
       embeddingModelId,
-      userId,
-      tenantId,
     );
     await this.elasticsearchService.createIndexIfNotExists(actualDimensions);
 
@@ -735,7 +726,6 @@ export class KnowledgeBaseService {
         // Generate vectors
         const embeddings = await this.embeddingService.getEmbeddings(
           texts,
-          userId,
           embeddingModelId,
         );
 
@@ -846,7 +836,6 @@ export class KnowledgeBaseService {
         kb.chunkSize,
         kb.chunkOverlap,
         kb.embeddingModelId,
-        userId,
       );
       this.logger.debug(`File ${kbId}: Chunk config validated.`);
 
@@ -876,7 +865,6 @@ export class KnowledgeBaseService {
         validatedConfig.chunkSize,
         validatedConfig.chunkOverlap,
         kb.embeddingModelId,
-        userId,
       );
       this.logger.log(`Chunk config: ${configSummary}`);
       this.logger.log(
@@ -917,8 +905,6 @@ export class KnowledgeBaseService {
       const recommendedBatchSize =
         await this.chunkConfigService.getRecommendedBatchSize(
           kb.embeddingModelId,
-          userId,
-          tenantId,
           parseInt(process.env.CHUNK_BATCH_SIZE || '100'),
         );
 
@@ -937,8 +923,6 @@ export class KnowledgeBaseService {
       // 6. Get actual model dimensions and check index exists
       const actualDimensions = await this.getActualModelDimensions(
         kb.embeddingModelId,
-        userId,
-        tenantId,
       );
       await this.elasticsearchService.createIndexIfNotExists(actualDimensions);
 
@@ -968,7 +952,6 @@ export class KnowledgeBaseService {
               const chunkTexts = batch.map((chunk) => chunk.content);
               const embeddings = await this.embeddingService.getEmbeddings(
                 chunkTexts,
-                userId,
                 kb.embeddingModelId,
               );
 
@@ -1044,7 +1027,6 @@ export class KnowledgeBaseService {
               try {
                 const embeddings = await this.embeddingService.getEmbeddings(
                   [chunk.content], // Single text
-                  userId,
                   kb.embeddingModelId,
                 );
 
@@ -1111,7 +1093,6 @@ export class KnowledgeBaseService {
               const batchTexts = batch.map((c) => c.content);
               const embeddings = await this.embeddingService.getEmbeddings(
                 batchTexts,
-                userId,
                 kb.embeddingModelId,
               );
 
@@ -1161,7 +1142,6 @@ export class KnowledgeBaseService {
                 try {
                   const embeddings = await this.embeddingService.getEmbeddings(
                     [chunk.content], // Single text
-                    userId,
                     kb.embeddingModelId,
                   );
 
@@ -1222,7 +1202,6 @@ export class KnowledgeBaseService {
           try {
             const embeddings = await this.embeddingService.getEmbeddings(
               chunkTexts,
-              userId,
               kb.embeddingModelId,
             );
 
@@ -1273,7 +1252,6 @@ export class KnowledgeBaseService {
                 try {
                   const embeddings = await this.embeddingService.getEmbeddings(
                     [chunk.content], // Single text
-                    userId,
                     kb.embeddingModelId,
                   );
 
@@ -1712,8 +1690,6 @@ export class KnowledgeBaseService {
    */
   private async getActualModelDimensions(
     embeddingModelId: string,
-    userId: string,
-    tenantId: string,
   ): Promise<number> {
     const defaultDimensions = parseInt(
       process.env.DEFAULT_VECTOR_DIMENSIONS || '2560',
@@ -1721,11 +1697,8 @@ export class KnowledgeBaseService {
 
     try {
       // 1. Prioritize getting from model config
-      const modelConfig = await this.modelConfigService.findOne(
-        embeddingModelId,
-        userId,
-        tenantId,
-      );
+      const modelConfig =
+        await this.modelConfigService.findOne(embeddingModelId);
 
       if (modelConfig && modelConfig.dimensions) {
         this.logger.log(
@@ -1738,7 +1711,6 @@ export class KnowledgeBaseService {
       this.logger.log(`Probing model dimensions: ${embeddingModelId}`);
       const probeEmbeddings = await this.embeddingService.getEmbeddings(
         ['probe'],
-        userId,
         embeddingModelId,
       );
 
@@ -1751,14 +1723,9 @@ export class KnowledgeBaseService {
         // Update model config for next use
         if (modelConfig) {
           try {
-            await this.modelConfigService.update(
-              userId,
-              tenantId,
-              modelConfig.id,
-              {
-                dimensions: actualDimensions,
-              },
-            );
+            await this.modelConfigService.update(modelConfig.id, {
+              dimensions: actualDimensions,
+            });
             this.logger.log(
               `Updated model ${modelConfig.name} dimension config to ${actualDimensions}`,
             );

+ 2 - 3
server/src/model-config/dto/model-config-response.dto.ts

@@ -1,5 +1,5 @@
 // server/src/model-config/dto/model-config-response.dto.ts
-import { Exclude } from 'class-transformer';
+import { Exclude, Expose, Transform } from 'class-transformer';
 import { ModelConfig } from '../model-config.entity';
 
 export class ModelConfigResponseDto {
@@ -9,13 +9,12 @@ export class ModelConfigResponseDto {
   modelId: string;
   baseUrl?: string;
 
-  @Exclude() // Exclude API key from being returned to the client
+  @Transform(({ value }) => (value ? '********' : undefined))
   apiKey?: string;
 
   type: string;
   isEnabled?: boolean;
   isDefault?: boolean;
-  userId: string;
   createdAt: Date;
   updatedAt: Date;
 

+ 10 - 36
server/src/model-config/model-config.controller.ts

@@ -32,49 +32,32 @@ export class ModelConfigController {
   @Post()
   @HttpCode(HttpStatus.CREATED)
   async create(
-    @Req() req,
     @Body() createModelConfigDto: CreateModelConfigDto,
   ): Promise<ModelConfigResponseDto> {
-    const modelConfig = await this.modelConfigService.create(
-      req.user.id,
-      req.user.tenantId,
-      createModelConfigDto,
-    );
+    const modelConfig =
+      await this.modelConfigService.create(createModelConfigDto);
     return plainToClass(ModelConfigResponseDto, modelConfig);
   }
 
   @Get()
-  async findAll(@Req() req): Promise<ModelConfigResponseDto[]> {
-    const modelConfigs = await this.modelConfigService.findAll(
-      req.user.id,
-      req.user.tenantId,
-    );
+  async findAll(): Promise<ModelConfigResponseDto[]> {
+    const modelConfigs = await this.modelConfigService.findAll();
     return modelConfigs.map((mc) => plainToClass(ModelConfigResponseDto, mc));
   }
 
   @Get(':id')
-  async findOne(
-    @Req() req,
-    @Param('id') id: string,
-  ): Promise<ModelConfigResponseDto> {
-    const modelConfig = await this.modelConfigService.findOne(
-      id,
-      req.user.id,
-      req.user.tenantId,
-    );
+  async findOne(@Param('id') id: string): Promise<ModelConfigResponseDto> {
+    const modelConfig = await this.modelConfigService.findOne(id);
     return plainToClass(ModelConfigResponseDto, modelConfig);
   }
 
   @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
   @Put(':id')
   async update(
-    @Req() req,
     @Param('id') id: string,
     @Body() updateModelConfigDto: UpdateModelConfigDto,
   ): Promise<ModelConfigResponseDto> {
     const modelConfig = await this.modelConfigService.update(
-      req.user.id,
-      req.user.tenantId,
       id,
       updateModelConfigDto,
     );
@@ -84,23 +67,14 @@ export class ModelConfigController {
   @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
   @Delete(':id')
   @HttpCode(HttpStatus.NO_CONTENT)
-  async remove(@Req() req, @Param('id') id: string): Promise<void> {
-    await this.modelConfigService.remove(req.user.id, req.user.tenantId, id);
+  async remove(@Param('id') id: string): Promise<void> {
+    await this.modelConfigService.remove(id);
   }
 
   @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
   @Patch(':id/set-default')
-  async setDefault(
-    @Req() req,
-    @Param('id') id: string,
-  ): Promise<ModelConfigResponseDto> {
-    const userId = req.user.id;
-    const tenantId = req.user.tenantId;
-    const modelConfig = await this.modelConfigService.setDefault(
-      userId,
-      tenantId,
-      id,
-    );
+  async setDefault(@Param('id') id: string): Promise<ModelConfigResponseDto> {
+    const modelConfig = await this.modelConfigService.setDefault(id);
     return plainToClass(ModelConfigResponseDto, modelConfig);
   }
 }

+ 0 - 20
server/src/model-config/model-config.entity.ts

@@ -3,12 +3,9 @@ import {
   Column,
   CreateDateColumn,
   Entity,
-  JoinColumn,
-  ManyToOne,
   PrimaryGeneratedColumn,
   UpdateDateColumn,
 } from 'typeorm';
-import { User } from '../user/user.entity';
 
 @Entity('model_configs')
 export class ModelConfig {
@@ -77,23 +74,6 @@ export class ModelConfig {
   @Column({ type: 'text', nullable: true })
   providerName?: string;
 
-  // ==================== Existing Fields ====================
-
-  @Column({ type: 'text', nullable: true })
-  userId: string;
-
-  // null = global/system model (visible to all tenants)
-  // set = private to this tenant
-  @Column({ type: 'text', nullable: true, name: 'tenant_id' })
-  tenantId: string;
-
-  @ManyToOne(() => User, (user) => user.modelConfigs, {
-    onDelete: 'CASCADE',
-    nullable: true,
-  })
-  @JoinColumn({ name: 'userId' })
-  user: User | null;
-
   @CreateDateColumn({ name: 'created_at' })
   createdAt: Date;
 

+ 13 - 85
server/src/model-config/model-config.service.ts

@@ -27,47 +27,22 @@ export class ModelConfigService {
   ) {}
 
   async create(
-    userId: string,
-    tenantId: string,
     createModelConfigDto: CreateModelConfigDto,
   ): Promise<ModelConfig> {
     const modelConfig = this.modelConfigRepository.create({
       ...createModelConfigDto,
-      userId,
-      tenantId,
     });
     return this.modelConfigRepository.save(modelConfig);
   }
 
-  async findAll(userId: string, tenantId: string): Promise<ModelConfig[]> {
-    return this.modelConfigRepository
-      .createQueryBuilder('model')
-      .where(
-        'model.tenantId = :tenantId OR model.tenantId IS NULL OR model.tenantId = :globalTenantId',
-        {
-          tenantId,
-          globalTenantId: GLOBAL_TENANT_ID,
-        },
-      )
-      .getMany();
+  async findAll(): Promise<ModelConfig[]> {
+    return this.modelConfigRepository.find();
   }
 
-  async findOne(
-    id: string,
-    userId: string,
-    tenantId: string,
-  ): Promise<ModelConfig> {
-    const modelConfig = await this.modelConfigRepository
-      .createQueryBuilder('model')
-      .where('model.id = :id', { id })
-      .andWhere(
-        '(model.tenantId = :tenantId OR model.tenantId IS NULL OR model.tenantId = :globalTenantId)',
-        {
-          tenantId,
-          globalTenantId: GLOBAL_TENANT_ID,
-        },
-      )
-      .getOne();
+  async findOne(id: string): Promise<ModelConfig> {
+    const modelConfig = await this.modelConfigRepository.findOne({
+      where: { id },
+    });
 
     if (!modelConfig) {
       throw new NotFoundException(
@@ -77,44 +52,15 @@ export class ModelConfigService {
     return modelConfig;
   }
 
-  async findByType(
-    userId: string,
-    tenantId: string,
-    type: string,
-  ): Promise<ModelConfig[]> {
-    return this.modelConfigRepository
-      .createQueryBuilder('model')
-      .where('model.type = :type', { type })
-      .andWhere(
-        '(model.tenantId = :tenantId OR model.tenantId IS NULL OR model.tenantId = :globalTenantId)',
-        {
-          tenantId,
-          globalTenantId: GLOBAL_TENANT_ID,
-        },
-      )
-      .getMany();
+  async findByType(type: string): Promise<ModelConfig[]> {
+    return this.modelConfigRepository.find({ where: { type } });
   }
 
   async update(
-    userId: string,
-    tenantId: string,
     id: string,
     updateModelConfigDto: UpdateModelConfigDto,
   ): Promise<ModelConfig> {
-    const modelConfig = await this.findOne(id, userId, tenantId);
-
-    if (!modelConfig) {
-      throw new NotFoundException(
-        this.i18nService.formatMessage('modelConfigNotFound', { id }),
-      );
-    }
-
-    // Only allow updating if it belongs to the tenant, or if it's a global admin (not fully implemented, so we check tenantId)
-    if (modelConfig.tenantId && modelConfig.tenantId !== tenantId) {
-      throw new ForbiddenException(
-        this.i18nService.getMessage('cannotUpdateOtherTenantModel'),
-      );
-    }
+    const modelConfig = await this.findOne(id);
 
     // Update the model
     const updated = this.modelConfigRepository.merge(
@@ -124,14 +70,7 @@ export class ModelConfigService {
     return this.modelConfigRepository.save(updated);
   }
 
-  async remove(userId: string, tenantId: string, id: string): Promise<void> {
-    // Only allow removing if it exists and accessible in current tenant context
-    const model = await this.findOne(id, userId, tenantId);
-    if (model.tenantId && model.tenantId !== tenantId) {
-      throw new ForbiddenException(
-        this.i18nService.getMessage('cannotDeleteOtherTenantModel'),
-      );
-    }
+  async remove(id: string): Promise<void> {
     const result = await this.modelConfigRepository.delete({ id });
     if (result.affected === 0) {
       throw new NotFoundException(
@@ -143,26 +82,15 @@ export class ModelConfigService {
   /**
    * Set the specified model as default
    */
-  async setDefault(
-    userId: string,
-    tenantId: string,
-    id: string,
-  ): Promise<ModelConfig> {
-    const modelConfig = await this.findOne(id, userId, tenantId);
+  async setDefault(id: string): Promise<ModelConfig> {
+    const modelConfig = await this.findOne(id);
 
-    // Clear default flag for other models of the same type (within current tenant or global)
+    // Clear default flag for other models of the same type
     await this.modelConfigRepository
       .createQueryBuilder()
       .update(ModelConfig)
       .set({ isDefault: false })
       .where('type = :type', { type: modelConfig.type })
-      .andWhere(
-        '(tenantId = :tenantId OR tenantId IS NULL OR tenantId = :globalTenantId)',
-        {
-          tenantId,
-          globalTenantId: GLOBAL_TENANT_ID,
-        },
-      )
       .execute();
 
     modelConfig.isDefault = true;

+ 0 - 2
server/src/rag/rag.service.ts

@@ -59,9 +59,7 @@ export class RagService {
       try {
         const vectors = await this.embeddingService.getEmbeddings(
           [query],
-          userId,
           embeddingModelId,
-          tenantId,
         );
         queryVector = vectors[0];
       } catch (error) {

+ 1 - 5
server/src/rag/rerank.service.ts

@@ -42,11 +42,7 @@ export class RerankService {
     let modelConfig;
     try {
       // 1. Get model config
-      modelConfig = await this.modelConfigService.findOne(
-        rerankModelId,
-        userId,
-        tenantId || 'default',
-      );
+      modelConfig = await this.modelConfigService.findOne(rerankModelId);
 
       if (!modelConfig || modelConfig.type !== ModelType.RERANK) {
         this.logger.warn(`Invalid rerank model config: ${rerankModelId}`);

+ 0 - 3
server/src/user/user.entity.ts

@@ -66,9 +66,6 @@ export class User {
   @UpdateDateColumn({ name: 'updated_at' })
   updatedAt: Date;
 
-  @OneToMany(() => ModelConfig, (modelConfig) => modelConfig.user)
-  modelConfigs: ModelConfig[];
-
   @OneToOne(() => UserSetting, (setting) => setting.user)
   userSetting: UserSetting;
 

+ 2 - 1
server/src/user/user.service.ts

@@ -177,7 +177,7 @@ export class UserService implements OnModuleInit {
     const user = await this.usersRepository.save({
       username,
       password: hashedPassword,
-      displayName,
+      displayName: displayName || username,
       isAdmin,
       tenantId: tenantId ?? undefined,
     } as any);
@@ -398,6 +398,7 @@ export class UserService implements OnModuleInit {
       await this.usersRepository.save({
         username: 'admin',
         password: hashedPassword,
+        displayName: 'Admin',
         isAdmin: true,
         role: UserRole.SUPER_ADMIN,
       });

+ 1 - 5
server/src/vision-pipeline/vision-pipeline-cost-aware.service.ts

@@ -219,11 +219,7 @@ export class VisionPipelineCostAwareService {
     modelId: string,
     tenantId?: string,
   ): Promise<VisionModelConfig> {
-    const config = await this.modelConfigService.findOne(
-      modelId,
-      userId,
-      tenantId || 'default',
-    );
+    const config = await this.modelConfigService.findOne(modelId);
 
     if (!config) {
       throw new Error(`Model config not found: ${modelId}`);

+ 0 - 6
server/src/vision-pipeline/vision-pipeline.service.ts

@@ -104,9 +104,7 @@ export class VisionPipelineService {
       // Step 3: Get Vision model configuration
       this.logger.log('🤖 Step 3/4: Preparation of Vision model');
       const modelConfig = await this.getVisionModelConfig(
-        options.userId,
         options.modelId,
-        options.tenantId,
       );
       this.logger.log(
         `✅ Vision model configuration completed: ${modelConfig.modelId}`,
@@ -225,14 +223,10 @@ export class VisionPipelineService {
    * Get Vision model configuration
    */
   private async getVisionModelConfig(
-    userId: string,
     modelId: string,
-    tenantId?: string,
   ): Promise<VisionModelConfig> {
     const config = await this.modelConfigService.findOne(
       modelId,
-      userId,
-      tenantId || 'default',
     );
 
     if (!config) {

+ 7 - 4
web/components/ChatInterface.tsx

@@ -32,6 +32,7 @@ interface ChatInterfaceProps {
   onPreviewSource?: (source: ChatSource) => void;
   onOpenFile?: (source: ChatSource) => void;
   onHistoryIdCreated?: (historyId: string) => void;
+  authToken?: string; // Add optional auth token prop
 }
 
 const ChatInterface: React.FC<ChatInterfaceProps> = ({
@@ -50,7 +51,8 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
   onHistoryMessagesLoaded,
   onPreviewSource,
   onOpenFile,
-  onHistoryIdCreated
+  onHistoryIdCreated,
+  authToken
 }) => {
   const { t, language } = useLanguage();
   const [messages, setMessages] = useState<Message[]>([]);
@@ -167,8 +169,9 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
     setIsLoading(true);
     setMessages((prev) => [...prev, newMessage]);
 
-    const authToken = localStorage.getItem('authToken');
-    if (!authToken) {
+    const effectiveToken = authToken || localStorage.getItem('kb_api_key') || localStorage.getItem('authToken');
+    
+    if (!effectiveToken) {
       const errorMsg: Message = {
         id: generateUUID(),
         role: Role.MODEL,
@@ -204,7 +207,7 @@ const ChatInterface: React.FC<ChatInterfaceProps> = ({
       const stream = chatService.streamChat(
         userText,
         history,
-        authToken,
+        effectiveToken,
         language,
         settings.selectedEmbeddingId,
         settings.selectedLLMId, // Pass selected LLM ID

+ 1 - 0
web/components/views/ChatView.tsx

@@ -368,6 +368,7 @@ export const ChatView: React.FC<ChatViewProps> = ({
                         onHistoryMessagesLoaded={() => setHistoryMessages(null)}
                         onHistoryIdCreated={setCurrentHistoryId}
                         onPreviewSource={setPreviewSource}
+                        authToken={authToken}
                         onOpenFile={(source) => {
                             if (source.fileId) {
                                 if (isFormatSupportedForPreview(source.fileName)) {

+ 6 - 10
web/services/apiClient.ts

@@ -135,17 +135,13 @@ class ApiClient {
   
   // Legacy compatibility method — returns raw Response for streaming and other special cases
   async request(path: string, options: RequestInit = {}): Promise<Response> {
-    const apiKey = localStorage.getItem('kb_api_key');
-    const activeTenantId = localStorage.getItem('kb_active_tenant_id');
-    const token = localStorage.getItem('authToken');
+    const authHeaders = this.getAuthHeaders();
     const headers = new Headers(options.headers);
-
-    if (apiKey) headers.set('x-api-key', apiKey);
-    if (activeTenantId) headers.set('x-tenant-id', activeTenantId);
-    if (token) headers.set('Authorization', `Bearer ${token}`);
-
-    const language = localStorage.getItem('userLanguage') || 'zh';
-    headers.set('x-user-language', language);
+    
+    // Merge auth headers into request headers
+    Object.entries(authHeaders).forEach(([key, value]) => {
+      headers.set(key, value);
+    });
 
     let url = path;
     if (!path.startsWith('http')) {