chore: 清理 docs 目录冗余文档(55个→9个)
删除: - docs/1.0/ 全部22个(旧Simple KB文档,已被替代) - docs/2.0/ 全部2个(空文件/过时) - docs/design/ 全部4个(旧功能设计) - docs/plans/ 旧版5个(保留v2.0设计文档) - docs/superpowers/ 全部4个(飞书集成文档) - docs/根目录杂项 8个(开发规范/快速参考/调试/飞书/Git设置) - docs/3.0/knowledge_graph_analysis.md(不相关) 保留: - docs/3.0/ 考核工作流设计 2个 - docs/plans/ v2.0设计文档 1个 - docs/tests/ 测试文档 6个 合计: 55个 → 9个 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
-444
@@ -1,444 +0,0 @@
|
||||
# API リファレンス
|
||||
|
||||
## 基本情報
|
||||
|
||||
- **ベース URL**: `http://localhost:3000`
|
||||
- **認証方式**: JWT Bearer トークン
|
||||
- **Content-Type**: `application/json`
|
||||
|
||||
## 認証 API
|
||||
|
||||
### ユーザー登録
|
||||
|
||||
```http
|
||||
POST /auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "string",
|
||||
"password": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**レスポンス**:
|
||||
|
||||
```json
|
||||
{
|
||||
"message": "ユーザーが正常に作成されました",
|
||||
"user": {
|
||||
"id": "string",
|
||||
"username": "string",
|
||||
"isAdmin": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### ユーザーログイン
|
||||
|
||||
```http
|
||||
POST /auth/login
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "string",
|
||||
"password": "string"
|
||||
}
|
||||
```
|
||||
|
||||
**レスポンス**:
|
||||
|
||||
```json
|
||||
{
|
||||
"access_token": "jwt_token_string",
|
||||
"user": {
|
||||
"id": "string",
|
||||
"username": "string",
|
||||
"isAdmin": false
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### パスワード変更
|
||||
|
||||
```http
|
||||
POST /auth/change-password
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"currentPassword": "string",
|
||||
"newPassword": "string"
|
||||
}
|
||||
```
|
||||
|
||||
## モデル設定 API
|
||||
|
||||
### モデル一覧の取得
|
||||
|
||||
```http
|
||||
GET /model-configs
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**レスポンス**:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"provider": "openai|gemini",
|
||||
"modelId": "string",
|
||||
"baseUrl": "string",
|
||||
"type": "llm|embedding|rerank",
|
||||
"supportsVision": boolean
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### モデル設定の作成
|
||||
|
||||
```http
|
||||
POST /model-configs
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "string",
|
||||
"provider": "openai|gemini",
|
||||
"modelId": "string",
|
||||
"baseUrl": "string",
|
||||
"apiKey": "string",
|
||||
"type": "llm|embedding|rerank",
|
||||
"supportsVision": boolean
|
||||
}
|
||||
```
|
||||
|
||||
### モデル設定の更新
|
||||
|
||||
```http
|
||||
PUT /model-configs/:id
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "string",
|
||||
"apiKey": "string",
|
||||
// ... その他のフィールド
|
||||
}
|
||||
```
|
||||
|
||||
### モデル設定の削除
|
||||
|
||||
```http
|
||||
DELETE /model-configs/:id
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
## ナレッジベース API
|
||||
|
||||
### ファイルのアップロード
|
||||
|
||||
```http
|
||||
POST /upload
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
{
|
||||
"file": File,
|
||||
"chunkSize": number,
|
||||
"chunkOverlap": number,
|
||||
"embeddingModelId": "string",
|
||||
"mode": "fast|precise" // 処理モード
|
||||
}
|
||||
```
|
||||
|
||||
**レスポンス**:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"originalName": "string",
|
||||
"size": number,
|
||||
"mimetype": "string",
|
||||
"status": "pending|indexing|completed|failed"
|
||||
}
|
||||
```
|
||||
|
||||
### ファイル一覧の取得
|
||||
|
||||
```http
|
||||
GET /knowledge-bases
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**レスポンス**:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "string",
|
||||
"name": "string",
|
||||
"originalName": "string",
|
||||
"size": number,
|
||||
"mimetype": "string",
|
||||
"status": "pending|indexing|completed|failed",
|
||||
"createdAt": "datetime"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### ファイルの削除
|
||||
|
||||
```http
|
||||
DELETE /knowledge-bases/:id
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
### ナレッジベースの全消去
|
||||
|
||||
```http
|
||||
DELETE /knowledge-bases/clear
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
## チャット API
|
||||
|
||||
### ストリーミングチャット
|
||||
|
||||
```http
|
||||
POST /chat/stream
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "string",
|
||||
"history": [
|
||||
{
|
||||
"role": "user|assistant",
|
||||
"content": "string"
|
||||
}
|
||||
],
|
||||
"userLanguage": "zh|en|ja"
|
||||
}
|
||||
```
|
||||
|
||||
**レスポンス**: Server-Sent Events (SSE)
|
||||
|
||||
```
|
||||
data: {"type": "content", "data": "ナレッジベースを検索中..."}
|
||||
|
||||
data: {"type": "content", "data": "関連情報が見つかりました..."}
|
||||
|
||||
data: {"type": "content", "data": "回答内容の断片"}
|
||||
|
||||
data: {"type": "sources", "data": [
|
||||
{
|
||||
"fileName": "string",
|
||||
"content": "string",
|
||||
"score": number,
|
||||
"chunkIndex": number
|
||||
}
|
||||
]}
|
||||
|
||||
data: [DONE]
|
||||
```
|
||||
|
||||
## ユーザー設定 API
|
||||
|
||||
### ユーザー設定の取得
|
||||
|
||||
```http
|
||||
GET /user-settings
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**レスポンス**:
|
||||
|
||||
```json
|
||||
{
|
||||
"selectedLLMId": "string",
|
||||
"selectedEmbeddingId": "string",
|
||||
"selectedRerankId": "string",
|
||||
"temperature": number,
|
||||
"maxTokens": number,
|
||||
"topK": number,
|
||||
"enableRerank": boolean,
|
||||
"similarityThreshold": number,
|
||||
"enableFullTextSearch": boolean,
|
||||
"language": "zh|en|ja"
|
||||
}
|
||||
```
|
||||
|
||||
### ユーザー設定の更新
|
||||
|
||||
```http
|
||||
PUT /user-settings
|
||||
Authorization: Bearer <token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"selectedLLMId": "string",
|
||||
"temperature": number,
|
||||
"maxTokens": number,
|
||||
// ... その他の設定フィールド
|
||||
}
|
||||
```
|
||||
|
||||
## Vision Pipeline API
|
||||
|
||||
### 推奨モードの取得
|
||||
|
||||
```http
|
||||
GET /api/vision/recommend-mode?file=xxx&size=xxx
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**レスポンス**:
|
||||
|
||||
```json
|
||||
{
|
||||
"recommendedMode": "precise",
|
||||
"reason": "ファイルサイズが大きいため、高精度モードを推奨します",
|
||||
"estimatedCost": 0.5,
|
||||
"estimatedTime": 60,
|
||||
"warnings": ["処理時間が長くなる可能性があります", "API 利用料が発生します"]
|
||||
}
|
||||
```
|
||||
|
||||
### LibreOffice 変換サービス
|
||||
|
||||
```http
|
||||
POST /libreoffice/convert
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
{
|
||||
"file": File
|
||||
}
|
||||
```
|
||||
|
||||
**レスポンス**:
|
||||
|
||||
```json
|
||||
{
|
||||
"pdf_path": "/uploads/document.pdf",
|
||||
"converted": true,
|
||||
"original": "document.docx",
|
||||
"file_size": 102400
|
||||
}
|
||||
```
|
||||
|
||||
### ヘルスチェック
|
||||
|
||||
```http
|
||||
GET /libreoffice/health
|
||||
```
|
||||
|
||||
**レスポンス**:
|
||||
|
||||
```json
|
||||
{
|
||||
"status": "healthy",
|
||||
"service": "libreoffice-converter",
|
||||
"version": "1.0.0",
|
||||
"uptime": 3600.5
|
||||
}
|
||||
```
|
||||
|
||||
## ユーザー管理 API (管理者用)
|
||||
|
||||
### ユーザー一覧の取得
|
||||
|
||||
```http
|
||||
GET /users
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
### ユーザーの作成
|
||||
|
||||
```http
|
||||
POST /users
|
||||
Authorization: Bearer <admin_token>
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"username": "string",
|
||||
"password": "string",
|
||||
"isAdmin": boolean
|
||||
}
|
||||
```
|
||||
|
||||
### ユーザーの削除
|
||||
|
||||
```http
|
||||
DELETE /users/:id
|
||||
Authorization: Bearer <admin_token>
|
||||
```
|
||||
|
||||
## エラーレスポンス形式
|
||||
|
||||
```json
|
||||
{
|
||||
"statusCode": number,
|
||||
"message": "string",
|
||||
"error": "string"
|
||||
}
|
||||
```
|
||||
|
||||
## ステータスコードの説明
|
||||
|
||||
- `200` - 成功
|
||||
- `201` - 作成成功
|
||||
- `400` - リクエストパラメータの不正
|
||||
- `401` - 認証エラー / トークン無効
|
||||
- `403` - 権限不足
|
||||
- `404` - リソースが見つかりません
|
||||
- `409` - リソースの競合
|
||||
- `500` - サーバー内部エラー
|
||||
|
||||
## 実装例
|
||||
|
||||
### JavaScript/TypeScript
|
||||
|
||||
```javascript
|
||||
// ログイン
|
||||
const loginResponse = await fetch('/auth/login', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
username: 'user',
|
||||
password: 'password'
|
||||
})
|
||||
});
|
||||
|
||||
const { access_token } = await loginResponse.json();
|
||||
|
||||
// ファイル一覧の取得
|
||||
const filesResponse = await fetch('/knowledge-bases', {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${access_token}`
|
||||
}
|
||||
});
|
||||
|
||||
const files = await filesResponse.json();
|
||||
|
||||
// ストリーミングチャット
|
||||
const chatResponse = await fetch('/chat/stream', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${access_token}`,
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: 'こんにちは',
|
||||
history: [],
|
||||
userLanguage: 'ja'
|
||||
})
|
||||
});
|
||||
|
||||
const reader = chatResponse.body.getReader();
|
||||
// SSE ストリームの処理...
|
||||
```
|
||||
@@ -1,361 +0,0 @@
|
||||
# チャンクサイズの制限に関する完全スキーム
|
||||
|
||||
## 🎯 設計目標
|
||||
|
||||
**主要な問題の解決:**
|
||||
|
||||
1. ✅ チャンクサイズがモデルの入力制限を超えないようにする
|
||||
2. ✅ 環境変数でグローバルな上限を設定可能にする
|
||||
3. ✅ フロントエンドのスライダーで動的に制限し、上限を超えられないようにする
|
||||
4. ✅ バックエンドで自動検証と調整を行う
|
||||
|
||||
---
|
||||
|
||||
## 📋 設定階層構造
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 環境変数の設定 (server/.env) │
|
||||
│ MAX_CHUNK_SIZE=8191 │
|
||||
│ MAX_OVERLAP_SIZE=200 │
|
||||
└─────────────────────────────────────────┘
|
||||
↓ 優先度1(最も厳格)
|
||||
┌─────────────────────────────────────────┐
|
||||
│ モデル制限設定 (ChunkConfigService) │
|
||||
│ OpenAI: 8191 tokens │
|
||||
│ Gemini: 2048 tokens │
|
||||
└─────────────────────────────────────────┘
|
||||
↓ 優先度2
|
||||
┌─────────────────────────────────────────┐
|
||||
│ ユーザー設定 (フロントエンドスライダー)│
|
||||
│ chunkSize: 200 tokens │
|
||||
│ chunkOverlap: 40 tokens │
|
||||
└─────────────────────────────────────────┘
|
||||
↓ 最終検証
|
||||
┌─────────────────────────────────────────┐
|
||||
│ 実際に適用される値 (自動調整) │
|
||||
└─────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 環境変数の設定
|
||||
|
||||
### server/.env
|
||||
|
||||
```env
|
||||
# チャンクサイズの上限 (tokens)
|
||||
# 使用する埋め込みモデルに合わせて設定
|
||||
# OpenAI text-embedding-3-large: 8191
|
||||
# OpenAI text-embedding-3-small: 8191
|
||||
# Google Gemini embedding-001: 2048
|
||||
MAX_CHUNK_SIZE=8191
|
||||
|
||||
# チャンクオーバーラップの上限 (tokens)
|
||||
# チャンクサイズの 10-20% を推奨
|
||||
MAX_OVERLAP_SIZE=200
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🏗️ アーキテクチャの実装
|
||||
|
||||
### 1. ChunkConfigService (バックエンドコア)
|
||||
|
||||
```typescript
|
||||
// 環境変数から上限を読み込む
|
||||
private readonly envMaxChunkSize: number;
|
||||
private readonly envMaxOverlapSize: number;
|
||||
|
||||
// 主要なモデルの制限
|
||||
private readonly MODEL_LIMITS = {
|
||||
'text-embedding-3-large': {
|
||||
maxInputTokens: 8191,
|
||||
maxBatchSize: 2048,
|
||||
expectedDimensions: 3072,
|
||||
},
|
||||
'embedding-001': {
|
||||
maxInputTokens: 2048,
|
||||
maxBatchSize: 100,
|
||||
expectedDimensions: 768,
|
||||
},
|
||||
};
|
||||
|
||||
// 最終的な上限を計算
|
||||
const effectiveMaxChunkSize = Math.min(
|
||||
this.envMaxChunkSize, // 環境変数
|
||||
limits.maxInputTokens // モデルの制限
|
||||
);
|
||||
```
|
||||
|
||||
### 2. 検証ロジック
|
||||
|
||||
```typescript
|
||||
async validateChunkConfig(chunkSize, chunkOverlap, modelId, userId) {
|
||||
const warnings = [];
|
||||
|
||||
// 1. 最終的な上限を計算
|
||||
const effectiveMaxChunkSize = Math.min(
|
||||
this.envMaxChunkSize,
|
||||
limits.maxInputTokens
|
||||
);
|
||||
|
||||
// 2. チャンクサイズの検証
|
||||
if (chunkSize > effectiveMaxChunkSize) {
|
||||
warnings.push(`上限 ${effectiveMaxChunkSize} を超えています`);
|
||||
chunkSize = effectiveMaxChunkSize;
|
||||
}
|
||||
|
||||
// 3. オーバーラップサイズの検証
|
||||
const maxOverlap = Math.min(
|
||||
this.envMaxOverlapSize,
|
||||
Math.floor(chunkSize * 0.5)
|
||||
);
|
||||
if (chunkOverlap > maxOverlap) {
|
||||
warnings.push(`オーバーラップが上限 ${maxOverlap} を超えています`);
|
||||
chunkOverlap = maxOverlap;
|
||||
}
|
||||
|
||||
return {
|
||||
chunkSize,
|
||||
chunkOverlap,
|
||||
warnings,
|
||||
effectiveMaxChunkSize,
|
||||
effectiveMaxOverlapSize,
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 3. API エンドポイント
|
||||
|
||||
```typescript
|
||||
// GET /api/knowledge-bases/chunk-config/limits?embeddingModelId=xxx
|
||||
{
|
||||
"maxChunkSize": 8191,
|
||||
"maxOverlapSize": 200,
|
||||
"defaultChunkSize": 200,
|
||||
"defaultOverlapSize": 40,
|
||||
"modelInfo": {
|
||||
"name": "text-embedding-3-large",
|
||||
"maxInputTokens": 8191,
|
||||
"maxBatchSize": 2048,
|
||||
"expectedDimensions": 3072
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎨 フロントエンドの実装
|
||||
|
||||
### IndexingModal.tsx
|
||||
|
||||
```typescript
|
||||
// 状態管理
|
||||
const [limits, setLimits] = useState(null);
|
||||
const [chunkSize, setChunkSize] = useState(200);
|
||||
const [chunkOverlap, setChunkOverlap] = useState(40);
|
||||
|
||||
// モデル選択時に制限をロード
|
||||
useEffect(() => {
|
||||
if (selectedEmbedding) {
|
||||
const limitData = await chunkConfigService.getLimits(selectedEmbedding, token);
|
||||
setLimits(limitData);
|
||||
|
||||
// 現在の値を自動調整
|
||||
if (chunkSize > limitData.maxChunkSize) {
|
||||
setChunkSize(limitData.maxChunkSize);
|
||||
}
|
||||
}
|
||||
}, [selectedEmbedding]);
|
||||
|
||||
// スライダー変更時の処理
|
||||
const handleChunkSizeChange = (value) => {
|
||||
if (limits && value > limits.maxChunkSize) {
|
||||
showWarning(`最大値は ${limits.maxChunkSize} です`);
|
||||
setChunkSize(limits.maxChunkSize);
|
||||
return;
|
||||
}
|
||||
setChunkSize(value);
|
||||
|
||||
// オーバーラップの自動調整
|
||||
if (chunkOverlap > value * 0.5) {
|
||||
setChunkOverlap(Math.floor(value * 0.5));
|
||||
}
|
||||
};
|
||||
```
|
||||
|
||||
### UI 機能
|
||||
|
||||
1. **動的なスライダー範囲**
|
||||
|
||||
```jsx
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max={limits?.maxChunkSize || 8191} // 動的な上限
|
||||
value={chunkSize}
|
||||
onChange={handleChunkSizeChange}
|
||||
/>
|
||||
```
|
||||
|
||||
2. **制限のリアルタイム表示**
|
||||
|
||||
```
|
||||
チャンクサイズ: 200 tokens (上限: 8191)
|
||||
```
|
||||
|
||||
3. **モデル情報の表示**
|
||||
|
||||
```
|
||||
モデル: text-embedding-3-large
|
||||
チャンク上限: 8191 tokens
|
||||
オーバーラップ上限: 200 tokens
|
||||
バッチ制限: 2048
|
||||
```
|
||||
|
||||
4. **最適化アドバイス**
|
||||
|
||||
```
|
||||
💡 最適化アドバイス
|
||||
• チャンクが大きすぎます (800)。検索精度に影響する可能性があります。
|
||||
• 少なくとも 80 tokens のオーバーラップを推奨します。
|
||||
• 最大値を使用すると、処理速度が低下する可能性があります。
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 ユースケース例
|
||||
|
||||
### シナリオ1: OpenAI + 環境変数による制限
|
||||
|
||||
**設定:**
|
||||
|
||||
```env
|
||||
MAX_CHUNK_SIZE=4000 # モデルより厳格なカスタム制限
|
||||
```
|
||||
|
||||
**ユーザー操作:**
|
||||
|
||||
```
|
||||
1. モデル選択: text-embedding-3-large
|
||||
2. スライダー上限: 4000 (環境変数による制限)
|
||||
3. ユーザー設定: 3000 tokens
|
||||
4. バックエンド検証: ✅ 合格
|
||||
5. 実適用値: 3000 tokens
|
||||
```
|
||||
|
||||
### シナリオ2: Gemini + モデルによる制限
|
||||
|
||||
**設定:**
|
||||
|
||||
```env
|
||||
MAX_CHUNK_SIZE=8191 # 環境変数は緩和
|
||||
```
|
||||
|
||||
**ユーザー操作:**
|
||||
|
||||
```
|
||||
1. モデル選択: embedding-001
|
||||
2. スライダー上限: 2048 (モデル制限の方が厳格)
|
||||
3. ユーザー設定: 1500 tokens
|
||||
4. バックエンド検証: ✅ 合格
|
||||
5. 実適用値: 1500 tokens
|
||||
```
|
||||
|
||||
### シナリオ3: 制限超過時の自動調整
|
||||
|
||||
**ユーザー操作:**
|
||||
|
||||
```
|
||||
1. モデル選択: embedding-001 (制限 2048)
|
||||
2. ユーザー入力: 3000 tokens
|
||||
3. フロントエンド表示: "最大値は 2048 です"
|
||||
4. スライダーを自動的に 2048 に調整
|
||||
5. バックエンド記録: ⚠️ 設定修正ログ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔍 優先順位ルール
|
||||
|
||||
### 上限計算ロジック
|
||||
|
||||
```typescript
|
||||
最終的な上限 = min(環境変数, モデル制限)
|
||||
|
||||
例:
|
||||
- 環境変数: 8191
|
||||
- モデル制限: 2048 (Gemini)
|
||||
- 最終上限: 2048 ✅
|
||||
|
||||
- 環境変数: 4000
|
||||
- モデル制限: 8191 (OpenAI)
|
||||
- 最終上限: 4000 ✅
|
||||
```
|
||||
|
||||
### 検証順序
|
||||
|
||||
```typescript
|
||||
1. チャンクサイズ ≤ 最終上限 かを確認
|
||||
2. チャンクサイズ ≥ 最小値 (50) かを確認
|
||||
3. オーバーラップサイズ ≤ 環境変数の上限 かを確認
|
||||
4. オーバーラップサイズ ≤ チャンクサイズの 50% かを確認
|
||||
5. オーバーラップサイズ ≥ 0 かを確認
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 デプロイ時の推奨設定
|
||||
|
||||
### 開発環境
|
||||
|
||||
```env
|
||||
# テストに適した設定
|
||||
MAX_CHUNK_SIZE=8191
|
||||
MAX_OVERLAP_SIZE=200
|
||||
```
|
||||
|
||||
### 本番環境 (OpenAI)
|
||||
|
||||
```env
|
||||
# 大容量ファイルへの対策を考慮した保守的な設定
|
||||
MAX_CHUNK_SIZE=4000
|
||||
MAX_OVERLAP_SIZE=500
|
||||
```
|
||||
|
||||
### 本番環境 (Gemini)
|
||||
|
||||
```env
|
||||
# モデルの制限に合わせた設定
|
||||
MAX_CHUNK_SIZE=2048
|
||||
MAX_OVERLAP_SIZE=300
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ メリットのまとめ
|
||||
|
||||
| 特徴 | 実装方法 | 効果 |
|
||||
|------|----------|------|
|
||||
| **安全性** | 環境変数 + モデル制限の二重保護 | API の制限を超えない |
|
||||
| **柔軟性** | 環境変数で調整可能 | 異なるデプロイ要件に対応 |
|
||||
| **ユーザー体験** | フロントエンドでの動的制限 | 無効な値を選択できない |
|
||||
| **透明性** | 制限情報をリアルタイム表示 | 設定理由が明確 |
|
||||
| **自動調整** | バックエンドでの検証・修正 | 実行時のエラーを回避 |
|
||||
| **ログ管理** | 詳細な警告情報 | 問題の切り分けがスムーズ |
|
||||
|
||||
---
|
||||
|
||||
## 🎯 結論
|
||||
|
||||
このスキームにより、以下が実現されました:
|
||||
|
||||
1. ✅ **環境変数の設定** - グローバルに制御可能な上限
|
||||
2. ✅ **モデル制限の認識** - 異なるモデルの自動識別
|
||||
3. ✅ **フロントエンドでの制限** - 無効な値を選択不可に
|
||||
4. ✅ **バックエンド検証** - 二重の保険
|
||||
5. ✅ **自動調整** - 制限超過時の自動修正
|
||||
6. ✅ **透明なフィードバック** - 制限理由の表示
|
||||
|
||||
**これで、ユーザーがモデルの制限を超える値を選択することはなくなり、システムが自動的に保護されます!**
|
||||
@@ -1,165 +0,0 @@
|
||||
# 現在の実装状況ドキュメント
|
||||
|
||||
## システムアーキテクチャ
|
||||
|
||||
### 技術スタック
|
||||
|
||||
- **フロントエンド**: React + TypeScript + Vite + Tailwind CSS
|
||||
- **バックエンド**: NestJS + LangChain + Elasticsearch + TypeORM
|
||||
- **データベース**: SQLite (ユーザー、モデル設定、ナレッジベース、ユーザー設定)
|
||||
- **ベクトルストレージ**: Elasticsearch
|
||||
- **ファイル処理**: Apache Tika (高速モード) + Vision Pipeline (高精度モード)
|
||||
- **認証**: JWT
|
||||
- **ドキュメント変換**: LibreOffice + ImageMagick
|
||||
|
||||
### コアモジュール
|
||||
|
||||
#### 1. ユーザー認証システム (Auth)
|
||||
|
||||
- JWT 認証システム
|
||||
- ユーザー登録/ログイン/パスワード変更
|
||||
- ユーザー管理画面
|
||||
- ルート保護と権限制御
|
||||
|
||||
#### 2. モデル設定管理 (ModelConfig)
|
||||
|
||||
- 多様なモデルプロバイダーをサポート:
|
||||
- **OpenAI 互換**: OpenAI API および互換インターフェース(DeepSeek, Claude など)に対応
|
||||
- **Google Gemini**: ネイティブ SDK によるサポート
|
||||
- モデルタイプ:
|
||||
- **LLM**: 対話生成モデル
|
||||
- **Embedding**: ベクトル化モデル
|
||||
- **Rerank**: 再ランキングモデル
|
||||
- ユーザー独自の API キーとモデルパラメータの設定が可能
|
||||
- ビジョン機能のフラグ管理をサポート
|
||||
|
||||
#### 3. ナレッジベース管理 (KnowledgeBase)
|
||||
|
||||
- ファイルのアップロードと保存(日本語ファイル名に対応)
|
||||
- **デュアルモード処理**:
|
||||
- **高速モード**: Apache Tika によるテキスト抽出
|
||||
- **高精度モード**: Vision Pipeline による画像・テキスト混合処理
|
||||
- インテリジェントなドキュメントのチャンク分割とベクトル化
|
||||
- Elasticsearch インデックスとハイブリッド検索
|
||||
- ファイルステータス管理(待機中、インデックス中、完了、失敗)
|
||||
- 数百種類のファイル形式をサポート: PDF, Word, PPT, Excel, Markdown, コードファイル, 画像など
|
||||
|
||||
#### 4. RAG 質問応答システム (Chat)
|
||||
|
||||
- **ストリーミング出力**: Server-Sent Events (SSE) を利用
|
||||
- **インテリジェント検索**: LangChain キーワード抽出 + ES ハイブリッド検索
|
||||
- **類似度フィルタリング**: 関連性の低いコンテンツをフィルタリングするしきい値を設定可能
|
||||
- **引用表示**: ソースの断片、ファイル名、類似度スコアを表示
|
||||
- **多言語サポート**: ユーザーの言語設定に合わせて AI の回答言語を調整
|
||||
|
||||
#### 5. 統合設定管理 (Unified Settings)
|
||||
|
||||
- **統合設定モーダル**: 各種管理機能を一つのタブ形式インターフェースに集約
|
||||
- **一般設定 (General)**: 言語切り替え、パスワード変更
|
||||
- **ユーザー管理 (User)**: ユーザー一覧、ユーザー追加(管理者機能)
|
||||
- **モデル管理 (Model)**: LLM, Embedding, Rerank モデルの設定と管理
|
||||
- **クイックアクセス**: サイドバー下部の「設定」ボタンからワンクリックでアクセス可能
|
||||
- **一貫した体験**: 統一されたフォーム操作とステータスフィードバック
|
||||
|
||||
## チャットフロー
|
||||
|
||||
```
|
||||
ユーザーの質問 → キーワード抽出 → ESハイブリッド検索 → 類似度フィルタリング → コンテキスト構築 → LLMストリーミング生成 → 引用元の表示
|
||||
```
|
||||
|
||||
### 詳細ステップ
|
||||
|
||||
1. **キーワード抽出**
|
||||
- LangChain を使用して、ユーザーの質問から 3-5 個のキーワードを抽出します。
|
||||
- 不要な言葉(「の」「は」「のぼり」など)を除去します。
|
||||
|
||||
2. **ハイブリッド検索**
|
||||
- キーワードを組み合わせてベクトル検索を実行します。
|
||||
- 全文検索を併用して再現率を向上させます。
|
||||
- 類似度しきい値でフィルタリングします。
|
||||
- 最も関連性の高いセグメントを返します。
|
||||
|
||||
3. **ストリーミング生成**
|
||||
- 処理の進捗を表示します(「ナレッジベースを検索中...」など)。
|
||||
- LLM が生成した回答内容をリアルタイムで出力します。
|
||||
- 取得したセグメントに基づき、引用付きの回答を生成します。
|
||||
- 多言語での回答をサポートします。
|
||||
|
||||
4. **引用元の表示**
|
||||
- セグメント内容の要約(最大150文字)を表示します。
|
||||
- 出典元ファイル名を表示します。
|
||||
- 類似度のパーセンテージを表示します。
|
||||
- セグメント番号を表示します。
|
||||
|
||||
## 主要な API エンドポイント
|
||||
|
||||
### 認証関連
|
||||
|
||||
- `POST /auth/login` - ログイン
|
||||
- `POST /auth/register` - ユーザー登録
|
||||
- `POST /auth/change-password` - パスワード変更(設定モーダル内から呼び出し)
|
||||
|
||||
### チャット API
|
||||
|
||||
- `POST /chat/stream` - ストリーミングチャット
|
||||
- ユーザーが設定した LLM モデルと API キーを自動取得
|
||||
- OpenAI 互換インターフェースと Gemini をサポート
|
||||
- SSE ストリーミングレスポンスを返却
|
||||
- 多言語パラメータの受け渡しに対応
|
||||
|
||||
### モデル管理
|
||||
|
||||
- `GET /model-configs` - モデル設定の一覧取得
|
||||
- `POST /model-configs` - モデル設定の作成
|
||||
- `PUT /model-configs/:id` - モデル設定の更新
|
||||
- `DELETE /model-configs/:id` - モデル設定の削除
|
||||
|
||||
### ナレッジベース管理
|
||||
|
||||
- `POST /upload` - ファイルアップロード(チャンクパラメータの設定が可能)
|
||||
- `GET /knowledge-bases` - ファイル一覧の取得
|
||||
- `DELETE /knowledge-bases/:id` - ファイルの削除
|
||||
- `DELETE /knowledge-bases/clear` - ナレッジベースの全消去
|
||||
|
||||
### ユーザー設定
|
||||
|
||||
- `GET /user-settings` - 設定の取得
|
||||
- `PUT /user-settings` - 設定の更新
|
||||
- **注**: フロントエンドは統一された `SettingsModal` コンポーネントを介してこれらのエンドポイントと通信します。
|
||||
|
||||
## 利用方法
|
||||
|
||||
1. **ユーザー登録/ログイン**
|
||||
2. **基本構成の設定**
|
||||
- サイドバー下部の「設定」ボタンをクリックします。
|
||||
- 「モデル管理」タブで OpenAI 互換の LLM と Embedding モデルを追加します。
|
||||
- API キーとモデルパラメータを設定します。
|
||||
- 「一般設定」タブで言語を切り替えたり、パスワードを変更したりできます。
|
||||
3. **ドキュメントのアップロード**
|
||||
- PDF, テキスト, 画像などの形式をサポートします。
|
||||
- チャンクサイズと Embedding モデルを設定します。
|
||||
- 自動的にベクトル化とインデックス化が行われます。
|
||||
4. **詳細設定の調整**
|
||||
- 使用するモデルを選択します。
|
||||
- 推論パラメータと検索パラメータを構成します。
|
||||
- UI 言語を設定します。
|
||||
5. **対話を開始**
|
||||
- 質問を送信します。
|
||||
- ストリーミング処理の経過を観察します。
|
||||
- 引用付きのインテリジェントな回答を確認します。
|
||||
|
||||
## 特筆すべき機能
|
||||
|
||||
- ✅ **ユーザー分離**: 各ユーザーに独立したモデル設定とナレッジベースを提供
|
||||
- ✅ **ストリーミング体験**: 処理の進捗と生成内容をリアルタイム表示
|
||||
- ✅ **インテリジェント検索**: キーワード抽出 + ハイブリッド検索 + 類似度フィルタリング
|
||||
- ✅ **引用追跡**: 回答の根拠となるソースと関連セグメントを明確に表示
|
||||
- ✅ **マルチモデル対応**: OpenAI 互換インターフェース + Gemini ネイティブサポート
|
||||
- ✅ **柔軟な設定**: ユーザー独自の API キーと推論パラメータのカスタマイズ
|
||||
- ✅ **多言語サポート**: UI と AI 回答の両方を完全国際化
|
||||
- ✅ **ビジョン機能**: 画像処理をサポートするマルチモーダルモデルに対応
|
||||
- ✅ **ユーザー管理**: 登録、ログイン、パスワード管理の完備
|
||||
- ✅ **デュアルモード処理**: 高速モード (テキストのみ) + 高精度モード (画像・テキスト混合)
|
||||
- ✅ **メモリの最適化**: 大容量ファイルを分割処理し、メモリオーバーフローを防止
|
||||
- ✅ **統合管理**: モデル、ユーザー、一般設定を一つにまとめたモダンな UI
|
||||
- ✅ **ミニマルなデザイン**: サイドバーとヘッダーの冗余を排除し、対話体験に集中
|
||||
@@ -1,444 +0,0 @@
|
||||
# デプロイガイド
|
||||
|
||||
## 開発環境のデプロイ
|
||||
|
||||
### 前提条件
|
||||
|
||||
- Node.js 18+
|
||||
- Yarn
|
||||
- Docker & Docker Compose
|
||||
|
||||
### 1. プロジェクトのクローン
|
||||
|
||||
```bash
|
||||
git clone <repository-url>
|
||||
cd simple-kb
|
||||
```
|
||||
|
||||
### 2. 依存関係のインストール
|
||||
|
||||
```bash
|
||||
yarn install
|
||||
```
|
||||
|
||||
### 3. 基本サービスの起動
|
||||
|
||||
```bash
|
||||
# Elasticsearch と Tika を起動
|
||||
docker-compose up -d elasticsearch tika
|
||||
```
|
||||
|
||||
### 4. 環境変数の設定
|
||||
|
||||
```bash
|
||||
# 環境変数のテンプレートをコピー
|
||||
cp server/.env.sample server/.env
|
||||
|
||||
# 設定ファイルを編集
|
||||
vim server/.env
|
||||
```
|
||||
|
||||
### 5. 開発サービスの起動
|
||||
|
||||
```bash
|
||||
# フロントエンドとバックエンドを同時に起動
|
||||
yarn dev
|
||||
|
||||
# または個別に起動
|
||||
yarn dev:web # フロントエンド (ポート 5173)
|
||||
yarn dev:server # バックエンド (ポート 3000)
|
||||
```
|
||||
|
||||
## 本番環境のデプロイ
|
||||
|
||||
### Docker Compose を使用する場合
|
||||
|
||||
1. **環境変数の設定**
|
||||
|
||||
```bash
|
||||
cp .env.sample .env
|
||||
# 本番環境の設定を編集
|
||||
```
|
||||
|
||||
1. **ビルドと起動**
|
||||
|
||||
```bash
|
||||
# すべてのサービスを一括起動
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
1. **サービスへのアクセス**
|
||||
|
||||
- HTTPS: <https://localhost> (推奨)
|
||||
- HTTP: <http://localhost> (HTTPS へ自動リダイレクト)
|
||||
- バックエンド API: Nginx プロキシ経由でアクセス
|
||||
- Elasticsearch: <http://localhost:9200>
|
||||
- Tika: <http://localhost:9998>
|
||||
|
||||
### サービス構成
|
||||
|
||||
- **nginx**: リバースプロキシ。HTTPS と CORS 処理を担当
|
||||
- **web**: フロントエンド静的ファイルサービス (React)
|
||||
- **server**: バックエンド API サービス (NestJS)
|
||||
- **libreoffice**: LibreOffice ドキュメント変換サービス (FastAPI, ポート 8100)
|
||||
- **elasticsearch**: ベクトル検索エンジン (ポート 9200)
|
||||
- **tika**: ドキュメント内容抽出サービス (ポート 9998)
|
||||
|
||||
### アーキテクチャ図
|
||||
|
||||
```
|
||||
ユーザー → nginx → web (React)
|
||||
↓
|
||||
server (NestJS) → elasticsearch
|
||||
↓ ↓
|
||||
libreoffice tika
|
||||
(FastAPI) (Java)
|
||||
```
|
||||
|
||||
### SSL 証明書の設定
|
||||
|
||||
#### 自己署名証明書(開発環境用)
|
||||
|
||||
```bash
|
||||
# 自己署名証明書を生成
|
||||
./nginx/generate-ssl.sh
|
||||
```
|
||||
|
||||
#### 本番環境用証明書
|
||||
|
||||
正式な SSL 証明書を以下の場所に配置してください:
|
||||
|
||||
- 証明書ファイル: `nginx/ssl/cert.pem`
|
||||
- 秘密鍵ファイル: `nginx/ssl/key.pem`
|
||||
|
||||
### Docker 常用コマンド
|
||||
|
||||
```bash
|
||||
# すべてのサービスのログを表示
|
||||
docker-compose logs -f
|
||||
|
||||
# 特定のサービスのログを表示
|
||||
docker-compose logs -f nginx
|
||||
docker-compose logs -f server
|
||||
|
||||
# サービスの再起動
|
||||
docker-compose restart
|
||||
|
||||
# 特定のサービスの再起動
|
||||
docker-compose restart nginx
|
||||
|
||||
# サービスの停止
|
||||
docker-compose down
|
||||
|
||||
# 再ビルドして起動
|
||||
docker-compose up -d --build
|
||||
```
|
||||
|
||||
### データの永続化
|
||||
|
||||
#### SQLite データベース
|
||||
|
||||
- データベースファイルの場所: `./data/database.sqlite`
|
||||
- コンテナ外部に自動マウントされるため、コンテナ再起動時もデータは失われません。
|
||||
|
||||
#### アップロードファイル
|
||||
|
||||
- ファイルの保存場所: `./uploads/`
|
||||
- アップロードされたすべてのドキュメントと画像はホストマシンに保存されます。
|
||||
|
||||
#### Elasticsearch データ
|
||||
|
||||
- データボリューム: `elasticsearch-data`
|
||||
- ベクトルインデックスデータが永続的に保存されます。
|
||||
|
||||
### 手動デプロイ
|
||||
|
||||
1. **フロントエンドのビルド**
|
||||
|
||||
```bash
|
||||
cd web
|
||||
yarn build
|
||||
```
|
||||
|
||||
1. **バックエンドのビルド**
|
||||
|
||||
```bash
|
||||
cd server
|
||||
yarn build
|
||||
```
|
||||
|
||||
1. **Nginx の設定**
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
# フロントエンド静的ファイル
|
||||
location / {
|
||||
root /path/to/web/dist;
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
# バックエンド API プロキシ
|
||||
location /api {
|
||||
proxy_pass http://localhost:3000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 環境変数の設定
|
||||
|
||||
### バックエンド環境変数 (server/.env)
|
||||
|
||||
```bash
|
||||
# データベース
|
||||
DATABASE_PATH=./data/metadata.db
|
||||
|
||||
# Elasticsearch
|
||||
ELASTICSEARCH_HOST=http://localhost:9200
|
||||
ELASTICSEARCH_INDEX=knowledge_base_chunks
|
||||
|
||||
# Tika サービス
|
||||
TIKA_HOST=http://localhost:9998
|
||||
|
||||
# LibreOffice サービス
|
||||
LIBREOFFICE_URL=http://localhost:8100
|
||||
|
||||
# Vision API
|
||||
VISION_API_KEY=sk-xxx-your-key
|
||||
VISION_API_BASE=https://api.openai.com/v1
|
||||
VISION_MODEL=gpt-4-vision-preview
|
||||
|
||||
# ファイルアップロード
|
||||
UPLOAD_FILE_PATH=./uploads
|
||||
MAX_FILE_SIZE=50MB
|
||||
|
||||
# 一時ディレクトリ
|
||||
TEMP_DIR=./temp
|
||||
|
||||
# JWT
|
||||
JWT_SECRET=your-jwt-secret-key
|
||||
JWT_EXPIRES_IN=7d
|
||||
|
||||
# サービスポート
|
||||
PORT=3000
|
||||
```
|
||||
|
||||
### フロントエンド環境変数 (web/.env)
|
||||
|
||||
```bash
|
||||
# API アドレス
|
||||
VITE_API_BASE_URL=http://localhost:3000
|
||||
|
||||
# アプリケーション設定
|
||||
VITE_APP_TITLE=簡易ナレッジベース
|
||||
VITE_MAX_FILE_SIZE=50
|
||||
```
|
||||
|
||||
## バックアップと復元
|
||||
|
||||
### SQLite データベースのバックアップ
|
||||
|
||||
```bash
|
||||
# バックアップ
|
||||
cp server/data/metadata.db backup/metadata_$(date +%Y%m%d).db
|
||||
|
||||
# 復元
|
||||
cp backup/metadata_20240101.db server/data/metadata.db
|
||||
```
|
||||
|
||||
### Elasticsearch データのバックアップ
|
||||
|
||||
```bash
|
||||
# スナップショット用リポジトリの作成
|
||||
curl -X PUT "localhost:9200/_snapshot/backup_repo" -H 'Content-Type: application/json' -d'
|
||||
{
|
||||
"type": "fs",
|
||||
"settings": {
|
||||
"location": "/backup/elasticsearch"
|
||||
}
|
||||
}'
|
||||
|
||||
# スナップショットの作成
|
||||
curl -X PUT "localhost:9200/_snapshot/backup_repo/snapshot_1"
|
||||
```
|
||||
|
||||
## 監視とログ
|
||||
|
||||
### アプリケーションログ
|
||||
|
||||
- フロントエンドログ: ブラウザのコンソール
|
||||
- バックエンドログ: `server/logs/`
|
||||
- Elasticsearch ログ: Docker コンテナログ
|
||||
|
||||
### ヘルスチェック
|
||||
|
||||
```bash
|
||||
# バックエンドのヘルスチェック
|
||||
curl http://localhost:3000/health
|
||||
|
||||
# Elasticsearch のヘルスチェック
|
||||
curl http://localhost:9200/_cluster/health
|
||||
```
|
||||
|
||||
## トラブルシューティング
|
||||
|
||||
### よくある質問
|
||||
|
||||
1. **Elasticsearch への接続に失敗する**
|
||||
- Docker コンテナの状態を確認してください。
|
||||
- ポート 9200 がアクセス可能か確認してください。
|
||||
- ファイアウォールの設定を確認してください。
|
||||
|
||||
2. **ファイルのアップロードに失敗する**
|
||||
- uploads ディレクトリの権限を確認してください。
|
||||
- Tika サービスが正常に動作しているか確認してください。
|
||||
- ファイルサイズの制限を確認してください。
|
||||
|
||||
3. **モデル API の呼び出しに失敗する**
|
||||
- API キーの設定を確認してください。
|
||||
- ネットワーク接続を確認してください。
|
||||
- モデル ID が正しいか確認してください。
|
||||
|
||||
#### 1. Elasticsearch への接続失敗
|
||||
|
||||
**症状**: バックエンドログに "Connection refused" または "ECONNREFUSED" と表示される
|
||||
**解決策**:
|
||||
|
||||
- Docker コンテナの状態確認: `docker-compose ps`
|
||||
- ポート 9200 のアクセス確認: `curl http://localhost:9200`
|
||||
- ファイアウォール設定の確認
|
||||
- Elasticsearch の再起動: `docker-compose restart elasticsearch`
|
||||
|
||||
#### 2. ファイルのアップロード失敗
|
||||
|
||||
**症状**: アップロード時に「アップロード失敗」または「処理失敗」と表示される
|
||||
**解決策**:
|
||||
|
||||
- uploads ディレクトリの権限確認: `ls -la server/uploads/`
|
||||
- Tika サービスの状態確認: `curl http://localhost:9998/version`
|
||||
- ファイルサイズ制限の確認(デフォルト 50MB)
|
||||
- バックエンドログの詳細エラーを確認
|
||||
|
||||
#### 3. モデル API の呼び出し失敗
|
||||
|
||||
**症状**: チャット時に「API キーが無効」または「モデルの呼び出しに失敗」と表示される
|
||||
**解決策**:
|
||||
|
||||
- API キーの設定が正しいか確認
|
||||
- ネットワーク接続とファイアウォールの確認
|
||||
- モデル ID の確認(例: gpt-4, gpt-3.5-turbo)
|
||||
- API の利用残高と権限の確認
|
||||
- Base URL の設定確認(OpenAI 互換インターフェースの場合)
|
||||
|
||||
#### 4. ユーザー認証の問題
|
||||
|
||||
**症状**: ログイン失敗またはトークンの期限切れ
|
||||
**解決策**:
|
||||
|
||||
- ユーザー名とパスワードの確認
|
||||
- ブラウザのキャッシュと localStorage のクリア
|
||||
- JWT_SECRET の設定確認
|
||||
- ユーザーアカウントの再登録
|
||||
|
||||
#### 5. ナレッジベースの検索結果が出ない
|
||||
|
||||
**症状**: 質問後に「関連する知識が見つかりません」と表示される
|
||||
**解決策**:
|
||||
|
||||
- ファイルのインデックスが完了しているか確認(ステータスが「完了」)
|
||||
- 類似度しきい値の設定を調整
|
||||
- Embedding モデルの設定確認
|
||||
- 別のキーワードで質問してみる
|
||||
|
||||
#### 6. フロントエンドページにアクセスできない
|
||||
|
||||
**症状**: ブラウザに「このサイトにアクセスできません」と表示される
|
||||
**解決策**:
|
||||
|
||||
- フロントエンドサービスがポート 5173 で動作しているか確認
|
||||
- ファイアウォールとプロキシの設定確認
|
||||
- ブラウザのキャッシュのクリア
|
||||
- シークレットモードでのアクセスを試す
|
||||
|
||||
### デバッグツール
|
||||
|
||||
#### サービス状態の確認
|
||||
|
||||
```bash
|
||||
# すべての Docker コンテナを確認
|
||||
docker-compose ps
|
||||
|
||||
# ポートの使用状況を確認
|
||||
netstat -tulpn | grep :5173
|
||||
netstat -tulpn | grep :3000
|
||||
```
|
||||
|
||||
#### 詳細ログの表示
|
||||
|
||||
```bash
|
||||
# バックエンドログ
|
||||
docker-compose logs -f server
|
||||
|
||||
# Elasticsearch ログ
|
||||
docker-compose logs -f elasticsearch
|
||||
|
||||
# フロントエンド開発ログ
|
||||
cd web && yarn dev
|
||||
```
|
||||
|
||||
#### ヘルスチェック
|
||||
|
||||
```bash
|
||||
# バックエンド API のヘルスチェック
|
||||
curl http://localhost:3000/health
|
||||
|
||||
# Elasticsearch のヘルスチェック
|
||||
curl http://localhost:9200/_cluster/health
|
||||
|
||||
# LibreOffice サービスのチェック
|
||||
curl http://localhost:8100/health
|
||||
|
||||
# LibreOffice API ドキュメントの表示
|
||||
open http://localhost:8100/docs
|
||||
```
|
||||
|
||||
### パフォーマンスの最適化
|
||||
|
||||
#### 1. Elasticsearch のチューニング
|
||||
|
||||
```bash
|
||||
# JVM ヒープメモリの増量
|
||||
export ES_JAVA_OPTS="-Xms2g -Xmx2g"
|
||||
|
||||
# インデックス状態の確認
|
||||
curl http://localhost:9200/_cat/indices?v
|
||||
```
|
||||
|
||||
#### 2. ファイル処理の最適化
|
||||
|
||||
- 同時にアップロードするファイル数を制限する
|
||||
- チャンクサイズを適切に調整する(推奨 512-1024 トークン)
|
||||
- 不要なインデックスデータを定期的に整理する
|
||||
|
||||
### データの復元
|
||||
|
||||
#### SQLite データベースの復元
|
||||
|
||||
```bash
|
||||
# バックアップから復元
|
||||
cp backup/metadata_20240101.db server/data/metadata.db
|
||||
|
||||
# データベースの整合性チェック
|
||||
sqlite3 server/data/metadata.db "PRAGMA integrity_check;"
|
||||
```
|
||||
|
||||
#### Elasticsearch データの復元
|
||||
|
||||
```bash
|
||||
# スナップショットの復元
|
||||
curl -X POST "localhost:9200/_snapshot/backup_repo/snapshot_1/_restore"
|
||||
```
|
||||
@@ -1,217 +0,0 @@
|
||||
# 簡易ナレッジベース (Simple Knowledge Base) - システム設計ドキュメント
|
||||
|
||||
## 1. プロジェクト概要
|
||||
|
||||
本プロジェクトは、React + NestJS をベースに開発されたフルスタックのナレッジベースQ&Aシステム(RAG - Retrieval-Augmented Generation)です。
|
||||
ユーザーは多様な形式のドキュメントをアップロードし、チャンク分割やインデックス設定をカスタマイズした上で、大規模言語モデル(LLM)を利用してナレッジベースに基づいた質問応答を行うことができます。
|
||||
|
||||
---
|
||||
|
||||
## 2. コアアーキテクチャ設計
|
||||
|
||||
### 2.1 技術スタック
|
||||
|
||||
- **フロントエンド**: React 19 + TypeScript + Vite + Tailwind CSS
|
||||
- **バックエンド**: NestJS + LangChain + TypeORM
|
||||
- **データベース**: SQLite (ユーザー、モデル設定、ナレッジベースのメタデータ)
|
||||
- **ベクトルストレージ**: Elasticsearch
|
||||
- **ファイル処理**: Apache Tika (高速モード) + Vision Pipeline (高精度モード)
|
||||
- **認証**: JWT
|
||||
- **ドキュメント変換**: LibreOffice + ImageMagick
|
||||
|
||||
### 2.2 システムアーキテクチャ
|
||||
|
||||
```
|
||||
ユーザー -> Reactフロントエンド -> NestJSバックエンド -> SQLite/Elasticsearch
|
||||
|
|
||||
v
|
||||
LangChain Agent
|
||||
|
|
||||
v
|
||||
大言語モデル (LLM)
|
||||
|
||||
高精度モードのプロセス:
|
||||
PDF/Word/PPT -> LibreOffice -> PDF -> ImageMagick -> Vision Model -> 構造化コンテンツ
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. コア機能モジュール
|
||||
|
||||
### 3.1 ユーザー認証システム
|
||||
|
||||
- **JWT 認証**: ユーザー名/パスワードに基づくログインシステム
|
||||
- **ユーザー管理**: ユーザー登録、パスワード変更をサポート
|
||||
- **権限制御**: ユーザーデータの隔離。各ユーザーに独立したナレッジベースを提供
|
||||
- **管理画面**: 統合された設定モーダルによるユーザー管理(作成/リスト表示)
|
||||
|
||||
### 3.2 モデル設定管理
|
||||
|
||||
- **マルチプロバイダー対応**:
|
||||
- **OpenAI 互換**: OpenAI API および互換インターフェース(DeepSeek, Claude など)に対応
|
||||
- **Google Gemini**: ネイティブ SDK によるサポート
|
||||
- **モデルタイプ**:
|
||||
- **LLM**: 対話生成モデル
|
||||
- **Embedding**: ベクトル化モデル
|
||||
- **Rerank**: 再ランキングモデル
|
||||
- **ユーザーカスタマイズ**: ユーザーが独自の API キーとモデルパラメータを設定可能
|
||||
- **ビジョンサポート**: モデルが画像処理に対応しているかどうかのフラグ管理
|
||||
- **統合管理**: 設定モーダルの「モデル管理」タブで一括管理
|
||||
|
||||
### 3.3 ナレッジベース管理
|
||||
|
||||
- **ファイルアップロード**: PDF、ドキュメント、画像など多様な形式に対応
|
||||
- **デュアルモード処理**:
|
||||
- **高速モード**: Apache Tika を使用したテキスト抽出
|
||||
- **高精度モード**: Vision Pipeline を使用した画像・テキスト混合処理
|
||||
- **インテリジェント・チャンキング**: チャンクサイズとオーバーラップを調整可能
|
||||
- **ベクトルインデックス**: ユーザーが選択した Embedding モデルによるベクトル化
|
||||
- **ステータス管理**: ファイル処理状況の追跡(待機中、インデックス中、完了、失敗)
|
||||
|
||||
### 3.4 RAG 質問応答システム
|
||||
|
||||
- **インテリジェント検索**:
|
||||
- キーワード抽出とクエリの最適化
|
||||
- ハイブリッド検索(ベクトル検索 + 全文検索)
|
||||
- 類似度しきい値によるフィルタリング
|
||||
- **ストリーミング生成**:
|
||||
- Server-Sent Events (SSE) によるストリーミング出力
|
||||
- 処理の進捗をリアルタイムに表示
|
||||
- 生成コンテンツを1文字ずつ表示
|
||||
- **引用追跡**:
|
||||
- 回答の出典ファイルを表示
|
||||
- 関連するドキュメントセグメントを表示
|
||||
- 類似度スコアの表示
|
||||
|
||||
### 3.5 多言語サポート
|
||||
|
||||
- **インターフェースの国際化**: 日本語、中国語、英語に対応
|
||||
- **インテリジェント回答**: ユーザーの言語設定に基づいた AI による回答
|
||||
- **言語設定の永続化**: ユーザーの選択した言語を自動保存
|
||||
|
||||
---
|
||||
|
||||
## 4. データモデル設計
|
||||
|
||||
### 4.1 SQLite テーブル
|
||||
|
||||
**ユーザー表 (users)**
|
||||
|
||||
- id, username, password_hash, is_admin, created_at
|
||||
|
||||
**モデル設定表 (model_configs)**
|
||||
|
||||
- id, user_id, name, provider, model_id, base_url, api_key, type, supports_vision
|
||||
|
||||
**ナレッジベースファイル表 (knowledge_bases)**
|
||||
|
||||
- id, user_id, name, original_name, storage_path, size, mimetype, status, created_at
|
||||
|
||||
**ユーザー設定表 (user_settings)**
|
||||
|
||||
- user_id, selected_llm_id, selected_embedding_id, temperature, max_tokens, top_k, similarity_threshold, language
|
||||
|
||||
### 4.2 Elasticsearch インデックス
|
||||
|
||||
**ナレッジベース・チャンクインデックス (knowledge_base_chunks)**
|
||||
|
||||
```json
|
||||
{
|
||||
"chunk_id": "string",
|
||||
"knowledge_base_id": "string",
|
||||
"user_id": "string",
|
||||
"file_name": "string",
|
||||
"content": "text",
|
||||
"embedding": "dense_vector[1536]",
|
||||
"chunk_index": "integer",
|
||||
"metadata": "object"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API エンドポイント設計
|
||||
|
||||
### 5.1 認証
|
||||
|
||||
- `POST /auth/login` - ログイン
|
||||
- `POST /auth/register` - ユーザー登録
|
||||
- `POST /auth/change-password` - パスワード変更
|
||||
|
||||
### 5.2 モデル管理
|
||||
|
||||
- `GET /model-configs` - モデル設定の取得
|
||||
- `POST /model-configs` - モデル設定の作成
|
||||
- `PUT /model-configs/:id` - モデル設定の更新
|
||||
- `DELETE /model-configs/:id` - モデル設定の削除
|
||||
|
||||
### 5.3 ナレッジベース
|
||||
|
||||
- `POST /upload` - ファイルアップロード
|
||||
- `GET /knowledge-bases` - ファイル一覧の取得
|
||||
- `DELETE /knowledge-bases/:id` - ファイルの削除
|
||||
- `DELETE /knowledge-bases/clear` - ナレッジベースの全削除
|
||||
|
||||
### 5.4 チャット
|
||||
|
||||
- `POST /chat/stream` - ストリーミングチャット(SSE)
|
||||
|
||||
### 5.5 ユーザー設定
|
||||
|
||||
- `GET /user-settings` - 設定の取得
|
||||
- `PUT /user-settings` - 設定の更新
|
||||
|
||||
---
|
||||
|
||||
## 6. コアフロー
|
||||
|
||||
### 6.1 ファイル処理フロー
|
||||
|
||||
**高速モード**:
|
||||
|
||||
```
|
||||
アップロード → メタデータ保存 → Tikaテキスト抽出 → チャンク分割 → ベクトル化 → ESインデックス → ステータス更新
|
||||
```
|
||||
|
||||
**高精度モード**:
|
||||
|
||||
```
|
||||
アップロード → LibreOffice変換 → PDF画像化 → Vision分析 → 構造化コンテンツ → ベクトル化 → ESインデックス
|
||||
```
|
||||
|
||||
### 6.2 RAG 質問応答フロー
|
||||
|
||||
```
|
||||
ユーザーの質問 → キーワード抽出 → ハイブリッド検索 → 類似度フィルタリング → プロンプト構築 → LLM生成 → ストリーミング出力 → 引用表示
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. デプロイ構成
|
||||
|
||||
### 7.1 開発環境
|
||||
|
||||
- フロントエンド: Vite 開発サーバー (ポート 5173)
|
||||
- バックエンド: NestJS 開発サーバー (ポート 3000)
|
||||
- Elasticsearch: Docker コンテナ (ポート 9200)
|
||||
- Apache Tika: Docker コンテナ (ポート 9998)
|
||||
|
||||
### 7.2 本番環境
|
||||
|
||||
- Docker Compose を使用したコンテナ化デプロイ
|
||||
- Nginx によるリバースプロキシと負荷分散
|
||||
- SSL 証明書の設定
|
||||
|
||||
---
|
||||
|
||||
## 8. 特徴的な機能
|
||||
|
||||
- ✅ **ユーザー分離**: ユーザーごとに独立したモデル設定とナレッジベースを保持
|
||||
- ✅ **ストリーミング体験**: 処理状況と生成内容をリアルタイム表示
|
||||
- ✅ **インテリジェント検索**: キーワード抽出 + ハイブリッド検索 + 類似度フィルタリング
|
||||
- ✅ **引用追跡**: 回答の根拠となるソースと関連セグメントを明確に表示
|
||||
- ✅ **マルチモデル対応**: OpenAI 互換インターフェース + Gemini ネイティブサポート
|
||||
- ✅ **柔軟な設定**: ユーザー独自の API キーと推論パラメータのカスタマイズ
|
||||
- ✅ **多言語対応**: UI と AI 回答の完全な国際化サポート
|
||||
- ✅ **ビジョン機能**: 画像処理に対応したマルチモーダルモデルのサポート
|
||||
- ✅ **デュアルモード処理**: 高速モード (テキストのみ) + 高精度モード (画像・テキスト混合)
|
||||
@@ -1,71 +0,0 @@
|
||||
# 開発基準
|
||||
|
||||
## コードコメントの基準
|
||||
|
||||
### 1. コメントの言語
|
||||
|
||||
- **すべてのコードコメントは中国語を使用する必要があります**
|
||||
- 以下を含みますが、これらに限定されません:
|
||||
- 関数/メソッドのコメント
|
||||
- 行内コメント
|
||||
- コードブロックの説明
|
||||
- TODO/FIXME コメント
|
||||
|
||||
### 2. ログ出力の基準
|
||||
|
||||
- **すべてのログ出力は中国語を使用する必要があります**
|
||||
- 以下を含みますが、これらに限定されません:
|
||||
- `logger.log()` 情報ログ
|
||||
- `logger.warn()` 警告ログ
|
||||
- `logger.error()` ラーログ
|
||||
- `console.log()` デバッグ出力
|
||||
|
||||
### 3. エラーメッセージの基準
|
||||
|
||||
- **ユーザーに表示されるエラーメッセージは中国語を使用します**
|
||||
- **開発デバッグ用のエラーメッセージは中国語を使用します**
|
||||
- 例外スロー時のエラーメッセージには中国語を使用します
|
||||
|
||||
## 例
|
||||
|
||||
### 正しいコメントとログ
|
||||
|
||||
```typescript
|
||||
// 正解:中国語のコメント
|
||||
async getEmbeddings(texts: string[]): Promise<number[][]> {
|
||||
this.logger.log(`正在为 ${texts.length} 个文本生成嵌入向量`); // 正解:中国語のログ
|
||||
|
||||
try {
|
||||
// APIを呼び出して埋め込みベクトルを取得
|
||||
const response = await this.callEmbeddingAPI(texts);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error('获取嵌入向量失败', error); // 正解:中国語のログ
|
||||
throw new Error('嵌入向量生成失败'); // 正解:中国語のエラーメッセージ
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 誤ったコメントとログ
|
||||
|
||||
```typescript
|
||||
// 誤り:英語のコメントとログ
|
||||
async getEmbeddings(texts: string[]): Promise<number[][]> {
|
||||
this.logger.log(`Getting embeddings for ${texts.length} texts`);
|
||||
|
||||
try {
|
||||
// Call API to get embeddings
|
||||
const response = await this.callEmbeddingAPI(texts);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get embeddings', error);
|
||||
throw new Error('Embedding generation failed');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 履行基準
|
||||
|
||||
1. **コードレビュー時には、必ずコメントとログの言語をチェックしてください**
|
||||
2. **新規コードは、中国語のコメントとログの基準に従う必要があります**
|
||||
3. **既存のコードをリファクタリングする際は、同時にコメントとログも中国語に更新してください**
|
||||
@@ -1,219 +0,0 @@
|
||||
# Embedding モデル ID 連携の修正
|
||||
|
||||
## 🐛 問題の記述
|
||||
|
||||
```
|
||||
混合検索失敗: NotFoundException: ModelConfig with ID "embedding-3" not found or not owned by user.
|
||||
```
|
||||
|
||||
## 🔍 問題の分析
|
||||
|
||||
### 混同されやすい概念
|
||||
|
||||
システム内には2種類の異なる「ID」が存在します:
|
||||
|
||||
1. **モデル設定テーブルの ID** (`ModelConfig.id`)
|
||||
- データベースの主キー
|
||||
- 例:`"embedding-3"`, `"default-embedding"`
|
||||
- 用途:`ModelConfigService.findOne(id, userId)`
|
||||
|
||||
2. **モデル識別子** (`ModelConfig.modelId`)
|
||||
- AI ベンダー側でのモデル名
|
||||
- 例:`"text-embedding-3-large"`, `"text-embedding-ada-002"`
|
||||
- 用途:AI API 呼び出し時のパラメータ
|
||||
|
||||
### データフロー
|
||||
|
||||
```
|
||||
ユーザー設定: user_setting.selectedEmbeddingId = "embedding-3" ✅ テーブルID
|
||||
|
||||
フロントエンド: settings.selectedEmbeddingId
|
||||
↓ 転送
|
||||
バックエンド Controller: selectedEmbeddingId = "embedding-3"
|
||||
↓ 転送
|
||||
ChatService: embeddingModel.id = "embedding-3" ✅ 正常
|
||||
↓ 転送
|
||||
hybridSearch: embeddingModelId = "embedding-3"
|
||||
↓ 転送
|
||||
EmbeddingService.getEmbeddings(embeddingModelId)
|
||||
↓ 呼び出し
|
||||
ModelConfigService.findOne("embedding-3", userId) ✅ 正常
|
||||
```
|
||||
|
||||
### 以前の誤り
|
||||
|
||||
**ChatService.ts (誤り):**
|
||||
|
||||
```typescript
|
||||
// 182行目付近
|
||||
searchResults = await this.hybridSearch(
|
||||
[message],
|
||||
userId,
|
||||
embeddingModel.modelId, // ❌ 誤り! "text-embedding-3-large" を渡してしまっていた
|
||||
);
|
||||
```
|
||||
|
||||
**hybridSearch (受信側):**
|
||||
|
||||
```typescript
|
||||
private async hybridSearch(
|
||||
keywords: string[],
|
||||
userId: string,
|
||||
embeddingModelId?: string, // "text-embedding-3-large" を受け取ってしまう
|
||||
)
|
||||
```
|
||||
|
||||
**EmbeddingService (期待値):**
|
||||
|
||||
```typescript
|
||||
async getEmbeddings(
|
||||
texts: string[],
|
||||
userId: string,
|
||||
embeddingModelConfigId: string, // 本来は "embedding-3" を期待
|
||||
) {
|
||||
const modelConfig = await this.modelConfigService.findOne(
|
||||
embeddingModelConfigId, // ❌ "text-embedding-3-large" で検索しても見つからない!
|
||||
userId,
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
## ✅ 修正内容
|
||||
|
||||
### 修正箇所
|
||||
|
||||
**server/src/chat/chat.service.ts:**
|
||||
|
||||
```typescript
|
||||
// 182行目付近
|
||||
searchResults = await this.hybridSearch(
|
||||
[message],
|
||||
userId,
|
||||
embeddingModel.id, // ✅ テーブルID "embedding-3" を使用するように変更
|
||||
);
|
||||
```
|
||||
|
||||
### 修正後のフロー
|
||||
|
||||
```
|
||||
1. ユーザーが埋め込みモデルを選択: text-embedding-3-large
|
||||
↓
|
||||
2. システムが user_setting テーブルに保存:
|
||||
selectedEmbeddingId = "embedding-3" (ModelConfig テーブルの主キー)
|
||||
↓
|
||||
3. フロントエンドがチャットリクエストを送信:
|
||||
{ selectedEmbeddingId: "embedding-3" }
|
||||
↓
|
||||
4. バックエンド Controller が受信:
|
||||
selectedEmbeddingId = "embedding-3"
|
||||
↓
|
||||
5. ChatService がモデルを検索:
|
||||
embeddingModel = models.find(m => m.id === "embedding-3")
|
||||
// 結果: { id: "embedding-3", modelId: "text-embedding-3-large", ... }
|
||||
↓
|
||||
6. ChatService が hybridSearch を呼び出し:
|
||||
hybridSearch(..., embeddingModel.id) // "embedding-3" を渡す
|
||||
↓
|
||||
7. hybridSearch が EmbeddingService を呼び出し:
|
||||
getEmbeddings(..., "embedding-3")
|
||||
↓
|
||||
8. EmbeddingService が設定を検索:
|
||||
findOne("embedding-3", userId) // ✅ 設定が見つかる
|
||||
↓
|
||||
9. AI API を呼び出し:
|
||||
model: "text-embedding-3-large" // modelId を用いて API を実行
|
||||
```
|
||||
|
||||
## 📊 ID の対応関係
|
||||
|
||||
| シーン | 使用するフィールド | 例 | 用途 |
|
||||
|------|-----------|--------|------|
|
||||
| ユーザー設定 | `user_setting.selectedEmbeddingId` | `"embedding-3"` | ユーザーの選択を保存 |
|
||||
| 設定の検索 | `ModelConfig.id` | `"embedding-3"` | データベースクエリ |
|
||||
| API 呼び出し | `ModelConfig.modelId` | `"text-embedding-3-large"` | AI ベンダーのインターフェース |
|
||||
|
||||
## 🔑 重要な原則
|
||||
|
||||
### 1. データベース操作にはテーブル ID を使用する
|
||||
|
||||
```typescript
|
||||
// ✅ 正解
|
||||
const model = await modelConfigService.findOne(modelId, userId); // modelId = "embedding-3"
|
||||
|
||||
// ❌ 誤り
|
||||
const model = await modelConfigService.findOne(modelId, userId); // modelId = "text-embedding-3-large"
|
||||
```
|
||||
|
||||
### 2. API 呼び出しにはモデル識別子を使用する
|
||||
|
||||
```typescript
|
||||
// ✅ 正解
|
||||
fetch(apiUrl, {
|
||||
body: JSON.stringify({
|
||||
model: modelConfig.modelId, // "text-embedding-3-large"
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
### 3. 内部的な受け渡しにはテーブル ID を使用する
|
||||
|
||||
```typescript
|
||||
// ✅ 正解
|
||||
embeddingService.getEmbeddings(texts, userId, modelConfig.id); // "embedding-3"
|
||||
|
||||
// ❌ 誤り
|
||||
embeddingService.getEmbeddings(texts, userId, modelConfig.modelId); // "text-embedding-3-large"
|
||||
```
|
||||
|
||||
## 🧪 検証
|
||||
|
||||
### テスト手順
|
||||
|
||||
1. **ユーザー設定の確認**
|
||||
|
||||
```sql
|
||||
SELECT selectedEmbeddingId FROM user_setting WHERE userId = 'xxx';
|
||||
-- 期待値: "embedding-3" (テーブルID)
|
||||
```
|
||||
|
||||
2. **モデル設定の確認**
|
||||
|
||||
```sql
|
||||
SELECT id, modelId, name FROM model_config WHERE userId = 'xxx';
|
||||
-- 期待値: embedding-3 | text-embedding-3-large | Text Embedding 3 Large
|
||||
```
|
||||
|
||||
3. **チャットメッセージの送信**
|
||||
- バックエンドログを確認
|
||||
- 期待される出力: "使用嵌入模型: Text Embedding 3 Large text-embedding-3-large ID: embedding-3"
|
||||
|
||||
4. **埋め込みベクトルの生成確認**
|
||||
- ログに "从 Text Embedding 3 Large 获取到 X 个嵌入向量" と表示されること
|
||||
|
||||
### 期待されるログ出力
|
||||
|
||||
```
|
||||
=== ChatService.streamChat ===
|
||||
User ID: user-123
|
||||
Selected Embedding ID: embedding-3
|
||||
ID に基づいてモデルを検索: embedding-3
|
||||
使用するモデル: Text Embedding 3 Large text-embedding-3-large ID: embedding-3
|
||||
埋め込みベクトルを生成中...
|
||||
Text Embedding 3 Large から 1 個の埋め込みベクトルを取得しました。次元数: 2560
|
||||
```
|
||||
|
||||
## 📁 修正されたファイル
|
||||
|
||||
- `server/src/chat/chat.service.ts` - 182行目。 `embeddingModel.modelId` ではなく `embeddingModel.id` を渡すように変更。
|
||||
|
||||
## 💡 学んだ教訓
|
||||
|
||||
1. **2種類の ID を区別すること**:テーブル主キー vs モデル識別子
|
||||
2. **パラメータ名を明確にすること**:`embeddingModelConfigId` vs `embeddingModelId`
|
||||
3. **呼び出し先の期待値を確認すること**:`EmbeddingService` がどのタイプの ID を求めているか
|
||||
4. **ログ出力の工夫**:デバッグを容易にするため、両方の ID を出力する
|
||||
|
||||
```typescript
|
||||
console.log('使用するモデル:', embeddingModel.name, embeddingModel.modelId, 'ID:', embeddingModel.id);
|
||||
// 出力: 使用するモデル: Text Embedding 3 Large text-embedding-3-large ID: embedding-3
|
||||
```
|
||||
@@ -1,29 +0,0 @@
|
||||
# 功能说明
|
||||
|
||||
## 用户信息显示功能已完成
|
||||
|
||||
此更新为系统添加了以下功能:
|
||||
|
||||
1. 在侧边栏顶部显示当前登录用户的信息,包括:
|
||||
- 用户头像和用户名
|
||||
- 管理员标识(如果用户是管理员)
|
||||
- 用户ID的部分显示
|
||||
|
||||
2. 主要文件变更:
|
||||
- 创建了 `UserInfoDisplay.tsx` 组件
|
||||
- 更新了 `SidebarRail.tsx` 以集成用户信息显示
|
||||
- 更新了 `App.tsx` 以传递 currentUser 数据
|
||||
- 所有现有翻译已支持相关文本
|
||||
|
||||
## 实现细节
|
||||
|
||||
- 用户信息只在侧边栏展开时显示
|
||||
- 使用 Lucide React 图标增强可视化
|
||||
- 支持三种语言的界面文本 (中文/英文/日文)
|
||||
- 管理员用户会显示特殊标记
|
||||
- 界面美观且与现有设计风格保持一致
|
||||
- 避免了信息重复显示
|
||||
|
||||
## 部署
|
||||
|
||||
此功能已准备好部署,无需额外配置。
|
||||
@@ -1,94 +0,0 @@
|
||||
# 内网部署指南 - Simple-KB 知识库系统
|
||||
|
||||
## 概述
|
||||
|
||||
本文档介绍如何在内部网络环境中部署Simple-KB知识库系统,确保所有外部依赖都被移除或替换为内部资源。
|
||||
|
||||
## 主要修改内容
|
||||
|
||||
### 1. 外部CDN资源移除
|
||||
|
||||
已完成修改:
|
||||
- 将 KaTeX CSS 文件从外部 CDN (https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css) 移至本地
|
||||
- `web/index.html` 已更新为引用本地 `/katex/katex.min.css`
|
||||
- KaTeX CSS 文件已复制到 `web/public/katex/katex.min.css`
|
||||
|
||||
### 2. AI模型API配置
|
||||
|
||||
系统本身支持内部模型API配置:
|
||||
- 模型配置通过 `ModelConfig` 实体管理
|
||||
- 支持自定义 `baseUrl` 来指定内部模型服务
|
||||
- 用户可通过UI界面配置内部模型端点
|
||||
|
||||
## 内网部署配置步骤
|
||||
|
||||
### 步骤1: 部署内部AI模型服务
|
||||
|
||||
在启动Simple-KB之前,请确保已部署内部AI模型服务,如:
|
||||
- 自托管的OpenAI兼容接口 (如 vLLM, Text Generation WebUI等)
|
||||
- 内部大语言模型服务
|
||||
- 内部嵌入模型服务
|
||||
|
||||
### 步骤2: 配置模型端点
|
||||
|
||||
1. 启动Simple-KB系统
|
||||
2. 登录系统后,在模型配置页面添加内部模型配置:
|
||||
- LLM模型: 配置内部LLM服务的URL和API密钥
|
||||
- 嵌入模型: 配置内部嵌入服务的URL和API密钥
|
||||
- 重排序模型: 配置内部重排序服务的URL和API密钥
|
||||
|
||||
### 步骤3: Docker配置(可选高级配置)
|
||||
|
||||
如果需要修改Docker构建过程以使用内部注册表,请修改以下文件:
|
||||
|
||||
#### 修改 server/Dockerfile:
|
||||
```dockerfile
|
||||
# 替换这行:
|
||||
RUN yarn config set registry https://registry.npmmirror.com && \
|
||||
# 为:
|
||||
RUN yarn config set registry http://your-internal-npm-registry && \
|
||||
```
|
||||
|
||||
#### 修改 web/Dockerfile:
|
||||
```dockerfile
|
||||
# 替换这行:
|
||||
RUN yarn config set registry https://registry.npmmirror.com && \
|
||||
# 为:
|
||||
RUN yarn config set registry http://your-internal-npm-registry && \
|
||||
```
|
||||
|
||||
#### 修改 libreoffice-server/Dockerfile:
|
||||
```dockerfile
|
||||
# 替换APK仓库源
|
||||
RUN echo "http://your-internal-mirror/alpine/v3.19/main" > /etc/apk/repositories && \
|
||||
echo "http://your-internal-mirror/alpine/v3.19/community" >> /etc/apk/repositories && \
|
||||
|
||||
# 替换pip源
|
||||
RUN pip install --no-cache-dir -r requirements.txt -i http://your-internal-pypi/
|
||||
|
||||
# 替换npm源
|
||||
RUN npm install --registry=http://your-internal-npm-registry
|
||||
```
|
||||
|
||||
### 步骤4: Nginx配置
|
||||
|
||||
如果需要修改Nginx配置以适应内部环境:
|
||||
|
||||
1. 更新 `nginx/conf.d/kb.conf` 中的SSL证书路径
|
||||
2. 根据需要修改服务器名称
|
||||
3. 确保代理路径正确指向内部服务
|
||||
|
||||
## 验证步骤
|
||||
|
||||
1. 确认前端界面正常加载且无外部资源错误
|
||||
2. 测试数学公式渲染功能是否正常(KaTeX功能)
|
||||
3. 配置内部模型服务并测试问答功能
|
||||
4. 确认所有API调用都在内部网络中完成
|
||||
|
||||
## 注意事项
|
||||
|
||||
- 系统的所有核心功能现均可在内部网络中运行
|
||||
- 外部CDN依赖已被完全移除
|
||||
- AI模型服务需单独部署内部实例
|
||||
- 在完全离线环境中,构建过程可能需要预先下载所有依赖包
|
||||
- 如需完全离线部署,建议预构建镜像并部署到内部镜像仓库
|
||||
@@ -1,40 +0,0 @@
|
||||
# 内网部署修改摘要 - Simple-KB 知识库系统
|
||||
|
||||
## 修改概述
|
||||
|
||||
已完成对Simple-KB知识库系统的修改,以支持内部网络环境部署,消除了外部依赖。
|
||||
|
||||
## 具体修改内容
|
||||
|
||||
### 1. 外部CDN资源移除
|
||||
- **文件**: `web/index.html`
|
||||
- **修改**: 将 KaTeX CSS 从外部 CDN (https://cdn.jsdelivr.net/npm/katex@0.16.9/dist/katex.min.css) 更改为本地资源 (/katex/katex.min.css)
|
||||
- **文件**: `web/public/katex/katex.min.css`
|
||||
- **操作**: 从 node_modules 复制 KaTeX CSS 文件到本地目录
|
||||
|
||||
### 2. 文档更新
|
||||
- **新增文件**: `INTERNAL_DEPLOYMENT_GUIDE.md`
|
||||
- **内容**: 详细的内网部署指南,包括配置内部AI模型服务的方法
|
||||
- **更新文件**: `README.md`
|
||||
- **内容**: 添加了内网部署章节,链接到部署指南
|
||||
|
||||
## 系统状态
|
||||
|
||||
✅ **已完成**:
|
||||
- 消除前端外部CDN依赖
|
||||
- 提供内部网络部署文档
|
||||
- 保持所有原有功能完整性
|
||||
|
||||
✅ **系统已准备好在内部网络环境中部署**:
|
||||
- 所有前端资源均为本地资源
|
||||
- AI模型服务可通过配置指向内部服务
|
||||
- 系统不再依赖外部CDN或API端点(除用户自行配置的AI模型外)
|
||||
|
||||
## 部署说明
|
||||
|
||||
要在内部网络中部署此系统:
|
||||
|
||||
1. 按照 `INTERNAL_DEPLOYMENT_GUIDE.md` 的说明进行配置
|
||||
2. 部署内部AI模型服务(如适用)
|
||||
3. 配置模型端点以使用内部服务
|
||||
4. 启动系统并验证功能
|
||||
@@ -1,464 +0,0 @@
|
||||
# ナレッジベースの強化機能設計
|
||||
|
||||
## 🎯 機能概要
|
||||
|
||||
今回の開発には、以下の3つのコア機能が含まれます:
|
||||
|
||||
1. **ナレッジベースのグループ化** - グループを作成し、ドキュメントを複数のグループに所属させ、検索時にグループを指定可能にします。
|
||||
2. **検索履歴** - 対話プロセス全体を保存し、過去の会話の閲覧や再開を可能にします。
|
||||
3. **PDF プレビュー** - すべてのファイルを PDF 形式に変換し、オンラインでプレビューできるようにします。
|
||||
|
||||
## 🗄️ データベース設計
|
||||
|
||||
### 新規テーブル構造
|
||||
|
||||
```sql
|
||||
-- ナレッジベースグループ管理テーブル
|
||||
CREATE TABLE knowledge_groups (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
description TEXT,
|
||||
color TEXT DEFAULT '#3B82F6', -- グループの色分けID
|
||||
user_id TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- ドキュメント・グループ関連付けテーブル (多対多)
|
||||
CREATE TABLE knowledge_base_groups (
|
||||
knowledge_base_id TEXT NOT NULL,
|
||||
group_id TEXT NOT NULL,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (knowledge_base_id, group_id),
|
||||
FOREIGN KEY (knowledge_base_id) REFERENCES knowledge_base(id) ON DELETE CASCADE,
|
||||
FOREIGN KEY (group_id) REFERENCES knowledge_groups(id) ON DELETE CASCADE
|
||||
);
|
||||
|
||||
-- 検索履歴テーブル
|
||||
CREATE TABLE search_history (
|
||||
id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL,
|
||||
title TEXT NOT NULL, -- 対話タイトル (質問の先頭50文字)
|
||||
selected_groups TEXT, -- JSON配列: ["group1", "group2"] または null(すべて)
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- 対話メッセージテーブル
|
||||
CREATE TABLE chat_messages (
|
||||
id TEXT PRIMARY KEY,
|
||||
search_history_id TEXT NOT NULL,
|
||||
role TEXT NOT NULL CHECK (role IN ('user', 'assistant')),
|
||||
content TEXT NOT NULL,
|
||||
sources TEXT, -- JSON配列: 引用ソース情報
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (search_history_id) REFERENCES search_history(id) ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### 既存テーブルの修正
|
||||
|
||||
```sql
|
||||
-- knowledge_base テーブルに PDF パスフィールドを追加
|
||||
ALTER TABLE knowledge_base ADD COLUMN pdf_path TEXT;
|
||||
```
|
||||
|
||||
## 🔌 API エンドポイント設計
|
||||
|
||||
### ナレッジベースグループ API
|
||||
|
||||
```typescript
|
||||
// ユーザーの全グループを取得
|
||||
GET /api/knowledge-groups
|
||||
Response: {
|
||||
groups: Array<{
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
color: string;
|
||||
fileCount: number; // 含まれるファイル数
|
||||
createdAt: string;
|
||||
}>
|
||||
}
|
||||
|
||||
// グループの作成
|
||||
POST /api/knowledge-groups
|
||||
Body: { name: string; description?: string; color?: string }
|
||||
Response: { id: string; name: string; description?: string; color: string }
|
||||
|
||||
// グループの更新
|
||||
PUT /api/knowledge-groups/:id
|
||||
Body: { name?: string; description?: string; color?: string }
|
||||
|
||||
// グループの削除
|
||||
DELETE /api/knowledge-groups/:id
|
||||
|
||||
// グループ内のファイルを取得
|
||||
GET /api/knowledge-groups/:id/files
|
||||
Response: { files: KnowledgeBase[] }
|
||||
|
||||
// ファイルをグループに追加
|
||||
POST /api/knowledge-bases/:fileId/groups
|
||||
Body: { groupIds: string[] }
|
||||
|
||||
// グループからファイルを削除
|
||||
DELETE /api/knowledge-bases/:fileId/groups/:groupId
|
||||
```
|
||||
|
||||
### 検索履歴 API
|
||||
|
||||
```typescript
|
||||
// 検索履歴の取得 (ページネーション)
|
||||
GET /api/search-history?page=1&limit=20
|
||||
Response: {
|
||||
histories: Array<{
|
||||
id: string;
|
||||
title: string;
|
||||
selectedGroups: string[] | null;
|
||||
messageCount: number;
|
||||
lastMessageAt: string;
|
||||
createdAt: string;
|
||||
}>;
|
||||
total: number;
|
||||
page: number;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
// 対話詳細の取得
|
||||
GET /api/search-history/:id
|
||||
Response: {
|
||||
id: string;
|
||||
title: string;
|
||||
selectedGroups: string[] | null;
|
||||
messages: Array<{
|
||||
id: string;
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
sources?: Array<{
|
||||
fileName: string;
|
||||
content: string;
|
||||
score: number;
|
||||
chunkIndex: number;
|
||||
}>;
|
||||
createdAt: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
// 新しい対話の作成
|
||||
POST /api/search-history
|
||||
Body: {
|
||||
title: string;
|
||||
selectedGroups?: string[];
|
||||
firstMessage: string;
|
||||
}
|
||||
Response: { id: string }
|
||||
|
||||
// 対話の削除
|
||||
DELETE /api/search-history/:id
|
||||
|
||||
// 対話の継続 (既存のチャットインターフェースを拡張し、historyId パラメータを追加)
|
||||
POST /api/chat/stream
|
||||
Body: {
|
||||
message: string;
|
||||
history: ChatMessage[];
|
||||
userLanguage?: string;
|
||||
selectedGroups?: string[]; // 新規:選択されたグループ
|
||||
historyId?: string; // 新規:対話履歴ID
|
||||
}
|
||||
```
|
||||
|
||||
### PDF プレビュー API
|
||||
|
||||
```typescript
|
||||
// ファイルの PDF プレビューを取得
|
||||
GET /api/knowledge-bases/:id/pdf
|
||||
Response: PDF ファイルストリーム、または PDF URL へのリダイレクト
|
||||
|
||||
// PDF ステータスの確認
|
||||
GET /api/knowledge-bases/:id/pdf-status
|
||||
Response: {
|
||||
status: 'pending' | 'converting' | 'ready' | 'failed';
|
||||
pdfPath?: string;
|
||||
error?: string;
|
||||
}
|
||||
```
|
||||
|
||||
## 🎨 フロントエンドコンポーネント設計
|
||||
|
||||
### 1. ナレッジベースグループコンポーネント
|
||||
|
||||
```typescript
|
||||
// グループマネージャー
|
||||
interface GroupManagerProps {
|
||||
groups: KnowledgeGroup[];
|
||||
onCreateGroup: (group: CreateGroupData) => void;
|
||||
onUpdateGroup: (id: string, data: UpdateGroupData) => void;
|
||||
onDeleteGroup: (id: string) => void;
|
||||
}
|
||||
|
||||
// グループセレクター (検索時の選択用)
|
||||
interface GroupSelectorProps {
|
||||
groups: KnowledgeGroup[];
|
||||
selectedGroups: string[];
|
||||
onSelectionChange: (groupIds: string[]) => void;
|
||||
showSelectAll?: boolean;
|
||||
}
|
||||
|
||||
// ファイルグループタグ
|
||||
interface FileGroupTagsProps {
|
||||
fileId: string;
|
||||
groups: KnowledgeGroup[];
|
||||
assignedGroups: string[];
|
||||
onGroupsChange: (groupIds: string[]) => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 検索履歴コンポーネント
|
||||
|
||||
```typescript
|
||||
// 履歴リスト
|
||||
interface SearchHistoryListProps {
|
||||
histories: SearchHistoryItem[];
|
||||
onSelectHistory: (historyId: string) => void;
|
||||
onDeleteHistory: (historyId: string) => void;
|
||||
onLoadMore: () => void;
|
||||
hasMore: boolean;
|
||||
}
|
||||
|
||||
// 履歴対話ビューアー
|
||||
interface HistoryViewerProps {
|
||||
historyId: string;
|
||||
onContinueChat: (historyId: string) => void;
|
||||
onClose: () => void;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. PDF プレビューコンポーネント
|
||||
|
||||
```typescript
|
||||
// PDF プレビューアー
|
||||
interface PDFPreviewProps {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
// PDF プレビューボタン
|
||||
interface PDFPreviewButtonProps {
|
||||
fileId: string;
|
||||
fileName: string;
|
||||
status: 'pending' | 'converting' | 'ready' | 'failed';
|
||||
}
|
||||
```
|
||||
|
||||
## 🔄 ビジネスフロー設計
|
||||
|
||||
### ナレッジベースグループ化フロー
|
||||
|
||||
```
|
||||
1. ユーザーがグループを作成 → knowledge_groups テーブルに保存
|
||||
2. ファイルアップロード時 → グループを選択可能 → knowledge_base_groups テーブルに関連付けを保存
|
||||
3. 検索時 → グループを選択 → Elasticsearch のクエリ範囲をフィルタリング
|
||||
4. ファイル管理 → ファイルの所属グループを編集可能
|
||||
```
|
||||
|
||||
### 検索履歴フロー
|
||||
|
||||
```
|
||||
1. ユーザーがチャットを開始 → search_history データを生成
|
||||
2. 各メッセージ → chat_messages テーブルに保存
|
||||
3. 履歴の確認 → 履歴リストをページネーションでロード
|
||||
4. 履歴をクリック → 対話内容全体をロード
|
||||
5. 対話の継続 → 既存の履歴をベースに新しいメッセージを追加
|
||||
```
|
||||
|
||||
### PDF プレビューフロー
|
||||
|
||||
```
|
||||
1. ファイルアップロード → PDF かどうかを確認
|
||||
2. PDF 以外の場合 → LibreOffice を呼び出して PDF に変換
|
||||
3. PDF パスを knowledge_base.pdf_path に保存
|
||||
4. フロントエンドからプレビューをリクエスト → PDF ファイルストリームを返却
|
||||
5. HTML の <embed> または <iframe> を使用して PDF を表示
|
||||
```
|
||||
|
||||
## 🛠️ 技術実装のポイント
|
||||
|
||||
### 1. ES クエリ最適化 (グループフィルタリング)
|
||||
|
||||
```typescript
|
||||
// ElasticsearchService.hybridSearch を修正
|
||||
async hybridSearch(
|
||||
queryVector: number[],
|
||||
queryText: string,
|
||||
userId: string,
|
||||
topK: number = 10,
|
||||
threshold: number = 0.6,
|
||||
selectedGroups?: string[] // 新規パラメータ
|
||||
): Promise<any[]> {
|
||||
|
||||
// グループフィルタリング条件を構築
|
||||
const groupFilter = selectedGroups?.length
|
||||
? { terms: { "knowledge_base_id": await this.getFileIdsByGroups(selectedGroups, userId) } }
|
||||
: undefined;
|
||||
|
||||
// ES クエリにフィルタ条件を追加
|
||||
const query = {
|
||||
bool: {
|
||||
must: [/* 既存のクエリ条件 */],
|
||||
filter: [
|
||||
{ term: { user_id: userId } },
|
||||
...(groupFilter ? [groupFilter] : [])
|
||||
]
|
||||
}
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 2. PDF 変換サービスの統合
|
||||
|
||||
```typescript
|
||||
// KnowledgeBaseService に PDF 変換を追加
|
||||
async ensurePDFExists(kb: KnowledgeBase): Promise<string> {
|
||||
if (kb.pdfPath && await fs.pathExists(kb.pdfPath)) {
|
||||
return kb.pdfPath;
|
||||
}
|
||||
|
||||
if (kb.mimetype === 'application/pdf') {
|
||||
// 既に PDF なので、元のファイルをそのまま使用
|
||||
kb.pdfPath = kb.storagePath;
|
||||
} else {
|
||||
// LibreOffice を呼び出して変換
|
||||
const pdfPath = await this.libreOfficeService.convertToPDF(kb.storagePath);
|
||||
kb.pdfPath = pdfPath;
|
||||
}
|
||||
|
||||
await this.knowledgeBaseRepository.save(kb);
|
||||
return kb.pdfPath;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. チャット履歴の保存
|
||||
|
||||
```typescript
|
||||
// ChatService.streamChat メソッドを修正
|
||||
async *streamChat(
|
||||
message: string,
|
||||
history: ChatMessage[],
|
||||
userId: string,
|
||||
modelConfig: ModelConfig,
|
||||
userLanguage: string = 'zh',
|
||||
selectedEmbeddingId?: string,
|
||||
selectedGroups?: string[], // 新規
|
||||
historyId?: string // 新規
|
||||
): AsyncGenerator<{ type: 'content' | 'sources'; data: any }> {
|
||||
|
||||
// historyId がない場合は、新しい対話履歴を作成
|
||||
if (!historyId) {
|
||||
historyId = await this.createSearchHistory(userId, message, selectedGroups);
|
||||
}
|
||||
|
||||
// ユーザーメッセージを保存
|
||||
await this.saveChatMessage(historyId, 'user', message);
|
||||
|
||||
// ... 既存のロジック ...
|
||||
|
||||
// AI の回答を保存
|
||||
await this.saveChatMessage(historyId, 'assistant', fullResponse, sources);
|
||||
}
|
||||
```
|
||||
|
||||
## 📱 UI/UX 設計のポイント
|
||||
|
||||
### 1. グループ管理インターフェース
|
||||
|
||||
- サイドバーにグループリストを表示
|
||||
- グループへのファイルのドラッグ&ドロップに対応
|
||||
- グループの色分け表示
|
||||
- グループ内のファイル数を表示
|
||||
|
||||
### 2. 検索インターフェースの強化
|
||||
|
||||
- チャット入力欄の上にグループセレクターを追加
|
||||
- 複数グループの選択と状態表示に対応
|
||||
- 「全グループ」オプション
|
||||
|
||||
### 3. 履歴管理インターフェース
|
||||
|
||||
- 左側に履歴リスト、右側に対話内容を表示
|
||||
- 履歴にはタイトル、時間、メッセージ数を表示
|
||||
- 履歴の削除と対話の再開をサポート
|
||||
|
||||
### 4. PDF プレビュー
|
||||
|
||||
- モーダル形式で PDF を表示
|
||||
- フルスクリーン表示をサポート
|
||||
- 読み込み状態の表示とエラー処理
|
||||
|
||||
## 🚀 開発計画
|
||||
|
||||
### ✅ フェーズ1: データベースとバックエンド API (完了)
|
||||
|
||||
1. ✅ データベースのマイグレーションスクリプト
|
||||
2. ✅ グループ管理 API
|
||||
3. ✅ 履歴管理 API
|
||||
4. ✅ PDF プレビュー API
|
||||
5. ✅ チャットサービスの強化 (グループフィルタリングと履歴保存)
|
||||
6. ✅ Elasticsearch のグループフィルタリング機能
|
||||
|
||||
### 🔄 フェーズ2: フロントエンドコンポーネント開発 (進行中)
|
||||
|
||||
1. ⏳ グループ管理コンポーネント (基本機能は実装済み。アクセス方法を最適化予定)
|
||||
2. ⏳ 履歴管理コンポーネント (基本機能は実装済み)
|
||||
3. ⏳ PDF プレビューコンポーネント (基本機能は実装済み)
|
||||
4. ✅ **UI の刷新と設定の統合**: ヘッダーとサイドバーを整理し、設定の入り口を統一。新機能のためのスペースを確保。
|
||||
|
||||
### ⏳ フェーズ3: 統合とテスト (待機中)
|
||||
|
||||
1. ⏳ 機能の統合
|
||||
2. ⏳ エンドツーエンド (E2E) テスト
|
||||
3. ⏳ パフォーマンスの最適化
|
||||
|
||||
---
|
||||
|
||||
## ✅ 完了済みのバックエンド開発
|
||||
|
||||
### データベース設計
|
||||
|
||||
- ✅ 4つの新しいテーブルを作成:`knowledge_groups`、`knowledge_base_groups`、`search_history`、`chat_messages`
|
||||
- ✅ `knowledge_base` テーブルに `pdf_path` フィールドを追加
|
||||
- ✅ 完全なデータベースマイグレーションスクリプトを作成
|
||||
|
||||
### エンティティとサービス
|
||||
|
||||
- ✅ `KnowledgeGroup` エンティティとサービス (多対多関係をサポート)
|
||||
- ✅ `SearchHistory` および `ChatMessage` エンティティとサービス
|
||||
- ✅ `KnowledgeBase` エンティティを更新し、グループ関係と PDF パスを追加
|
||||
|
||||
### API エンドポイント
|
||||
|
||||
- ✅ ナレッジベースグループ管理 : `GET/POST/PUT/DELETE /api/knowledge-groups`
|
||||
- ✅ ファイル・グループ関連付け : `POST/DELETE /api/knowledge-bases/:id/groups`
|
||||
- ✅ 検索履歴管理 : `GET/POST/DELETE /api/search-history`
|
||||
- ✅ PDF プレビュー : `GET /api/knowledge-bases/:id/pdf` および `GET /api/knowledge-bases/:id/pdf-status`
|
||||
|
||||
### チャット機能の強化
|
||||
|
||||
- ✅ グループフィルタリング検索をサポート (`selectedGroups` パラメータ)
|
||||
- ✅ 対話履歴の自動生成と保存
|
||||
- ✅ 対話の再開をサポート (`historyId` パラメータ)
|
||||
- ✅ Elasticsearch によるグループフィルタリングクエリ
|
||||
|
||||
### テストと検証
|
||||
|
||||
- ✅ 自動テストスクリプト `test-enhancements.sh` を作成
|
||||
- ✅ すべての API エンドポイントが実装され、テスト可能
|
||||
|
||||
**バックエンド開発ステータス**: ✅ **完了** (約 95%)
|
||||
|
||||
**次のステップ**: フロントエンドコンポーネントの開発を開始
|
||||
|
||||
---
|
||||
|
||||
**予想開発期間**: 5〜8日
|
||||
**優先度**: グループ化機能 > PDF プレビュー > 履歴管理
|
||||
@@ -1,139 +0,0 @@
|
||||
# 大容量ファイルの処理最適化スキーム
|
||||
|
||||
## 🎯 背景
|
||||
|
||||
システムは大容量ファイルを処理する際に、メモリオーバーフローの問題を抱えていました:
|
||||
|
||||
- ファイルアップロード時にファイル全体がメモリに読み込まれる。
|
||||
- テキストのチャンク(分割)時に大量のチャンクオブジェクトが生成される。
|
||||
- ベクトル化時にすべてのベクトルが同時にメモリ上に保持される。
|
||||
- 例:500MB のドキュメントを処理する場合、7GB 以上のメモリが必要になる可能性がある。
|
||||
|
||||
## ✅ 実施済みの修正案
|
||||
|
||||
### 1. フロントエンドの最適化
|
||||
|
||||
- **デフォルトチャンクサイズ**: 500 から 200 トークンに削減 (チャンク数を 60% 削減)。
|
||||
- **ファイルサイズ制限**: 上限を 100MB に設定し、フロントエンドで検証。
|
||||
- **ユーザーへの通知**: 明確なエラーメッセージと改善アドバイスを追加。
|
||||
|
||||
### 2. バックエンドの検証
|
||||
|
||||
- **ファイル形式フィルタリング**: サポートされている形式のみを許可。
|
||||
- **サイズ検証**: バックエンドでもファイルサイズを二重チェック。
|
||||
- **設定パラメータの制限**: チャンク設定を安全な範囲に自動調整。
|
||||
|
||||
### 3. メモリ監視サービス
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class MemoryMonitorService {
|
||||
private readonly MAX_MEMORY_MB = 1024; // 1GB 上限
|
||||
private readonly BATCH_SIZE = 100; // 1バッチあたり 100 チャンク
|
||||
|
||||
// 大量データをバッチ処理
|
||||
async processInBatches<T, R>(items: T[], processor): Promise<R[]> {
|
||||
// バッチサイズを動的に調整
|
||||
// メモリ監視と GC (ガベージコレクション) のトリガー
|
||||
// メモリオーバーフローを回避
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. バッチベクトル化
|
||||
|
||||
- **バッチサイズ**: 100 チャンク / バッチ。
|
||||
- **メモリ監視**: メモリ使用状況をリアルタイムでチェック。
|
||||
- **自動 GC**: メモリがしきい値を超えた場合にガベージコレクションを強制実行。
|
||||
- **動的調整**: メモリ使用状況に基づいてバッチサイズを調整。
|
||||
|
||||
## 📊 最適化の効果
|
||||
|
||||
### 修正前 vs 修正後
|
||||
|
||||
| 指標 | 修正前 | 修正後 | 改善率 |
|
||||
|------|--------|--------|------|
|
||||
| メモリピーク | 7GB以上 | <1GB | 85%以上 |
|
||||
| チャンク数 | 500,000 | 1,000,000 (バッチ処理) | 安定的な処理 |
|
||||
| 処理方式 | 全量一括読み込み | バッチ処理 | メモリ制御可能 |
|
||||
| システム安定性 | 頻繁にクラッシュ | 安定稼働 | 顕著な向上 |
|
||||
|
||||
### テスト結果
|
||||
|
||||
| ファイルサイズ | 処理時間 | メモリピーク | ステータス |
|
||||
|---------|---------|---------|------|
|
||||
| 10MB | 8秒 | 280MB | ✅ |
|
||||
| 50MB | 35秒 | 450MB | ✅ |
|
||||
| 100MB | 72秒 | 680MB | ✅ |
|
||||
|
||||
## 🔧 設定パラメータ
|
||||
|
||||
### 環境変数
|
||||
|
||||
```env
|
||||
# ファイルアップロードの制限
|
||||
MAX_FILE_SIZE=104857600 # 100MB
|
||||
|
||||
# メモリ管理
|
||||
MAX_MEMORY_USAGE_MB=1024 # メモリ上限
|
||||
CHUNK_BATCH_SIZE=100 # バッチサイズ
|
||||
GC_THRESHOLD_MB=800 # GC トリガーしきい値
|
||||
|
||||
# チャンク設定
|
||||
DEFAULT_CHUNK_SIZE=200 # デフォルトチャンクサイズ
|
||||
DEFAULT_CHUNK_OVERLAP=40 # デフォルトオーバーラップサイズ
|
||||
```
|
||||
|
||||
### Docker 設定
|
||||
|
||||
```yaml
|
||||
services:
|
||||
server:
|
||||
environment:
|
||||
- NODE_OPTIONS=--max-old-space-size=2048
|
||||
- MAX_FILE_SIZE=104857600
|
||||
- CHUNK_BATCH_SIZE=100
|
||||
- MAX_MEMORY_USAGE_MB=1024
|
||||
```
|
||||
|
||||
## 🚀 今後の最適化の方向性
|
||||
|
||||
### フェーズ2: ストリーミングアーキテクチャ
|
||||
|
||||
- **ストリーミングテキスト抽出**: 全文をキャッシュせず、読み取ると同時に処理。
|
||||
- **ストリーミングチャンキング**: 一度に一つのテキストブロックのみを処理。
|
||||
- **増分インデックス**: チャンク、ベクトル化、インデックス化を一つずつ順次実行。
|
||||
|
||||
### フェーズ3: 非同期キュー
|
||||
|
||||
- **タスクキュー**: Redis/BullMQ によるバックグラウンド処理。
|
||||
- **進捗フィードバック**: リアルタイムな進捗バー表示。
|
||||
- **フォールトトレランス**: 失敗時の自動リトライと復旧。
|
||||
|
||||
### フェーズ4: 分散処理
|
||||
|
||||
- **マルチプロセス処理**: マルチコア CPU の活用。
|
||||
- **負荷分散**: 処リ負荷の分散。
|
||||
- **横断的拡張**: クラスターデプロイのサポート。
|
||||
|
||||
## 💡 推奨される使用方法
|
||||
|
||||
### ファイルサイズのアドバイス
|
||||
|
||||
- **小規模ファイル (<10MB)**: 通常通り処理されます。
|
||||
- **中規模ファイル (10-50MB)**: チャンクサイズを適宜調整してください。
|
||||
- **大規模ファイル (50-100MB)**: デフォルト設定のまま、処理完了までお待ちください。
|
||||
- **超大規模ファイル (>100MB)**: 事前に分割するか、専門のツールでプレ処理することを推奨します。
|
||||
|
||||
### パフォーマンス向上のアドバイス
|
||||
|
||||
- 同時にアップロードするファイル数を制限してください。
|
||||
- チャンクサイズを適切に調整してください(推奨:200-500 トークン)。
|
||||
- 不要になったインデックスデータを定期的に整理してください。
|
||||
- システムのメモリ使用状況を監視してください。
|
||||
|
||||
---
|
||||
|
||||
**ステータス**: 実施・検証済み
|
||||
**バージョン**: v1.0
|
||||
**更新日**: 2025-01-14
|
||||
@@ -1,348 +0,0 @@
|
||||
# 大容量ファイルアップロード時のメモリオーバーフロー修正のまとめ
|
||||
|
||||
## 問題の分析
|
||||
|
||||
### 根本的な原因
|
||||
|
||||
旧アーキテクチャには、大容量ファイルを処理する際のメモリボトルネックが複数存在していました:
|
||||
|
||||
1. **TikaService** - `fs.readFileSync()` により、ファイル全体を一度にメモリへ読み込んでいました。
|
||||
2. **TextChunkerService** - `chunkText()` が、生成されたすべてのチャンクを保持する配列を返していました。
|
||||
3. **KnowledgeBaseService** - すべてのチャンクのベクトルを一度に生成し、メモリ上に保持していました。
|
||||
4. **EmbeddingService** - すべてのチャンクの埋め込みベクトルを一括でリクエストしていました。
|
||||
|
||||
### メモリ使用量の推定(500MB ドキュメントの例)
|
||||
|
||||
| フェーズ | メモリ使用量 | 説明 |
|
||||
|------|----------|------|
|
||||
| Tika 抽出 | 約 1GB | 元ファイル + テキストデータ |
|
||||
| チャンク分割 | 約 500MB | 50万個のチャンクオブジェクト |
|
||||
| 一括ベクトル化 | 約 5.5GB | 50万個 × 2560次元 × 4バイト |
|
||||
| **合計ピーク時** | **約 7GB以上** | 制限を大幅に超過 |
|
||||
|
||||
---
|
||||
|
||||
## クイック修正案(実施済み)
|
||||
|
||||
### 1. フロントエンドの最適化
|
||||
|
||||
#### デフォルト設定の変更
|
||||
|
||||
**ファイル**: `web/components/IndexingModal.tsx`
|
||||
|
||||
```typescript
|
||||
// 変更前
|
||||
const [chunkSize, setChunkSize] = useState(500);
|
||||
const [chunkOverlap, setChunkOverlap] = useState(50);
|
||||
|
||||
// 変更後
|
||||
const [chunkSize, setChunkSize] = useState(200); // 50% 削減
|
||||
const [chunkOverlap, setChunkOverlap] = useState(40); // 20% 削減
|
||||
```
|
||||
|
||||
**効果**: チャンク数を約 60% 削減し、メモリ負荷を軽減。
|
||||
|
||||
#### ファイルサイズ制限の追加
|
||||
|
||||
**ファイル**: `web/App.tsx`
|
||||
|
||||
```typescript
|
||||
const MAX_FILE_SIZE = 104857600; // 100MB
|
||||
const MAX_SIZE_MB = 100;
|
||||
|
||||
// 検証ロジック
|
||||
if (file.size > MAX_FILE_SIZE) {
|
||||
errors.push(`${file.name} - ${MAX_SIZE_MB}MB の制限を超えています`);
|
||||
continue;
|
||||
}
|
||||
```
|
||||
|
||||
**効果**: 超大容量ファイルのアップロードをブロックし、フロントエンドで即座にフィードバック。
|
||||
|
||||
---
|
||||
|
||||
### 2. バックエンドの最適化
|
||||
|
||||
#### ファイルアップロード制限
|
||||
|
||||
**ファイル**: `server/src/upload/upload.module.ts`
|
||||
|
||||
```typescript
|
||||
MulterModule.registerAsync({
|
||||
useFactory: (configService: ConfigService) => {
|
||||
const maxFileSize = parseInt(
|
||||
configService.get<string>('MAX_FILE_SIZE', '104857600')
|
||||
);
|
||||
|
||||
return {
|
||||
storage: multer.diskStorage({...}),
|
||||
limits: {
|
||||
fileSize: maxFileSize, // 100MB 制限
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
#### アップロードコントローラーの強化
|
||||
|
||||
**ファイル**: `server/src/upload/upload.controller.ts`
|
||||
|
||||
```typescript
|
||||
// 1. ファイル形式のフィルタリング
|
||||
const allowedMimeTypes = [
|
||||
'application/pdf',
|
||||
'application/msword',
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
|
||||
'text/plain',
|
||||
'image/jpeg', 'image/png', 'image/gif', 'image/webp'
|
||||
];
|
||||
|
||||
// 2. ファイルサイズの検証
|
||||
if (file.size > maxFileSize) {
|
||||
throw new BadRequestException(
|
||||
`ファイルサイズが制限を超えています: ${this.formatBytes(file.size)}、最大許可: ${this.formatBytes(maxFileSize)}`
|
||||
);
|
||||
}
|
||||
|
||||
// 3. 設定パラメータの安全な制限
|
||||
const indexingConfig = {
|
||||
chunkSize: Math.max(100, Math.min(2000, config.chunkSize || 200)),
|
||||
chunkOverlap: Math.max(0, Math.min(500, config.chunkOverlap || 40)),
|
||||
// オーバーラップがチャンクサイズの 50% を超えないように調整
|
||||
chunkOverlap: Math.min(chunkOverlap, chunkSize * 0.5)
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. コアメモリ管理
|
||||
|
||||
#### メモリ監視サービス(新規作成)
|
||||
|
||||
**ファイル**: `server/src/knowledge-base/memory-monitor.service.ts`
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class MemoryMonitorService {
|
||||
private readonly MAX_MEMORY_MB = 1024; // 1GB 上限
|
||||
private readonly BATCH_SIZE = 100; // 1バッチ 100 チャンク
|
||||
private readonly GC_THRESHOLD_MB = 800; // GC トリガーしきい値
|
||||
|
||||
// メモリ使用状況の取得
|
||||
getMemoryUsage(): MemoryStats { ... }
|
||||
|
||||
// メモリが空くまで待機(タイムアウトあり)
|
||||
async waitForMemoryAvailable(): Promise<void> { ... }
|
||||
|
||||
// バッチサイズを動的に調整
|
||||
getDynamicBatchSize(currentMemoryMB: number): number { ... }
|
||||
|
||||
// 大量データのバッチ処理
|
||||
async processInBatches<T, R>(items: T[], processor): Promise<R[]> { ... }
|
||||
|
||||
// メモリ使用量の推定
|
||||
estimateMemoryUsage(itemCount, itemSizeBytes, vectorDim): number { ... }
|
||||
}
|
||||
```
|
||||
|
||||
#### 刷新された KnowledgeBaseService
|
||||
|
||||
**ファイル**: `server/src/knowledge-base/knowledge-base.service.ts`
|
||||
|
||||
```typescript
|
||||
private async vectorizeToElasticsearch(kbId, userId, text, config) {
|
||||
// 1. テキストのチャンク分割
|
||||
const chunks = this.textChunkerService.chunkText(text, chunkSize, chunkOverlap);
|
||||
|
||||
// 2. メモリ使用量を推定し、バッチ処理が必要か判断
|
||||
const useBatching = this.memoryMonitor.shouldUseBatching(
|
||||
chunks.length,
|
||||
avgChunkSize,
|
||||
defaultDimensions
|
||||
);
|
||||
|
||||
if (useBatching) {
|
||||
// 3. バッチ処理を実行
|
||||
await this.processInBatches(chunks, async (batch, batchIndex) => {
|
||||
// 3.1 バッチ単位でベクトルを生成
|
||||
const embeddings = await this.embeddingService.getEmbeddings(
|
||||
batch.map(c => c.content),
|
||||
userId,
|
||||
kb.embeddingModelId
|
||||
);
|
||||
|
||||
// 3.2 即座に Elasticsearch へインデックス
|
||||
for (let i = 0; i < batch.length; i++) {
|
||||
await this.elasticsearchService.indexDocument(...);
|
||||
}
|
||||
|
||||
// 3.3 参照のクリア
|
||||
batch.length = 0;
|
||||
});
|
||||
} else {
|
||||
// 小規模ファイルの一括処理
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 設定パラメータ
|
||||
|
||||
### 環境変数 (server/.env)
|
||||
|
||||
```env
|
||||
# ファイルアップロード設定
|
||||
UPLOAD_FILE_PATH=./uploads
|
||||
MAX_FILE_SIZE=104857600 # 100MB
|
||||
|
||||
# ベクトル次元
|
||||
DEFAULT_VECTOR_DIMENSIONS=2560
|
||||
|
||||
# メモリ管理設定
|
||||
MAX_MEMORY_USAGE_MB=1024 # メモリ上限 (MB)
|
||||
CHUNK_BATCH_SIZE=100 # バッチサイズ (チャンク数)
|
||||
GC_THRESHOLD_MB=800 # GC トリガーしきい値 (MB)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 改善の効果
|
||||
|
||||
### 最適化前(500MB ドキュメント)
|
||||
|
||||
- **チャンクサイズ**: 500 tokens
|
||||
- **チャンク数**: 約 500,000
|
||||
- **メモリピーク**: 約 7GB以上
|
||||
- **結果**: メモリ溢れによるプロセス停止
|
||||
|
||||
### 最適化後(500MB ドキュメント)
|
||||
|
||||
- **チャンクサイズ**: 200 tokens (デフォルト)
|
||||
- **チャンク数**: 約 1,000,000 (バッチ処理により制御)
|
||||
- **メモリピーク**: 1GB未満 (MAX_MEMORY_USAGE_MB で制限)
|
||||
- **結果**: 正常に処理完了、メモリ消費が安定
|
||||
|
||||
---
|
||||
|
||||
## 処理フローの比較
|
||||
|
||||
### 旧フロー
|
||||
|
||||
```
|
||||
ファイル → Tika 抽出(全量) → 切片(全量) → 向量(全量) → 索引(全量)
|
||||
↑ ↑ ↑ ↑
|
||||
ピーク: 7GB+ ピーク: 7GB+ ピーク: 7GB+ ピーク: 7GB+
|
||||
```
|
||||
|
||||
### 新フロー
|
||||
|
||||
```
|
||||
ファイル → Tika 抽出 → チャンク分割 → メモリ評価 → バッチ処理
|
||||
↓
|
||||
┌────────┴────────┐
|
||||
│ バッチ1 (100チャンク) │ → ベクトル化 → インデックス → クリア
|
||||
│ バッチ2 (100チャンク) │ → ベクトル化 → インデックス → クリア
|
||||
│ ... │
|
||||
└─────────────────┘
|
||||
ピーク: <1GB ピーク: <1GB ピーク: <1GB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 監視とログ
|
||||
|
||||
### メモリ監視ログの例
|
||||
|
||||
```
|
||||
[KnowledgeBaseService] メモリ状態 - 処理前: 256/1024MB
|
||||
[KnowledgeBaseService] 推定メモリ使用量: 1200MB
|
||||
[KnowledgeBaseService] 推定メモリ 1200MB がしきい値 716MB を超えたため、バッチ処理を使用します
|
||||
[MemoryMonitorService] バッチ処理開始: 500,000 項目
|
||||
[MemoryMonitorService] 処理中 1/5000 バッチ: 100 項目
|
||||
[KnowledgeBaseService] バッチ 1/5000 完了, 現在のメモリ: 280MB
|
||||
[MemoryMonitorService] メモリ消費が高いため、解放待ち... 950/1024MB
|
||||
[MemoryMonitorService] 強制ガベージコレクションを実行中...
|
||||
[MemoryMonitorService] GC 完了: 950MB → 320MB (630MB 解放)
|
||||
...
|
||||
[KnowledgeBaseService] バッチ処理完了: 500,000 項目, 所要時間 125.3s, 最終メモリ 350MB
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 今後の最適化の方向性
|
||||
|
||||
### フェーズ2:ストリーミングアーキテクチャ(推奨)
|
||||
|
||||
1. **ストリーミングテキスト抽出** - 全文をキャッシュせず、読み取りながら処理。
|
||||
2. **ストリーミングチャンキング** - 一度に一つのテキストブロックのみを処理。
|
||||
3. **増分インデックス** - チャンクごとにベクトル化とインデックス化を順次実行。
|
||||
|
||||
### フェーズ3:非同期キュー
|
||||
|
||||
1. **タスクキュー** - Redis/BullMQ を活用。
|
||||
2. **バックグラウンド処理** - メインスレッドをブロックしないよう設計。
|
||||
3. **進捗フィードバック** - リアルタイムな進捗バー表示。
|
||||
|
||||
---
|
||||
|
||||
## テストと検証
|
||||
|
||||
### テストシナリオ
|
||||
|
||||
| ファイルサイズ | チャンクサイズ | チャンク数 | 処理時間 | メモリピーク | 結果 |
|
||||
|----------|----------|----------|----------|----------|------|
|
||||
| 10MB | 200 | 20,000 | 8秒 | 280MB | ✅ |
|
||||
| 50MB | 200 | 100,000 | 35秒 | 450MB | ✅ |
|
||||
| 100MB | 200 | 200,000 | 72秒 | 680MB | ✅ |
|
||||
| 500MB | 200 | 1,000,000 | 310秒 | 950MB | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## デプロイのアドバイス
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
server:
|
||||
environment:
|
||||
- NODE_OPTIONS=--max-old-space-size=2048
|
||||
- MAX_FILE_SIZE=104857600
|
||||
- CHUNK_BATCH_SIZE=100
|
||||
- MAX_MEMORY_USAGE_MB=1024
|
||||
- GC_THRESHOLD_MB=800
|
||||
```
|
||||
|
||||
### 本番環境のモニタリング
|
||||
|
||||
- メモリ使用率の監視
|
||||
- 処理時間の計測
|
||||
- エラー率の追跡
|
||||
- アラートしきい値の設定
|
||||
|
||||
---
|
||||
|
||||
## まとめ
|
||||
|
||||
### 主要な改善点
|
||||
|
||||
1. ✅ **フロントエンドの制限**: デフォルトのチャンクサイズ縮小、ファイルサイズ制限。
|
||||
2. ✅ **バックエンドの検証**: ファイル形式、サイズ、設定値のバリデーション。
|
||||
3. ✅ **バッチ処理**: 100 チャンクごとの処理、および動的な調整。
|
||||
4. ✅ **メモリ監視**: リアルタイム監視と自動ガベージコレクション。
|
||||
5. ✅ **設定の柔軟化**: 環境変数による全パラメータの制御。
|
||||
|
||||
### メモリ最適化の効果
|
||||
|
||||
- **ピークメモリ**: 7GB以上から 1GB未満へ削減。
|
||||
- **安定性**: メモリ溢れによる停止を回避。
|
||||
- **拡張性**: より大容量のファイル処理に対応。
|
||||
|
||||
### ユーザー体験の向上
|
||||
|
||||
- 明確なエラー表示。
|
||||
- 合理的な初期構成。
|
||||
- 処理の進捗を可視化。
|
||||
- システム全体の安定稼働。
|
||||
@@ -1,90 +0,0 @@
|
||||
# PDF プレビュー機能の修正に関する説明
|
||||
|
||||
## 問題の分析
|
||||
|
||||
これまでの PDF プレビュー機能には、以下の問題がありました:
|
||||
|
||||
1. プレビューボタンをクリックした際、PDF のステータスチェックのみが行われ、変換処理が能動的に実行されていませんでした。
|
||||
2. フロントエンドで HEAD リクエストによるプリロードを行っていましたが、これではバックエンドの変換ロジックをトリガーできませんでした。
|
||||
3. LibreOffice サービスから返されるパスの処理が不適切でした。
|
||||
4. エラー処理が不足しており、ユーザーへのフィードバックが不十分でした。
|
||||
|
||||
## 修正内容
|
||||
|
||||
### 1. バックエンドの修正 (knowledge-base.service.ts)
|
||||
|
||||
- `ensurePDFExists` メソッドを修正し、PDF ファイルのパスを正しく処理するようにしました。
|
||||
- `getPDFStatus` メソッドを改善し、ステータスチェックの正確性を確保しました。
|
||||
- LibreOffice の変換ロジックを最適化し、PDF ファイルが正しい場所に保存されるようにしました。
|
||||
|
||||
### 2. LibreOffice サービスの修正 (libreoffice.service.ts)
|
||||
|
||||
- 変換ロジックを修正し、PDF ファイルがローカルファイルシステムに保存されるようにしました。
|
||||
- 重複した変換を避けるため、PDF ファイルの存在チェックを追加しました。
|
||||
- インターフェース定義を更新し、多様なレスポンス形式に対応しました。
|
||||
|
||||
### 3. フロントエンドの修正 (PDFPreview.tsx)
|
||||
|
||||
- ステータスチェックのロジックを変更し、`pending` 状態の際、能動的に変換をトリガーするようにしました。
|
||||
- エラー処理を改善し、ダウンロードや新しいウィンドウでの表示オプションを追加しました。
|
||||
- ユーザー体験向上のため、iframe のエラーハンドリングを追加しました。
|
||||
- UI へのフィードバックを最適化し、変換の進捗を分かりやすく表示するようにしました。
|
||||
|
||||
### 4. サービス層の修正 (pdfPreviewService.ts)
|
||||
|
||||
- プリロードメソッドを GET リクエストに変更し、変換をトリガーするようにしました。
|
||||
- 長時間の待機を避けるため、タイムアウト制御を追加しました。
|
||||
|
||||
## 新しいワークフロー
|
||||
|
||||
1. **ユーザーがプレビューボタンをクリック**
|
||||
- PDF プレビューのポップアップが開きます。
|
||||
- 「PDF を変換する準備をしています...」と表示されます。
|
||||
|
||||
2. **PDF ステータスのチェック**
|
||||
- `/api/knowledge-bases/:id/pdf-status` を呼び出します。
|
||||
- ステータスが `pending` の場合、次のステップに進みます。
|
||||
|
||||
3. **変換のトリガー**
|
||||
- `/api/knowledge-bases/:id/pdf` を呼び出します(GET リクエスト)。
|
||||
- バックエンドが `ensurePDFExists` メソッドを実行します。
|
||||
- 変換が必要な場合、LibreOffice サービスを呼び出します。
|
||||
|
||||
4. **ステータスのポーリング**
|
||||
- 3秒ごとにステータスをチェックします。
|
||||
- 「PDF を変換しています...」と表示されます。
|
||||
- ステータスが `ready` または `failed` になるまで継続します。
|
||||
|
||||
5. **結果の表示**
|
||||
- 成功:iframe 内に PDF を表示します。
|
||||
- 失敗:エラーメッセージと代替案(ダウンロード、新しいウィンドウで開く)を表示します。
|
||||
|
||||
## テスト手順
|
||||
|
||||
1. すべてのサービスを起動します:
|
||||
|
||||
```bash
|
||||
docker-compose up -d elasticsearch tika libreoffice
|
||||
yarn dev
|
||||
```
|
||||
|
||||
2. PDF 以外のファイル(Word 文書、PPT など)をアップロードします。
|
||||
|
||||
3. ファイルの横にある「目」のアイコンをクリックします。
|
||||
|
||||
4. 変換プロセスを確認します:
|
||||
- 「PDF を変換しています...」と表示されるはずです。
|
||||
- 数分後、PDF の内容が表示されます。
|
||||
- 失敗した場合は、エラーメッセージと代替案が表示されます。
|
||||
|
||||
## サポートされるファイル形式
|
||||
|
||||
- Microsoft Office: .doc, .docx, .ppt, .pptx, .xls, .xlsx
|
||||
- OpenDocument: .odt, .odp, .ods
|
||||
- その他: .rtf, .txt
|
||||
|
||||
## 注意事項
|
||||
|
||||
- 大容量ファイルの場合、変換には数分かかることがあります。
|
||||
- 変換に失敗した場合は、元のファイルのダウンロードを試みてください。
|
||||
- 重複した変換を避けるため、一度変換された PDF はキャッシュ(保存)されます。
|
||||
@@ -1,225 +0,0 @@
|
||||
# Simple Knowledge Base (simple-kb) 技術および機能アーキテクチャ
|
||||
|
||||
## 1. プロジェクト概要
|
||||
|
||||
**Simple Knowledge Base (simple-kb)** は、React と NestJS をベースにしたフルスタックのRAG(検索拡張生成)システムです。ユーザーは多様な形式のドキュメントをアップロードし、カスタム設定でインデックス化を行い、大規模言語モデル(LLM)を用いてナレッジベースに基づいた高度な問答を行うことができます。
|
||||
|
||||
最近のアップデートでは、Google NotebookLM に触発された「ナレッジグループ(Notebooks)」機能や「ポッドキャスト生成」機能が追加され、単なる検索システムを超えた学習・分析プラットフォームへと進化しています。
|
||||
|
||||
---
|
||||
|
||||
## 2. 技術アーキテクチャ
|
||||
|
||||
以下は、システムの全体的な技術アーキテクチャを示す図です。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
User[ユーザー] --> |HTTPS| Frontend[React フロントエンド]
|
||||
Frontend --> |REST API / SSE| Backend[NestJS バックエンド]
|
||||
|
||||
subgraph "Backend Services"
|
||||
Backend --> |ORM| SQLite[(SQLite DB)]
|
||||
Backend --> |Vector Search| ES[(Elasticsearch)]
|
||||
Backend --> |File Process| Tika[Apache Tika]
|
||||
Backend --> |Doc Convert| LibreOffice[LibreOffice]
|
||||
end
|
||||
|
||||
subgraph "AI Services (External)"
|
||||
Backend --> |API Call| LLM["LLM Provider\n(OpenAI/Gemini/Claude)"]
|
||||
Backend --> |Embedding| Embed["Embedding Model"]
|
||||
Backend --> |Rerank| Rerank["Rerank Model"]
|
||||
end
|
||||
```
|
||||
|
||||
### 2.1 フロントエンド (Frontend)
|
||||
|
||||
モダンなReactエコシステムを採用し、高速でインタラクティブなUIを実現しています。
|
||||
|
||||
- **フレームワーク**: React 19 + Vite
|
||||
- 最新のReact機能(Hooks, Context API)を活用。
|
||||
- Viteによる高速な開発サーバーとビルド。
|
||||
- **言語**: TypeScript
|
||||
- 型安全性による堅牢なコードベース。
|
||||
- **スタイリング**: Tailwind CSS
|
||||
- ユーティリティファーストCSSによる迅速なUI構築。
|
||||
- **UIコンポーネント**: Lucide React (アイコン)
|
||||
- **状態管理**: React Context + Hooks
|
||||
- `AuthContext`, `LanguageContext` などでグローバル状態を管理。
|
||||
- **通信**: Axios + Server-Sent Events (SSE)
|
||||
- RESTful APIとの通信およびAI生成テキストの流式表示(ストリーミング)。
|
||||
|
||||
### 2.2 バックエンド (Backend)
|
||||
|
||||
スケーラブルでモジュール化されたNode.jsアプリケーションです。
|
||||
|
||||
- **フレームワーク**: NestJS
|
||||
- Angularに影響を受けたモジュラーアーキテクチャ。
|
||||
- TypeScriptによる完全な型サポート。
|
||||
- **AIオーケストレーション**: LangChain.js
|
||||
- LLM、エンベディング、ベクターストアの統合管理。
|
||||
- **データベース**:
|
||||
- **SQLite**: ユーザー情報、設定、ファイルメタデータなどのリレーショナルデータ。
|
||||
- **Elasticsearch**: ドキュメントのベクトル埋め込み(Embedding)と全文検索インデックス。ベクトル次元数の自動検出とインデックス再構築に対応。
|
||||
- **認証**: Passport.js + JWT
|
||||
- セキュアなステートレス認証。
|
||||
|
||||
### 2.3 インフラ・ファイル処理
|
||||
|
||||
- **ファイル解析**:
|
||||
- **Apache Tika**: 「高速モード」でのテキスト抽出。
|
||||
- **Vision Pipeline**: 「精密モード」での画像・レイアウト解析(PDF -> 画像 -> Vision Model)。
|
||||
- **ドキュメント変換**: LibreOffice
|
||||
- Office文書(Word, PPTなど)をPDFに変換して処理。
|
||||
- **音声生成**: Edge-TTS (または類似サービス)
|
||||
- ポッドキャスト生成機能における音声合成。
|
||||
|
||||
---
|
||||
|
||||
## 3. 機能アーキテクチャ
|
||||
|
||||
システムは以下の主要な機能モジュールで構成されています。
|
||||
|
||||
### 3.1 ユーザー管理とセキュリティ
|
||||
|
||||
- **認証**: ユーザー登録、ログイン、JWTによるセッション管理。
|
||||
- **データ隔離**: 各ユーザーは独自のナレッジベース、設定、チャット履歴を持ち、他ユーザーからはアクセスできません。
|
||||
- **多言語UI**: 英語、中国語、日本語のインターフェース切り替えに対応。
|
||||
|
||||
### 3.2 知識管理 (Knowledge Management)
|
||||
|
||||
- **ファイルアップロード**:
|
||||
- ドラッグ&ドロップによる複数ファイルアップロード。
|
||||
- 対応フォーマット: PDF, Word, Excel, PPT, TXT, MD, 画像など。
|
||||
- **処理モード**:
|
||||
- **高速モード (Fast Mode)**: テキストのみを高速に抽出。コスト効率が良い。
|
||||
- **精密モード (Precise Mode)**: ページを画像化し、Visionモデルでレイアウトや図表を含めて解析。
|
||||
- **インデックス設定**:
|
||||
- チャンクサイズ(Chunk Size)、オーバーラップ(Overlap)のカスタマイズ。
|
||||
- エンベディングモデルの選択(OpenAI, Geminiなど)。
|
||||
|
||||
### 3.3 ナレッジグループ (Knowledge Groups)
|
||||
|
||||
- **概念**: ファイルを論理的なグループ(ノートブック)にまとめる機能。
|
||||
- **目的**: 特定の研究テーマやプロジェクトごとに資料を整理し、そのグループに限定したチャットが可能。
|
||||
- **機能**: グループの作成、編集、削除、ファイルとの関連付け。
|
||||
|
||||
### 3.4 RAGチャットシステム
|
||||
|
||||
- **ハイブリッド検索**:
|
||||
- ベクトル検索(意味的類似性)とキーワード検索(完全一致)を組み合わせ、リランク(Rerank)モデルで精度を向上。
|
||||
- **コンテキスト認識**: ユーザーの質問履歴や現在選択されているナレッジグループを考慮。
|
||||
- **流式回答 (Streaming Generation)**: AIの思考過程と回答をリアルタイムで表示。
|
||||
- **引用表示**: 回答の根拠となったドキュメントのソースと該当箇所を提示。
|
||||
|
||||
### 3.5 ポッドキャスト生成 (Podcasts)
|
||||
|
||||
- **概要**: ナレッジグループ内の資料に基づき、AIホストとゲストによる音声対話を生成。
|
||||
- **グローバル生成**: 全てのナレッジ、または特定のグループを指定してポッドキャストを作成。
|
||||
- **機能**: トピック指定、スクリプト生成(トランスクリプト)、音声再生。
|
||||
|
||||
### 3.6 ビジョンモデルによる高度なドキュメント処理 (Advanced Visual Processing)
|
||||
|
||||
- **PPT/PDFの視覚的解析**: 従来のテキスト抽出では失われがちなPowerPointやPDFのレイアウト情報、図表、グラフを保持。
|
||||
- **Vision Pipeline**:
|
||||
- **ページ画像化**: ドキュメントの各ページを高解像度画像に変換。
|
||||
- **マルチモーダル解析**: GPT-4oやClaude 3.5 Sonnetなどのビジョン対応モデルを使用し、画像内のテキストと視覚要素を統合して理解・説明。
|
||||
- **構造化データ化**: 複雑なスライドや帳票も、人間が見たままの文脈でインデックス化。
|
||||
|
||||
### 3.7 インタラクティブなノート作成 (Screenshot & Notes)
|
||||
|
||||
- **領域選択とOCR**: PDFプレビュー画面で任意の領域をマウスで矩形選択。
|
||||
- **自動テキスト抽出**: 選択範囲の画像からOCR(光学文字認識)でテキストを即座に抽出。
|
||||
- **ノート保存**: 抽出したテキストとキャプチャ画像をセットで「ノート」として保存し、後から参照や引用が可能。
|
||||
|
||||
### 3.8 システム全体設定 (System Configuration)
|
||||
|
||||
- **一元管理ドロワー**: 画面右上の設定アイコンから、システム全体の動作を一括設定。
|
||||
- **柔軟なモデル切り替え**:
|
||||
- **LLM**: チャットや推論に使用するメインモデル。
|
||||
- **Embedding**: 検索精度を左右するベクトル化モデル。
|
||||
- **Vision**: 精密モードで使用する画像解析モデル。
|
||||
- **即時反映**: 設定変更はシステム全体に即座に適用され、再起動なしで異なるモデルの挙動をテスト可能。
|
||||
|
||||
### 3.9 モデル管理 (Model Management)
|
||||
|
||||
- **BYOK (Bring Your Own Key)**: ユーザー自身のAPIキーを設定可能。
|
||||
- **一元管理**: 「システム構成(System Settings)」ドロワーから、グローバルなモデル設定(LLM, Embedding, Rerank, Vision)を一括管理。
|
||||
- **マルチプロバイダー**: OpenAI, Google Gemini, Anthropic (Claude), Ollama などのモデル設定をサポート。
|
||||
- **カスタム設定**: Temperature, Top-K, Max Tokens などの推論パラメータを調整可能。
|
||||
|
||||
---
|
||||
|
||||
## 4. データフロー
|
||||
|
||||
### 4.1 ドキュメント取り込みフロー
|
||||
|
||||
ドキュメントがアップロードされてから検索可能になるまでの処理フローです。
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User
|
||||
participant BE as Backend
|
||||
participant TP as Tika/Vision
|
||||
participant EM as "Embedding Model"
|
||||
participant ES as Elasticsearch
|
||||
|
||||
U->>BE: ファイルアップロード
|
||||
BE->>BE: ファイルタイプ判別
|
||||
|
||||
rect rgb(240, 248, 255)
|
||||
alt 高速モード
|
||||
BE->>TP: テキスト抽出 (Tika)
|
||||
TP-->>BE: 抽出テキスト
|
||||
else 精密モード
|
||||
BE->>BE: PDF/画像変換
|
||||
BE->>TP: 画像解析 (Vision API)
|
||||
TP-->>BE: 構造化テキスト
|
||||
end
|
||||
end
|
||||
|
||||
BE->>BE: チャンキング (分割)
|
||||
loop 各チャンク
|
||||
BE->>EM: ベクトル化リクエスト
|
||||
EM-->>BE: ベクトルデータ
|
||||
end
|
||||
|
||||
BE->>ES: インデックス保存 (ベクトル + メタデータ)
|
||||
BE-->>U: 処理完了通知
|
||||
```
|
||||
|
||||
1. **アップロード**: ユーザーがファイルを送信。
|
||||
1. **前処理**: ファイルタイプに応じた変換(例: docx -> pdf)。
|
||||
1. **解析**:
|
||||
- (高速モード) Tikaでテキスト抽出。
|
||||
- (精密モード) PDFを画像化 -> Vision APIで解析。
|
||||
1. **チャンキング**: 設定されたルールでテキストを分割。
|
||||
1. **埋め込み**: Embedding APIでベクトル化。
|
||||
1. **保存**: ベクトルとメタデータをElasticsearchに保存。
|
||||
|
||||
### 4.2 RAG検索・生成フロー
|
||||
|
||||
ユーザーの質問から回答生成までのRAGプロセスフローです。
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Q[ユーザーの質問] --> Embed[質問のベクトル化]
|
||||
Embed --> Search[ベクトル検索 + キーワード検索]
|
||||
Search --> ES[(Elasticsearch)]
|
||||
ES --> Results[検索結果候補]
|
||||
|
||||
Results --> Rerank{"リランク有効?"}
|
||||
Rerank -- Yes --> RerankModel[Rerankモデル]
|
||||
RerankModel --> TopK[上位結果抽出]
|
||||
Rerank -- No --> TopK
|
||||
|
||||
TopK --> Prompt["プロンプト構築\n(質問 + コンテキスト)"]
|
||||
Prompt --> LLM[LLM生成]
|
||||
LLM --> Stream[流式回答出力]
|
||||
```
|
||||
|
||||
1. **クエリ受信**: ユーザーの質問を受け取る。
|
||||
1. **検索**: 質問をベクトル化し、Elasticsearchで類似チャンクを検索(+キーワード検索)。
|
||||
1. **リランク (Optional)**: 検索結果をRerankモデルで再評価し、関連度順に並べ替え。
|
||||
1. **プロンプト構築**: 上位のチャンクをコンテキストとしてシステムプロンプトに組み込む。
|
||||
1. **生成**: LLMにプロンプトを送信し、回答を生成。
|
||||
1. **レスポンス**: 回答と参照ソースをフロントエンドにストリーミング送信。
|
||||
Binary file not shown.
@@ -1,87 +0,0 @@
|
||||
# RAG 機能の完全実装ドキュメント
|
||||
|
||||
## 実装完了 ✅
|
||||
|
||||
### バックエンドの実装
|
||||
|
||||
- **RagService**: コアとなる RAG ロジック。ベクトル検索とプロンプト構築をサポート。
|
||||
- **RagModule**: モジュール化されたカプセル化。
|
||||
- **API エンドポイント**: `POST /api/knowledge-bases/rag-search`
|
||||
- **類似度フィルタリング**: 動的なしきい値設定。
|
||||
- **LangChain 統合**: プロンプトテンプレートの管理。
|
||||
|
||||
### フロントエンドの実装
|
||||
|
||||
- **設定パネル**: 類似度しきい値スライダー (0.1-1.0)
|
||||
- **RAG サービス**: API 呼び出しのカプセル化。
|
||||
- **チャット統合**: 自動 RAG 検索と拡張。
|
||||
- **検索ステータス**: 「ナレッジベースを検索中...」のヒント表示。
|
||||
- **結果表示**: SearchResultsPanel コンポーネント。
|
||||
|
||||
## コアフロー
|
||||
|
||||
### 1. ユーザーの質問
|
||||
|
||||
```
|
||||
ユーザーが質問を入力 → RAG 検索がトリガーされる
|
||||
```
|
||||
|
||||
### 2. RAG 検索
|
||||
|
||||
```
|
||||
質問のベクトル化 → ES ベクトル検索 → 類似度フィルタリング → 拡張プロンプトの構築
|
||||
```
|
||||
|
||||
### 3. LLM 生成
|
||||
|
||||
```
|
||||
拡張プロンプト → LLM 推論 → 出典が付与された回答
|
||||
```
|
||||
|
||||
### 4. 結果の表示
|
||||
|
||||
```
|
||||
回答の表示 + [ファイル名.pdf] + 検索されたセグメントの確認
|
||||
```
|
||||
|
||||
## 主要な特徴
|
||||
|
||||
### ✅ インテリジェント検索
|
||||
|
||||
- ユーザーが選択した Embedding モデルを使用。
|
||||
- 類似度しきい値によるフィルタリングをサポート。
|
||||
- ファイルごとにグループ化して結果を表示。
|
||||
|
||||
### ✅ 拡張生成
|
||||
|
||||
- RAG プロンプトを自動構築。
|
||||
- ドキュメントのコンテキストと出典情報を含める。
|
||||
- 多言語での回答をサポート。
|
||||
|
||||
### ✅ ユーザー体験
|
||||
|
||||
- 検索プロセスの可視化。
|
||||
- 具体的な検索セグメントの確認が可能。
|
||||
- 自動的な出典の付与。
|
||||
- 関連コンテンツがない場合の明確な通知。
|
||||
|
||||
### ✅ 柔軟な設定
|
||||
|
||||
- 動的な類似度しきい値。
|
||||
- topK 結果数の制御。
|
||||
- 再ランキングのサポート(有効な場合)。
|
||||
|
||||
## 利用方法
|
||||
|
||||
1. **ドキュメントのアップロード** → 自動的にベクトルインデックスを作成。
|
||||
2. **設定の調整** → 類似度しきい値、topK など。
|
||||
3. **質問** → 自動的に RAG 検索と拡張を実行。
|
||||
4. **結果の確認** → 出典付きのインテリジェントな回答。
|
||||
5. **セグメントの確認** → 検索アイコンをクリックして具体的な内容を表示。
|
||||
|
||||
## 技術スタック
|
||||
|
||||
- **バックエンド**: NestJS + LangChain + Elasticsearch
|
||||
- **フロントエンド**: React + TypeScript
|
||||
- **ベクトル化**: 多様な Embedding モデルをサポート
|
||||
- **検索**: コサイン類似度 + しきい値フィルタリング
|
||||
@@ -1,47 +0,0 @@
|
||||
# ドキュメント索引
|
||||
|
||||
## 📚 主要ドキュメント
|
||||
|
||||
### 🏗️ システムアーキテクチャ
|
||||
|
||||
- [システム設計ドキュメント](DESIGN.md) - アーキテクチャ設計と技術スタックの全容
|
||||
- [現在の実装状況](CURRENT_IMPLEMENTATION.md) - 実装済み機能のリスト
|
||||
- [API リファレンス](API.md) - API エンドポイントの詳細説明
|
||||
|
||||
### 🚀 デプロイと運用
|
||||
|
||||
- [デプロイガイド](DEPLOYMENT.md) - 開発および本番環境でのデプロイ手順
|
||||
- [サポートされているファイル形式](SUPPORTED_FILE_TYPES.md) - 対応しているファイル拡張子の一覧
|
||||
- [開発基準](DEVELOPMENT_STANDARDS.md) - コードコメントおよびログに関する基準
|
||||
|
||||
### 🔧 機能の実装
|
||||
|
||||
- [RAG 機能の実装](RAG_COMPLETE_IMPLEMENTATION.md) - 検索拡張生成機能の詳細
|
||||
- [Vision Pipeline の実装](VISION_PIPELINE_COMPLETE.md) - 画像・テキスト混合処理の詳細
|
||||
- [チャンクサイズの制限](CHUNK_SIZE_LIMITS.md) - ドキュメント分割パラメータの管理
|
||||
|
||||
### 🐛 修正履歴
|
||||
|
||||
- [Embedding モデル ID の修正](EMBEDDING_MODEL_ID_FIX.md) - モデル設定の ID 連携に関する修正
|
||||
- [類似度スコアの修正](SIMILARITY_SCORE_BUGFIX.md) - 検索スコアが 100% を超える問題の修正
|
||||
- [メモリ最適化の修正](MEMORY_OPTIMIZATION_FIX.md) - 大容量ファイルによるメモリ溢れの問題の修正
|
||||
|
||||
## 🔍 クイックリファレンス
|
||||
|
||||
### 問題が発生した場合
|
||||
|
||||
1. 関連する修正ドキュメントを確認してください (EMBEDDING_MODEL_ID_FIX.md など)。
|
||||
2. デプロイ設定を確認してください (DEPLOYMENT.md)。
|
||||
3. ファイル形式がサポートされているか確認してください (SUPPORTED_FILE_TYPES.md)。
|
||||
|
||||
### 新機能の開発時
|
||||
|
||||
1. システム設計を確認してください (DESIGN.md)。
|
||||
2. API 仕様を確認してください (API.md)。
|
||||
3. 開発基準に従ってください (DEVELOPMENT_STANDARDS.md)。
|
||||
|
||||
### システムのデプロイ時
|
||||
|
||||
1. デプロイガイドに従って操作してください (DEPLOYMENT.md)。
|
||||
2. 環境変数とパラメータを設定してください。
|
||||
3. 各機能が正常に動作することを確認してください。
|
||||
@@ -1,249 +0,0 @@
|
||||
# 相似度スコアが 100% を超えるバグの修正
|
||||
|
||||
## 🐛 問題の記述
|
||||
|
||||
ユーザーがチャットインターフェースにて、引用ソースの適合度スコアが 100% を超えている現象を確認しました。これは数学的に不可能です(相似度スコアは 0〜100% の間であるべきです)。
|
||||
|
||||
**発生していた現象:**
|
||||
|
||||
```
|
||||
引用元表示:適合度 123.5%
|
||||
適合度 165.2%
|
||||
適合度 201.8%
|
||||
```
|
||||
|
||||
## 🔍 根本原因の分析
|
||||
|
||||
### 問題の発生源
|
||||
|
||||
Elasticsearch が返す生のスコア(`_score`)は、特に以下の場合に 1.0 を超えることがあります:
|
||||
|
||||
1. **ベクトル検索 (Vector Search)**:コサイン類似度を使用しますが、戻り値が 1.0 を超える場合があります。
|
||||
2. **全文検索 (Full-text Search)**:TF-IDF スコアが非常に大きくなる場合があります。
|
||||
3. **ハイブリッド検索 (Hybrid Search)**:ウェイトを組み合わせた後の合計が 1.0 を超える場合があります。
|
||||
|
||||
### データフローの分析
|
||||
|
||||
```
|
||||
Elasticsearch がスコアを返却 (_score = 1.5)
|
||||
↓
|
||||
elasticsearch.service.ts: searchSimilar() / searchFullText()
|
||||
↓
|
||||
chat.service.ts: hybridSearch()
|
||||
↓
|
||||
ChatService: result.score を返却
|
||||
↓
|
||||
フロントエンド ChatInterface.tsx: (source.score * 100).toFixed(1)%
|
||||
↓
|
||||
表示:150% ❌
|
||||
```
|
||||
|
||||
### 問題のあったコード
|
||||
|
||||
**elasticsearch.service.ts - hybridSearch メソッド:**
|
||||
|
||||
```typescript
|
||||
// 問題:Elasticsearch の _score をそのまま使用しており、1.0 を超える可能性がある
|
||||
vectorResults.forEach((result) => {
|
||||
combinedResults.set(result.id, {
|
||||
...result,
|
||||
vectorScore: result.score, // 例えば 1.5 になる可能性がある
|
||||
textScore: 0,
|
||||
combinedScore: result.score * vectorWeight, // 1.5 * 0.7 = 1.05
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
**ChatInterface.tsx - 表示ロジック:**
|
||||
|
||||
```typescript
|
||||
// 問題:スコアが 0〜1 の間であることを前提に 100 倍している
|
||||
{(source.score * 100).toFixed(1)}% // 1.05 * 100 = 105%
|
||||
```
|
||||
|
||||
## ✅ 解決策
|
||||
|
||||
### 1. ElasticsearchService にスコアの正規化を追加
|
||||
|
||||
**新規メソッド `normalizeScore` の追加:**
|
||||
|
||||
```typescript
|
||||
private normalizeScore(rawScore: number): number {
|
||||
if (!rawScore || rawScore <= 0) return 0.5;
|
||||
|
||||
// 広範囲のスコアを処理するため、対数正規化を使用
|
||||
const logScore = Math.log10(rawScore + 1);
|
||||
|
||||
// 0.5〜1.0 の範囲にマッピング
|
||||
const normalized = 0.5 + (logScore * 0.25);
|
||||
|
||||
// 最終的に 0.5〜1.0 の間に制限
|
||||
return Math.max(0.5, Math.min(1.0, normalized));
|
||||
}
|
||||
```
|
||||
|
||||
**なぜ対数正規化を使用するのか?**
|
||||
|
||||
- Elasticsearch のスコア範囲:1〜100 以上
|
||||
- log10(1) = 0 → 0.5
|
||||
- log10(10) = 1 → 0.75
|
||||
- log10(100) = 2 → 1.0
|
||||
- 結果が常に 0.5〜1.0 の間に収まるようになります。
|
||||
|
||||
### 2. すべての検索メソッドで正規化を適用
|
||||
|
||||
**searchSimilar メソッド:**
|
||||
|
||||
```typescript
|
||||
const results = response.hits.hits.map((hit: any) => ({
|
||||
id: hit._id,
|
||||
score: this.normalizeScore(hit._score), // ✅ 正規化を適用
|
||||
// ...
|
||||
}));
|
||||
```
|
||||
|
||||
**searchFullText メソッド:**
|
||||
|
||||
```typescript
|
||||
const results = response.hits.hits.map((hit: any) => ({
|
||||
id: hit._id,
|
||||
score: this.normalizeScore(hit._score), // ✅ 正規化を適用
|
||||
// ...
|
||||
}));
|
||||
```
|
||||
|
||||
**hybridSearch メソッド:**
|
||||
|
||||
```typescript
|
||||
// 結合された全スコアを取得して最大・最小を確認
|
||||
const allScores = Array.from(combinedResults.values()).map(r => r.combinedScore);
|
||||
const maxScore = Math.max(...allScores, 1);
|
||||
const minScore = Math.min(...allScores);
|
||||
|
||||
// 総合スコアでソートして上位 topK を取得し、0〜1 の範囲に正規化
|
||||
return Array.from(combinedResults.values())
|
||||
.sort((a, b) => b.combinedScore - a.combinedScore)
|
||||
.slice(0, topK)
|
||||
.map((result) => {
|
||||
// Min-Max 正規化
|
||||
let normalizedScore = (result.combinedScore - minScore) / (maxScore - minScore);
|
||||
|
||||
// 0.5〜1.0 の範囲にマッピング
|
||||
normalizedScore = 0.5 + (normalizedScore * 0.5);
|
||||
|
||||
// 0.5〜1.0 の間に制限
|
||||
normalizedScore = Math.max(0.5, Math.min(1.0, normalizedScore));
|
||||
|
||||
return {
|
||||
...result,
|
||||
score: normalizedScore,
|
||||
};
|
||||
});
|
||||
```
|
||||
|
||||
### 3. フロントエンドの表示ロジック(変更なし)
|
||||
|
||||
バックエンド側でスコアが 0〜1 の間に収まることを保証したため、フロントエンドの修正は不要です:
|
||||
|
||||
```typescript
|
||||
{(source.score * 100).toFixed(1)}% // 常に 50.0% 〜 100.0% が表示される
|
||||
```
|
||||
|
||||
## 📊 修正後の効果
|
||||
|
||||
### 修正前
|
||||
|
||||
| 元のスコア | 表示結果 | 問題点 |
|
||||
|---------|---------|------|
|
||||
| 1.5 | 150% | ❌ 100% を超える |
|
||||
| 2.0 | 200% | ❌ 100% を超える |
|
||||
| 0.8 | 80% | ✅ 正常 |
|
||||
|
||||
### 修正後
|
||||
|
||||
| 元のスコア | 正規化後 | 表示結果 | ステータス |
|
||||
|---------|---------|---------|------|
|
||||
| 1.5 | 0.875 | 87.5% | ✅ |
|
||||
| 2.0 | 0.938 | 93.8% | ✅ |
|
||||
| 0.8 | 0.750 | 75.0% | ✅ |
|
||||
|
||||
## 🧪 テスト・検証
|
||||
|
||||
### テスト手順
|
||||
|
||||
1. **テストドキュメントのアップロード**
|
||||
|
||||
```bash
|
||||
# 異なる内容を含むテストドキュメントを作成
|
||||
echo "人工知能 機械学習 深層学習" > test1.txt
|
||||
echo "Python JavaScript TypeScript" > test2.txt
|
||||
```
|
||||
|
||||
2. **検索クエリの実行**
|
||||
- クエリ:「人工知能」
|
||||
- 期待値:関連ドキュメントが表示され、スコアが 50〜100% の間であること。
|
||||
|
||||
3. **スコア範囲の検証**
|
||||
|
||||
```typescript
|
||||
// ブラウザのコンソールでチェック
|
||||
console.log('すべてのスコアが 50〜100 の間であるべきです');
|
||||
sources.forEach(s => {
|
||||
if (s.score * 100 > 100) console.error('スコアが 100% を超えています:', s);
|
||||
});
|
||||
```
|
||||
|
||||
### 期待される結果
|
||||
|
||||
- ✅ すべての相似度スコアが 50.0% 〜 100.0% の間にある。
|
||||
- ✅ 関連性の高いドキュメントは 100% に近い値を示す。
|
||||
- ✅ 関連性の低いドキュメントは 50% に近い値を示す。
|
||||
- ✅ 100% を超えるスコアは表示されない。
|
||||
|
||||
## 📝 修正ファイル
|
||||
|
||||
### バックエンド
|
||||
|
||||
- `server/src/elasticsearch/elasticsearch.service.ts`
|
||||
- プライベートメソッド `normalizeScore()` を追加
|
||||
- `searchSimilar()` にて正規化を適用
|
||||
- `searchFullText()` にて正規化を適用
|
||||
- `hybridSearch()` にて正規化を適用
|
||||
|
||||
### フロントエンド
|
||||
|
||||
- 修正なし(バックエンドでスコア範囲を保証)
|
||||
|
||||
## ⚠️ 注意事項
|
||||
|
||||
### 1. スコアの意味の変化
|
||||
|
||||
修正後、スコアは Elasticsearch の生の相似度を直接示すのではなく、以下の目安となります:
|
||||
|
||||
- **50-60%**:低い関連性
|
||||
- **60-75%**:中程度の関連性
|
||||
- **75-90%**:高い関連性
|
||||
- **90-100%**:非常に高い関連性
|
||||
|
||||
### 2. しきい値の調整
|
||||
|
||||
以前に相似度フィルタリング(例:`similarityThreshold: 0.7`)を使用していた場合、調整が必要になる可能性があります:
|
||||
|
||||
```typescript
|
||||
// 旧設定(生のスコアベース)
|
||||
similarityThreshold: 0.7
|
||||
|
||||
// 新設定(正規化スコアベース)
|
||||
similarityThreshold: 0.6 // 以前の 0.7 に相当する目安
|
||||
```
|
||||
|
||||
### 3. パフォーマンスへの影響
|
||||
|
||||
- 正規化の計算は非常に軽量です (O(1))。
|
||||
- 検索パフォーマンスへの影響はありません。
|
||||
|
||||
## 📚 参考文献
|
||||
|
||||
- [Elasticsearch Similarity Scoring](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-script-score-query.html)
|
||||
- [Vector Search Cosine Similarity](https://www.elastic.co/guide/en/elasticsearch/reference/current/dense-vector.html)
|
||||
- [Min-Max Normalization](https://en.wikipedia.org/wiki/Feature_scaling#Rescaling_(min-max_normalization))
|
||||
@@ -1,158 +0,0 @@
|
||||
# サポートされているファイル形式
|
||||
|
||||
本システムは Apache Tika を使用してドキュメントを解析しており、数百種類のファイル形式をサポートしています。
|
||||
|
||||
## 📋 サポートファイル形式一覧
|
||||
|
||||
### 📄 PDF ドキュメント
|
||||
|
||||
- `application/pdf` - PDF ドキュメント
|
||||
|
||||
### 📝 Microsoft Office ドキュメント
|
||||
|
||||
- `application/msword` - Word ドキュメント (.doc)
|
||||
- `application/vnd.openxmlformats-officedocument.wordprocessingml.document` - Word ドキュメント (.docx)
|
||||
- `application/vnd.ms-excel` - Excel スプレッドシート (.xls)
|
||||
- `application/vnd.openxmlformats-officedocument.spreadsheetml.sheet` - Excel スプレッドシート (.xlsx)
|
||||
- `application/vnd.ms-powerpoint` - PowerPoint プレゼンテーション (.ppt)
|
||||
- `application/vnd.openxmlformats-officedocument.presentationml.presentation` - PowerPoint プレゼンテーション (.pptx)
|
||||
|
||||
### 📊 OpenOffice / LibreOffice ドキュメント
|
||||
|
||||
- `application/vnd.oasis.opendocument.text` - テキストドキュメント (.odt)
|
||||
- `application/vnd.oasis.opendocument.spreadsheet` - スプレッドシート (.ods)
|
||||
- `application/vnd.oasis.opendocument.presentation` - プレゼンテーション (.odp)
|
||||
- `application/vnd.oasis.opendocument.graphics` - グラフィックドキュメント (.odg)
|
||||
|
||||
### 📝 テキストファイル
|
||||
|
||||
- `text/plain` - プレーンテキスト (.txt)
|
||||
- `text/markdown` - Markdown (.md, .markdown)
|
||||
- `text/html` - HTML ドキュメント (.html, .htm)
|
||||
- `text/csv` - CSV 表形式 (.csv)
|
||||
- `text/xml` - XML ドキュメント (.xml)
|
||||
- `application/xml` - XML ドキュメント
|
||||
- `application/json` - JSON データ (.json)
|
||||
|
||||
### 💻 コードファイル
|
||||
|
||||
- `text/x-python` - Python コード (.py)
|
||||
- `text/x-java` - Java コード (.java)
|
||||
- `text/x-c` - C コード (.c)
|
||||
- `text/x-c++` - C++ コード (.cpp, .cc, .cxx)
|
||||
- `text/javascript` - JavaScript コード (.js)
|
||||
- `text/typescript` - TypeScript コード (.ts)
|
||||
|
||||
### 🖼️ 画像ファイル
|
||||
|
||||
- `image/jpeg` - JPEG 画像 (.jpg, .jpeg)
|
||||
- `image/png` - PNG 画像 (.png)
|
||||
- `image/gif` - GIF 画像 (.gif)
|
||||
- `image/webp` - WebP 画像 (.webp)
|
||||
- `image/tiff` - TIFF 画像 (.tiff, .tif)
|
||||
- `image/bmp` - BMP 画像 (.bmp)
|
||||
- `image/svg+xml` - SVG ベクター画像 (.svg)
|
||||
|
||||
### 📦 圧縮ファイル
|
||||
|
||||
- `application/zip` - ZIP 圧縮アーカイブ (.zip)
|
||||
- `application/x-tar` - TAR アーカイブ (.tar)
|
||||
- `application/gzip` - GZIP 圧縮 (.gz)
|
||||
- `application/x-7z-compressed` - 7z 圧縮アーカイブ (.7z)
|
||||
|
||||
### 📚 その他のドキュメント形式
|
||||
|
||||
- `application/rtf` - RTF ドキュメント (.rtf)
|
||||
- `application/epub+zip` - EPUB 電子書籍 (.epub)
|
||||
- `application/x-mobipocket-ebook` - MOBI 電子書籍 (.mobi)
|
||||
|
||||
## 🔧 自動サポートルール
|
||||
|
||||
明示的なリスト以外にも、システムは以下のパターンを自動的にサポートします:
|
||||
|
||||
1. **すべてのテキストタイプ** - `text/` で始まるすべての MIME タイプ
|
||||
2. **Office ドキュメント** - `application/vnd.` で始まるすべてのタイプ
|
||||
3. **その他の形式** - `application/x-` で始まるすべてのタイプ
|
||||
|
||||
これは、特定の形式がリストになくても、Tika が解析可能であればシステムで処理できることを意味します。
|
||||
|
||||
## ⚠️ 注意事項
|
||||
|
||||
### 画像処理
|
||||
|
||||
- 画像ファイルから意味のある内容を抽出するには、**ビジョンモデル**の設定が必要です。
|
||||
- ビジョンモデルが設定されていない場合、システムはファイル名をコンテンツとして使用します。
|
||||
- 「システム設定」でビジョンをサポートする LLM(GPT-4V、Gemini など)を設定することをお勧めします。
|
||||
|
||||
### 大容量ファイルの処理
|
||||
|
||||
- ファイルサイズ制限:デフォルト 100MB(`.env` の `MAX_FILE_SIZE` で設定可能)
|
||||
- 大容量ファイルはバッチ処理され、メモリオーバーフローを防止します。
|
||||
- 推奨:最適なパフォーマンスを得るために、1ファイルあたり 50MB 以下にすることをお勧めします。
|
||||
|
||||
### エンコーディングの問題
|
||||
|
||||
- システムはファイルのエンコーディングを自動検出します。
|
||||
- UTF-8 エンコーディングのテキストファイルを推奨します。
|
||||
- UTF-8 以外のエンコーディングでは文字化けが発生する可能性があります。
|
||||
|
||||
## 📝 設定例
|
||||
|
||||
### 環境変数の設定
|
||||
|
||||
```env
|
||||
# ファイルアップロードの制限
|
||||
MAX_FILE_SIZE=104857600 # 100MB
|
||||
|
||||
# チャンク設定(Embeddingモデルに合わせて調整)
|
||||
MAX_CHUNK_SIZE=8191 # OpenAI embedding-3-large
|
||||
MAX_OVERLAP_SIZE=200
|
||||
```
|
||||
|
||||
### モデルの設定
|
||||
|
||||
フロントエンドの「システム設定」→「モデル管理」で Embedding モデルを設定する際:
|
||||
|
||||
- **最大入力 (Tokens)**: モデルの設定に従う(OpenAI=8191, Gemini=2048)
|
||||
- **ベクトル次元数**: モデルの出力設定に従う(text-embedding-3-large=2560, text-embedding-3-small=1536)
|
||||
- **バッチ処理制限**: モデルの設定に従う(OpenAI=2048, Gemini=100)
|
||||
|
||||
## 🔍 トラブルシューティング
|
||||
|
||||
### ファイル形式がサポートされていない
|
||||
|
||||
**エラー**: `不支持的文件类型: application/xxx` (サポートされていないファイル形式)
|
||||
|
||||
**解決策**:
|
||||
|
||||
1. ファイル形式がサポートリストに含まれているか確認してください。
|
||||
2. ファイルの拡張子が正しいか確認してください。
|
||||
3. テキストエディタで開き、内容が読み取れるか確認してください。
|
||||
4. 新しい形式のサポートが必要な場合は、Issue を送信してください。
|
||||
|
||||
### 解析に失敗する
|
||||
|
||||
**エラー**: `无法提取文本内容` (テキスト内容を抽出できません)
|
||||
|
||||
**解決策**:
|
||||
|
||||
1. Apache Tika サービスが動作しているか確認してください。
|
||||
2. Tika のログを確認してください:`docker-compose logs tika`
|
||||
3. 他のツールでファイルを開き、ファイルが破損していないか確認してください。
|
||||
4. ファイルの権限を確認してください。
|
||||
|
||||
### エンコーディングの問題
|
||||
|
||||
**現象**: テキストが文字化けする
|
||||
|
||||
**解決策**:
|
||||
|
||||
1. ファイルを UTF-8 エンコーディングに変換してください。
|
||||
2. テキストエディタで再度保存してください。
|
||||
3. システムの言語設定を確認してください。
|
||||
|
||||
## 📚 参考文献
|
||||
|
||||
- [Apache Tika 公式ドキュメント](https://tika.apache.org/1.24/formats.html)
|
||||
- [Tika サポート形式一覧](https://tika.apache.org/1.24/formats.html)
|
||||
- [MIME タイプ標準](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
|
||||
@@ -1,55 +0,0 @@
|
||||
# Elasticsearch vs Chroma 比較分析
|
||||
|
||||
Elasticsearch と Chroma は、現在人気のあるベクトルストレージソリューションですが、その設計思想と適用シナリオには大きな違いがあります。
|
||||
|
||||
**simple-kb** のようなナレッジベースプロジェクトにおいて、Elasticsearch (ES) を選択した主な理由は、その強力な **ハイブリッド検索 (Hybrid Search)** 能力を活用するためです。
|
||||
|
||||
以下に、両者の詳細な長所と短所の比較を示します。
|
||||
|
||||
## コア機能の比較まとめ
|
||||
|
||||
| 機能 | Elasticsearch (ES) | Chroma |
|
||||
| :--- | :--- | :--- |
|
||||
| **位置付け** | 汎用検索エンジン(全文検索 + ベクトル検索) | AI ネイティブ ベクトルデータベース |
|
||||
| **コアな強み** | **ハイブリッド検索** (BM25 + kNN)、強力なメタデータフィルタリング | **軽量で使いやすい**、Python 和性が高い、LLM 専用設計 |
|
||||
| **全文検索** | 👑 **業界標準** (BM25)、形態素解析、曖昧検索などをサポート | 弱い (主にベクトルの類似度に依存、テキスト検索は限定的) |
|
||||
| **リソース消費** | 🔴 **高** (Java ヒープメモリ、起動に通常 1GB+ メモリが必要) | 🟢 **極めて低い** (軽量プロセス、インメモリ実行も可能) |
|
||||
| **デプロイ・保守** | 🔴 複雑 (Java 環境、設定項目が多い) | 🟢 簡単 (`pip install` または軽量 Docker) |
|
||||
| **拡張性** | 分散クラスタが成熟しており、PB 級のデータをサポート | シングルノードは強力だが、分散クラスタ機能は比較的新しい |
|
||||
| **エコシステム** | 非常に豊富 (Kibana 可視化, Logstash など) | AI / LangChain エコシステムに特化 |
|
||||
|
||||
---
|
||||
|
||||
## 1. Elasticsearch の長所と短所 (なぜ simple-kb で採用したのか?)
|
||||
|
||||
**長所:**
|
||||
|
||||
* **ハイブリッド検索 (Hybrid Search) - 決定的な機能**: RAG システムにおける最大の課題は「専門用語が検索できない」ことです。
|
||||
* **ベクトル検索**は、意味の理解に優れています(例:「スマホ」で「iPhone」を検索可能)。
|
||||
* **キーワード検索 (ES)** は、正確な一致に優れています(例:エラーコード「Error 503」や特定の型番「RTX 4090」)。
|
||||
* ES はこれらを同時に実行し、スコアを加重して統合できます。これが現在の RAG システムの精度向上の鍵となります。
|
||||
* **強力なメタデータフィルタリング**: ベクトル検索の前後に、ユーザー権限、ファイルタイプ、時間範囲などのフィールドに基づいて、非常に効率的にデータをフィルタリングできます。
|
||||
* **成熟と安定**: ビッグデータ分野で10年以上の実績があります。
|
||||
|
||||
**短所:**
|
||||
|
||||
* **重い**: JVM ベースであり、メモリを消費します。個人開発者の小型 VPS で ES コンテナを実行するのは少し厳しい場合があります。
|
||||
* **学習コストが高い**: DSL クエリ構文が複雑で、設定が煩雑です。
|
||||
|
||||
## 2. Chroma の長所と短所
|
||||
|
||||
**長所:**
|
||||
|
||||
* **開発者体験 (DX) が最高**: 「AI Native」です。API 設計が Python 開発者の直感に非常に合っており、ES のような複雑な JSON クエリを書く必要がありません。
|
||||
* **軽量**: プロトタイプの迅速な開発 (PoC)、ローカルで動作する Agent、または中小規模のアプリケーションに最適です。
|
||||
* **Embedding 内蔵**: Chroma はシンプルな Embedding モデルを簡単に内蔵でき、すぐに使用可能です。
|
||||
|
||||
**短所:**
|
||||
|
||||
* **キーワード検索能力が弱い**: ユーザーが Embedding モデルにとって未知の非常に具体的な単語(例:社内のプロジェクトコード名)を検索する場合、純粋なベクトル類似度では検索が難しく、ES のような転置インデックスによる検索が必要です。
|
||||
* **機能が単一**: 基本的にベクトルストレージ専用です。システムがログ保存や通常の検索も必要とする場合、別途データベースを用意する必要があります。
|
||||
|
||||
## 結論:simple-kb における選択
|
||||
|
||||
* **現在のアーキテクチャ (ES)**: **本番環境レベルの正確性**を選択しました。デプロイは少し手間ですが(Docker が必要)、システムが「意味的な曖昧さ」や「キーワードの正確な検索」に直面した際に、優れたパフォーマンスを発揮することを保証します。
|
||||
* **もし Chroma に変更した場合**: システムのデプロイは非常に簡単になりますが(Docker コンテナさえ不要で、Python プロセスに組み込み可能)、特定の専門用語を扱う際に BM25 キーワード検索の補助がないため、**再現率(Recall)**が低下する可能性があります。
|
||||
Binary file not shown.
@@ -1,265 +0,0 @@
|
||||
# Vision Pipeline 完全実装
|
||||
|
||||
## 🎯 概要
|
||||
|
||||
Vision Pipeline は、画像とテキストが混在したドキュメントを処理するためのシステムの「高精度モード」機能です。LibreOffice による変換、ImageMagick による画像処理、および Vision モデルによる分析を通じて、完全なドキュメント内容の抽出を実現します。
|
||||
|
||||
### デュアルモードの比較
|
||||
|
||||
| 特徴 | 高速モード | 高精度モード |
|
||||
|------|---------|---------|
|
||||
| 処理ツール | Apache Tika | Vision Pipeline |
|
||||
| 画像処理 | ❌ スキップ | ✅ 完全な分析 |
|
||||
| 処理速度 | 高速 | 低速 |
|
||||
| コスト | 無料 | 約 $0.01/ページ |
|
||||
| 適用シーン | テキストのみのドキュメント | 画像・テキスト混在ドキュメント |
|
||||
|
||||
## 🏗️ 技術アーキテクチャ
|
||||
|
||||
### コアフロー
|
||||
|
||||
```
|
||||
ドキュメントのアップロード → LibreOffice 変換 → PDF を画像化 → Vision 分析 → ベクトルインデックス
|
||||
```
|
||||
|
||||
### サービスコンポーネント
|
||||
|
||||
#### 1. LibreOffice サービス (FastAPI)
|
||||
|
||||
- **ポート**: 8100
|
||||
- **機能**: ドキュメント形式の統一化 (Word/PPT/Excel → PDF)
|
||||
- **API ドキュメント**: <http://localhost:8100/docs>
|
||||
|
||||
```python
|
||||
# libreoffice-server/main.py
|
||||
from fastapi import FastAPI, File, UploadFile
|
||||
from pydantic import BaseModel
|
||||
|
||||
app = FastAPI(title="ドキュメント変換サービス")
|
||||
|
||||
@app.post("/convert")
|
||||
async def convert(file: UploadFile = File(...)):
|
||||
# 変換ロジック
|
||||
return {"pdf_path": "...", "converted": True}
|
||||
|
||||
@app.get("/health")
|
||||
async def health():
|
||||
return {"status": "healthy"}
|
||||
```
|
||||
|
||||
#### 2. PDF2Image サービス (Node.js)
|
||||
|
||||
```typescript
|
||||
// server/src/pdf2image/pdf2image.service.ts
|
||||
@Injectable()
|
||||
export class Pdf2ImageService {
|
||||
async convertToImages(pdfPath: string): Promise<string[]> {
|
||||
// ImageMagick を使用して変換
|
||||
const images = await this.imagemagick.convert(pdfPath, {
|
||||
density: 300,
|
||||
format: 'jpeg',
|
||||
quality: 85
|
||||
});
|
||||
return images;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 3. Vision サービス
|
||||
|
||||
```typescript
|
||||
// server/src/vision/vision.service.ts
|
||||
@Injectable()
|
||||
export class VisionService {
|
||||
async analyzeImage(imagePath: string, modelConfig: ModelConfig): Promise<VisionResult> {
|
||||
// OpenAI/Gemini Vision API を呼び出し
|
||||
const result = await this.callVisionAPI(imagePath, modelConfig);
|
||||
return {
|
||||
text: result.text,
|
||||
confidence: result.confidence,
|
||||
layout: result.layout
|
||||
};
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🚀 デプロイ設定
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```yaml
|
||||
services:
|
||||
libreoffice:
|
||||
build:
|
||||
context: ./libreoffice-server
|
||||
ports:
|
||||
- "8100:8100"
|
||||
volumes:
|
||||
- ./uploads:/uploads
|
||||
- ./temp:/temp
|
||||
|
||||
server:
|
||||
environment:
|
||||
- LIBREOFFICE_URL=http://libreoffice:8100
|
||||
- TEMP_DIR=/app/temp
|
||||
depends_on:
|
||||
- libreoffice
|
||||
```
|
||||
|
||||
### 環境変数
|
||||
|
||||
```env
|
||||
# LibreOffice サービス
|
||||
LIBREOFFICE_URL=http://127.0.0.1:8100
|
||||
|
||||
# 一時ファイルディレクトリ
|
||||
TEMP_DIR=./temp
|
||||
|
||||
# Vision API 設定
|
||||
VISION_API_KEY=sk-xxx
|
||||
VISION_MODEL=gpt-4-vision-preview
|
||||
```
|
||||
|
||||
## 💰 コスト管理
|
||||
|
||||
### 予想コスト
|
||||
|
||||
| ドキュメント形式 | ページ数 | 予想コスト | 処理時間 |
|
||||
|---------|------|---------|---------|
|
||||
| PDF | 10ページ | $0.10 | 約 1分 |
|
||||
| Word | 50ページ | $0.50 | 約 5分 |
|
||||
| PPT | 30ページ | $0.30 | 約 3分 |
|
||||
|
||||
### 節約戦略
|
||||
|
||||
- 小規模ドキュメント (<10ページ): 高精度モードを使用。
|
||||
- 大規模ドキュメント (>50ページ): 分割して処理するか、高速モードを検討。
|
||||
- テキストのみのドキュメント: 常に高速モードを使用。
|
||||
|
||||
## 🔧 利用方法
|
||||
|
||||
### 1. サービスの起動
|
||||
|
||||
```bash
|
||||
# すべてのサービスを起動
|
||||
docker-compose up -d
|
||||
|
||||
# 状態の確認
|
||||
docker-compose ps
|
||||
```
|
||||
|
||||
### 2. サービスの検証
|
||||
|
||||
```bash
|
||||
# LibreOffice のヘルスチェック
|
||||
curl http://localhost:8100/health
|
||||
|
||||
# API ドキュメントの確認
|
||||
open http://localhost:8100/docs
|
||||
|
||||
# 変換テスト
|
||||
curl -X POST -F "file=@test.docx" http://localhost:8100/convert
|
||||
```
|
||||
|
||||
### 3. Vision モデルの設定
|
||||
|
||||
1. 「モデル管理」に移動します。
|
||||
2. Vision をサポートするモデル (GPT-4V/Gemini Pro Vision) を追加します。
|
||||
3. API キーを設定します。
|
||||
4. 「ビジョンをサポート」オプションにチェックを入れます。
|
||||
|
||||
### 4. アップロードテスト
|
||||
|
||||
1. PDF/Word/PPT ファイルを選択します。
|
||||
2. アップロード画面で「高精度モード」を選択します。
|
||||
3. 処理の進捗とコストの見積もりを確認します。
|
||||
|
||||
## 🔍 トラブルシューティング
|
||||
|
||||
### LibreOffice サービスの問題
|
||||
|
||||
```bash
|
||||
# コンテナ状態の確認
|
||||
docker-compose ps libreoffice
|
||||
|
||||
# ログを表示
|
||||
docker-compose logs libreoffice
|
||||
|
||||
# サービスの再起動
|
||||
docker-compose restart libreoffice
|
||||
```
|
||||
|
||||
### Vision 分析の失敗
|
||||
|
||||
- API キーの設定を検証してください。
|
||||
- モデルが Vision をサポートしているか確認してください。
|
||||
- ネットワーク接続が正常か確認してください。
|
||||
- 詳細なエラーログを確認してください。
|
||||
|
||||
### メモリ使用率が高すぎる場合
|
||||
|
||||
- バッチ処理サイズを調整してください。
|
||||
- 同時処理数を制限してください。
|
||||
- メモリの使用状況を監視してください。
|
||||
|
||||
## 📊 監視指標
|
||||
|
||||
### 主要な指標
|
||||
|
||||
- 変換成功率: >95%
|
||||
- 平均処理時間: <10分 / 100ページ
|
||||
- Vision 分析の精度: >85%
|
||||
- コスト管理: <$0.30 / ドキュメント
|
||||
|
||||
### ログの確認
|
||||
|
||||
```bash
|
||||
# リアルタイムログ
|
||||
docker-compose logs -f server | grep "Vision\|高精度モード"
|
||||
|
||||
# LibreOffice ログ
|
||||
docker-compose logs -f libreoffice
|
||||
```
|
||||
|
||||
## ⚡ クイックコマンド
|
||||
|
||||
```bash
|
||||
# 一括起動
|
||||
docker-compose up -d
|
||||
|
||||
# ヘルスチェック
|
||||
curl http://localhost:8100/health
|
||||
|
||||
# API ドキュメントを表示
|
||||
open http://localhost:8100/docs
|
||||
|
||||
# 変換テスト
|
||||
curl -X POST -F "file=@test.docx" http://localhost:8100/convert | jq
|
||||
|
||||
# ログを表示
|
||||
docker-compose logs -f libreoffice server
|
||||
```
|
||||
|
||||
## 🎯 技術選型の説明
|
||||
|
||||
### なぜ FastAPI を選んだのか
|
||||
|
||||
| 特徴 | Flask | FastAPI | 優位点 |
|
||||
|------|-------|---------|------|
|
||||
| パフォーマンス | 中程度 | ⭐⭐⭐⭐⭐ 非同期 | 2〜3倍高速 |
|
||||
| ドキュメント | 拡張が必要 | ⭐⭐⭐⭐⭐ 自動生成 | `/docs` で即座にアクセス可能 |
|
||||
| 型安全性 | オプション | ⭐⭐⭐⭐⭐ 強制的 | エラーの削減 |
|
||||
| 本番対応 | 設定が必要 | ⭐⭐⭐⭐⭐ 即利用可能 | 最小限の設定で運用可能 |
|
||||
|
||||
### FastAPI の核となるメリット
|
||||
|
||||
1. **自動ドキュメント**: <http://localhost:8100/docs> にて利用可能。
|
||||
2. **型安全性**: リクエストパラメータを自動的に検証。
|
||||
3. **非同期処理**: 複数のリクエストを同時に処理可能。
|
||||
4. **本番対応**: パフォーマンスの最適化が組み込まれている。
|
||||
|
||||
---
|
||||
|
||||
**更新日**: 2025-01-14
|
||||
**バージョン**: v2.0
|
||||
**ステータス**: 実装済み
|
||||
@@ -1,32 +0,0 @@
|
||||
# Admin Feature Verification Test Cases
|
||||
|
||||
## 1. User Management Access Control
|
||||
- [ ] Non-admin users should NOT see the "User Management" menu item
|
||||
- [ ] Admin users should see the "User Management" menu item
|
||||
- [ ] Non-admin users attempting to access user management should get a permission error
|
||||
- [ ] Admin users should be able to access user management successfully
|
||||
|
||||
## 2. Admin User Password Modification
|
||||
- [ ] Admin users should see a "Change Password" button for each user in the user list
|
||||
- [ ] Clicking the button should open a password change modal
|
||||
- [ ] Admin users should be able to submit new passwords for other users
|
||||
- [ ] The password change should persist in the backend
|
||||
- [ ] Non-admin users should not have access to this functionality
|
||||
|
||||
## 3. Knowledge Base Upload Restrictions
|
||||
- [ ] Non-admin users should NOT see the "Upload File" button in Knowledge Base View
|
||||
- [ ] Admin users should see the "Upload File" button in Knowledge Base View
|
||||
- [ ] Non-admin users attempting to upload directly via API should get a permission error
|
||||
- [ ] Admin users should be able to upload files successfully
|
||||
|
||||
## 4. Knowledge Group Upload Restrictions
|
||||
- [ ] Non-admin users should NOT see the "Add File" or "Import Folder" buttons in Knowledge Group View
|
||||
- [ ] Admin users should see the "Add File" and "Import Folder" buttons in Knowledge Group View
|
||||
- [ ] Non-admin users attempting to upload via API should get a permission error
|
||||
- [ ] Admin users should be able to upload files to knowledge groups successfully
|
||||
|
||||
## 5. Backend Security
|
||||
- [ ] Upload endpoints (POST /upload and POST /upload/text) should require AdminGuard
|
||||
- [ ] Import task endpoint (POST /import-tasks) should require AdminGuard
|
||||
- [ ] User update endpoint (PUT /users/:id) should accept password changes from admins
|
||||
- [ ] All existing functionality should remain operational for authorized users
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 426 KiB |
@@ -1,117 +0,0 @@
|
||||
# Implementation Plan - AuraK External API Service (v2.0)
|
||||
|
||||
Provide an API service for external systems to access the KnowledgeBase functionalities, including chat, search, and document management.
|
||||
|
||||
## User Review Required
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Multi-Tenancy & Resource Sharing**:
|
||||
> - **Tenant Entity**: We will introduce a `Tenant` (Organization) entity.
|
||||
> - **Resource Scoping**: `User`, `KnowledgeBase`, `KnowledgeGroup`, `SearchHistory`, `Note`, and `ImportTask` will be scoped by `tenantId`.
|
||||
> - **Configuration Hierarchy**:
|
||||
> - **ModelConfig**: Inherited hierarchy: `System Models (Global)` -> `Tenant Models (Shared in Org)` -> `User Models (Private)`.
|
||||
> - **TenantSettings**: New entity to define organization-wide defaults (Language, Default Models, Search thresholds). `UserSetting` can still override these for personalization.
|
||||
> - **Data Migration**: Existing data will be migrated to a "Default Tenant" during the first run.
|
||||
> - **Elasticsearch Isolation**: The `tenantId` field will be added to the ES mapping and enforced in all search/delete queries.
|
||||
> - **Storage Partitioning**: Uploaded files will be stored in `uploads/{tenantId}/{fileId}` to isolate files at the filesystem level.
|
||||
> - **API Key**: Tied to `User`, and all operations will be automatically scoped to the user's and tenant's data range.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **RBAC & Interface Separation**:
|
||||
> - **Roles**:
|
||||
> - `SUPER_ADMIN`: Manage Tenants and global system settings.
|
||||
> - `TENANT_ADMIN`: Manage Users and Knowledge Bases within their Tenant.
|
||||
> - `USER`: Access Chat, Search, and Knowledge Base within their Tenant.
|
||||
> - **API Separation**: Administrative endpoints will be separated into `/api/v1/super-admin/*` and `/api/v1/admin/*`.
|
||||
> - **UI Separation**: Recommend separating the "Admin Portal" from the "User Workspace" to ensure a cleaner user experience and better security boundaries.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **Frontend Modernization & Boundary Separation (Google Workspace Style)**:
|
||||
> - **Design Aesthetic**: Adopt a clean, modern, and professional style inspired by Google Workspace (Gmail, Drive, Gemini), following Material Design 3 specifications.
|
||||
> - **Frontend Boundary Separation**:
|
||||
> - **User Workspace**: Focused purely on end-user tools (Chat, Notebooks, Personal Settings).
|
||||
> - **Admin Dashboard**: Dedicated area for management (Knowledge Base files, System/Tenant Settings, Global Models).
|
||||
> - **Implementation**: We will introduce `react-router-dom` to provide clear URL boundaries (e.g., `/` for workspace and `/admin` for management) OR use a strict state-based layout split (`WorkspaceLayout` vs `AdminLayout`) with an app switcher.
|
||||
> - **Core Elements**:
|
||||
> - **Sleek Navigation Rail**: A minimal, collapsible sidebar with rounded active states, scoped to the current boundary (Admin vs Workspace).
|
||||
> - **Top Global Search**: A prominent, rounded search bar for quick access.
|
||||
> - **Airy Layout**: Increased white space and soft shadows to improve readability.
|
||||
> - **Gemini-like Chat**: A modern AI chat interface with clean message bubbles and a refined input area.
|
||||
> - **Mockup**:
|
||||
> 
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### [Component] Database Schema
|
||||
#### [NEW] [tenant.entity.ts](file:///d:/tmp/KnowledgeBase/server/src/tenant/tenant.entity.ts)
|
||||
- Define `Tenant` entity with `id` and `name`.
|
||||
|
||||
#### [MODIFY] [user.entity.ts](file:///d:/tmp/KnowledgeBase/server/src/user/user.entity.ts)
|
||||
- Add `tenantId` column (ManyToOne relationship with `Tenant`).
|
||||
- Add `role` column (Enum: `SUPER_ADMIN`, `TENANT_ADMIN`, `USER`).
|
||||
- Add `apiKey` column for API authentication.
|
||||
|
||||
#### [NEW] [tenant-setting.entity.ts](file:///d:/tmp/KnowledgeBase/server/src/tenant/tenant-setting.entity.ts)
|
||||
- Store organization-wide defaults (similar fields to `UserSetting`).
|
||||
|
||||
#### [MODIFY] [model-config.entity.ts](file:///d:/tmp/KnowledgeBase/server/src/model-config/model-config.entity.ts)
|
||||
- Add `tenantId` column (nullable for global models).
|
||||
- Update lookup logic to include system-level and tenant-level models.
|
||||
|
||||
#### [MODIFY] [knowledge-base.entity.ts](file:///d:/tmp/KnowledgeBase/server/src/knowledge-base/knowledge-base.entity.ts)
|
||||
- Add `tenantId` column.
|
||||
|
||||
#### [MODIFY] [knowledge-group.entity.ts](file:///d:/tmp/KnowledgeBase/server/src/knowledge-group/knowledge-group.entity.ts)
|
||||
- Add `tenantId` column.
|
||||
|
||||
#### [MODIFY] [search-history.entity.ts](file:///d:/tmp/KnowledgeBase/server/src/search-history/search-history.entity.ts)
|
||||
- Add `tenantId` column.
|
||||
|
||||
#### [MODIFY] [note.entity.ts](file:///d:/tmp/KnowledgeBase/server/src/note/note.entity.ts)
|
||||
- Add `tenantId` column.
|
||||
|
||||
### [Component] Infrastructure & Storage
|
||||
#### [MODIFY] [elasticsearch.service.ts](file:///d:/tmp/KnowledgeBase/server/src/elasticsearch/elasticsearch.service.ts)
|
||||
- Update `createIndex` mapping to include `tenantId` as a `keyword` field.
|
||||
- Modify `searchSimilar`, `searchFullText`, and `hybridSearch` to include `term: { tenantId }` filter.
|
||||
|
||||
#### [MODIFY] [knowledge-base.service.ts](file:///d:/tmp/KnowledgeBase/server/src/knowledge-base/knowledge-base.service.ts)
|
||||
- Update file storage logic to use `uploads/{tenantId}/{fileId}` path.
|
||||
|
||||
#### [NEW] [migrations]
|
||||
- Create a migration script to:
|
||||
1. Create the `Tenant` table.
|
||||
2. Create a "Default" tenant.
|
||||
3. Update all existing records to link to the "Default" tenant.
|
||||
|
||||
### [Component] User & Auth
|
||||
#### [NEW] [super-admin.guard.ts](file:///d:/tmp/KnowledgeBase/server/src/auth/super-admin.guard.ts)
|
||||
- Guard that checks for `SUPER_ADMIN` role.
|
||||
|
||||
#### [NEW] [tenant-admin.guard.ts](file:///d:/tmp/KnowledgeBase/server/src/auth/tenant-admin.guard.ts)
|
||||
- Guard that checks for `TENANT_ADMIN` role or higher within the same tenant.
|
||||
|
||||
### [Component] Frontend Separation
|
||||
#### [MODIFY] [App.tsx](file:///d:/workspace/AuraK/web/App.tsx)
|
||||
- Introduce a clear layout abstraction: `WorkspaceLayout` and `AdminLayout`.
|
||||
- Add an "App Switcher" for Admin users to toggle between User Workspace and Admin Dashboard.
|
||||
|
||||
#### [NEW] [WorkspaceLayout.tsx](file:///d:/workspace/AuraK/web/components/layouts/WorkspaceLayout.tsx)
|
||||
- Contains a customized `WorkspaceSidebarRail` showing only user-centric views (`chat`, `notebooks`).
|
||||
|
||||
#### [NEW] [AdminLayout.tsx](file:///d:/workspace/AuraK/web/components/layouts/AdminLayout.tsx)
|
||||
- Contains an `AdminSidebarRail` showing management views (`knowledge`, `settings`).
|
||||
|
||||
---
|
||||
|
||||
## Verification Plan
|
||||
|
||||
### Automated Tests
|
||||
- `curl -X GET http://localhost:3001/api/v1/knowledge-bases -H "x-api-key: YOUR_KEY"`
|
||||
- `curl -X POST http://localhost:3001/api/v1/chat -H "x-api-key: YOUR_KEY" -d '{"message": "Hello"}'`
|
||||
|
||||
### Manual Verification
|
||||
1. **Retrieve API Key**: Login to the system, then call `/api/user/api-key` to get the key.
|
||||
2. **Test External Request**: Use the retrieved key to call the new `/api/v1/*` endpoints.
|
||||
3. **Check Swagger**: Visit `http://localhost:3001/api/docs`.
|
||||
4. **Verify Streaming**: Ensure `POST /api/v1/chat` with `stream: true` returns SSE chunks.
|
||||
@@ -1,66 +0,0 @@
|
||||
# 知识库引入知识图谱(Knowledge Graph)分析与实施建议
|
||||
|
||||
基于项目当前技术栈(前端 React/Vite,后端 NestJS,使用 Elasticsearch、TypeORM 和 Langchain),如果要在现有的知识库(Knowledge Base)中引入知识图谱(Knowledge Graph)功能,通常是为了向 GraphRAG(图检索增强生成)演进。这能极大地提升系统处理复杂查询(如跨文档、多跳推理关联)的能力。
|
||||
|
||||
以下是针对现有项目的架构分析与实施建议:
|
||||
|
||||
## 一、 核心价值与应用场景
|
||||
将知识图谱引入知识库后,您可以实现:
|
||||
1. **更精准的 RAG (GraphRAG)**:传统的向量检索(如 Elasticsearch 密集向量)擅长捕捉语义相似性,但在“找出A与C之间的所有中间联系”等多跳逻辑推理上表现不佳。图谱可以弥补这一短板。
|
||||
2. **可视化探索**:允许用户在前端以节点和边的形式,直观地浏览知识库中概念之间的关系。
|
||||
3. **知识融合**:将分布在多篇独立文档(PDF/Word等)中的碎片化信息,通过实体(Entity)统一串联成网状结构。
|
||||
|
||||
## 二、 技术选型建议
|
||||
考虑到后端已经深度集成了 `@langchain/core` 和 `@langchain/openai`,强烈建议最大限度复用 Langchain 的生态体系。
|
||||
|
||||
1. **图数据库 (Graph Database) 选型**:
|
||||
* **优选:Neo4j**。它是目前与大模型和 Langchain 集成最成熟的图数据库。Langchain 原生支持 Neo4j(通过相关的扩展包),并且可以直接用 Cypher 语言进行图查询。
|
||||
* **备选:Memgraph** 或 **NebulaGraph**。
|
||||
* **当前栈复用**:如果不希望引入新的数据库组件,您也可以先用关系型数据库(当前用的 TypeORM 控制的底层数据库)建立三元组表 `(head, relation, tail)` 作为简化版的图存储,但查询效率和图算法能力远不及原生图引擎。
|
||||
|
||||
2. **实体/关系提取 (Knowledge Extraction)**:
|
||||
* **方案**:利用 LLM(目前已接入的大语言模型)。使用 Langchain 构建信息抽取链(Extraction Chain),通过 Prompt 指导模型从文本分块(Chunks)中提取 `[实体1] -> [关系] -> [实体2]` 的三元组。
|
||||
* **工具**:可以使用 Langchain 提供的 `LLMGraphTransformer` 工具。
|
||||
|
||||
3. **前端可视化组件**:
|
||||
* **推荐**:`react-force-graph` (轻量、支持 2D/3D、适合 React)。
|
||||
* **备选**:`vis-network` 或 ECharts 的关系图组件。
|
||||
|
||||
## 三、 架构演进与实施步骤
|
||||
建议分几个阶段进行,逐步迭代:
|
||||
|
||||
### 第一阶段:数据管道升级(建图)
|
||||
在当前的文件上传、解析和切分(Text Splitters)流程中,**在存入 Elasticsearch 向量库的同时,增加一条图谱构建分支**:
|
||||
1. **文本分块 (Chunking)**:复用现有的切分逻辑。
|
||||
2. **实体抽取 (Extraction)**:将 Chunk 发送给 LLM,提示词例如:“从以下文本中提取关键实体(如人名、机构、技术、概念)及它们之间的关系,以 JSON 格式输出。”
|
||||
3. **实体消歧与对齐 (Entity Resolution)**:比如“Apple”和“苹果公司”应指向同一个节点。可以结合向量空间中相似度,将意思相近的实体进行合并。
|
||||
4. **存入图数据库**:将提取出的 Node(实体)和 Edge(关系)写入 Neo4j。并将源文档的 Chunk ID 作为属性附加在 Node 或 Edge 上,以此实现“图谱与原文档内容的双向链接”。
|
||||
|
||||
### 第二阶段:检索融合 (Hybrid RAG)
|
||||
改造当前的问答(Chat/Search)接口,引入**混合检索(Hybrid Retrieval)**:
|
||||
1. 用户输入问题后,首先利用 LLM 解析问题中的核心**实体**。
|
||||
2. **图检索**:从图库中查询这些实体,获取相关的连通子图(例如周围 2 度的节点和边)。
|
||||
3. **向量检索**:同时在 Elasticsearch 中进行传统的语义相似度检索。
|
||||
4. **上下文组装**:将图谱中查到的关系路径(如:`A -> 属于 -> B -> 包含 -> C`)转化为自然语言,与向量检索拿到的文本片段拼在一起,作为 prompt 提供给 LLM 回答。
|
||||
|
||||
### 第三阶段:前端图谱可视化
|
||||
1. 提供一个单独的“知识图谱”视图页面。
|
||||
2. 后端提供 `/api/knowledge-graph/nodes` 接口,根据当前知识图(或特定过滤条件下的子图)返回节点和链接数据。
|
||||
3. 前端使用 `react-force-graph` 渲染:
|
||||
* 点击某个节点,可以展开查询关联节点。
|
||||
* 关联节点可以溯源,显示出“该关系是由哪份知识库文档的哪一句话提取出来的”。
|
||||
|
||||
## 四、 项目需要做出的具体改动评估
|
||||
1. **依赖层 (`server/package.json`)**:
|
||||
* 需要新增类似 `neo4j-driver`、`@langchain/community` 等依赖。
|
||||
* Docker 环境:`docker-compose.yml` 中需要引入 Neo4j 容器。
|
||||
2. **知识处理流 (`server/src/knowledge-base/` 等目录)**:
|
||||
* 需要新增一个专门处理图操作的服务(如 `graph.service.ts`)。
|
||||
* 需要修改当前的导入任务(`import-task`),让其支持异步地调用大模型进行耗时的实体抽取操作(这部分成本较高且较慢,建议通过事件驱动或消息队列异步处理)。
|
||||
3. **前端展示 (`web/components/`)**:
|
||||
* 设计新的 UI 组件(如 `GraphViewer.tsx`)。
|
||||
|
||||
## 五、 核心建议与避坑指南
|
||||
1. **Token 成本与耗时控制**:从全文中提取高质量图谱极耗费 Token,且速度慢。建议初期只对**高价值核心文档**开启图谱提取,并且采用批处理异步任务。
|
||||
2. **模型能力要求**:信息抽取高度依赖模型的指令遵循(Instruction Following)能力。如果使用的是私有部署的小模型,可能会出现实体抽取格式混乱的问题;建议使用如 GPT-4o, Claude 3.5 Sonnet 等强模型进行图谱提取。
|
||||
3. **本体论 (Ontology) 建设**:完全开放域的“自由抽取”会导致图谱非常混乱(如“今天”也成了一个无意义节点)。建议在业务初期,预定义好希望提取的实体类型(如:只提取 `[产品]`, `[故障]`, `[解决方案]`, `[组件]` 四类),在 Prompt 中强制 LLM 遵守这一 Schema。
|
||||
@@ -1,96 +0,0 @@
|
||||
# Development Standards
|
||||
|
||||
## Code Language Requirements
|
||||
|
||||
### 1. Comments
|
||||
- **All code comments must be in English**
|
||||
- Includes the following (not limited to):
|
||||
- Function/method comments
|
||||
- Inline comments
|
||||
- Code block explanations
|
||||
- TODO/FIXME comments
|
||||
|
||||
### 2. Logging
|
||||
- **All log output must be in English**
|
||||
- Includes the following (not limited to):
|
||||
- `logger.log()` info logs
|
||||
- `logger.warn()` warning logs
|
||||
- `logger.error()` error logs
|
||||
- `console.log()` debug output
|
||||
|
||||
### 3. Error Messages
|
||||
- **Error messages must support internationalization (i18n)**
|
||||
- **User-facing error messages**: Display in the user's selected language (Japanese/Chinese/English) via i18n system
|
||||
- **Debug/development error messages**: Display in the user's selected language via i18n system
|
||||
- **Exception messages**: Use i18n for internationalized error messages
|
||||
|
||||
## Examples
|
||||
|
||||
### Correct Comments and Logs (English + i18n)
|
||||
|
||||
```typescript
|
||||
// Get embeddings for texts
|
||||
async getEmbeddings(texts: string[]): Promise<number[][]> {
|
||||
this.logger.log(`Getting embeddings for ${texts.length} texts`);
|
||||
|
||||
try {
|
||||
// Call API to get embeddings
|
||||
const response = await this.callEmbeddingAPI(texts);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to get embeddings', error);
|
||||
// Use i18n for user-facing error messages
|
||||
throw new Error(this.i18n.t('errors.embeddingGenerationFailed'));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Using i18n for Error Messages
|
||||
|
||||
```typescript
|
||||
import { I18nService } from './i18n.service';
|
||||
|
||||
async processDocument(file: Express.Multer.File) {
|
||||
try {
|
||||
// Process document...
|
||||
return result;
|
||||
} catch (error) {
|
||||
// Error message in user's selected language
|
||||
throw new Error(this.i18n.t('errors.documentProcessingFailed', {
|
||||
filename: file.originalname
|
||||
}));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Compliance Standards
|
||||
|
||||
1. **During code reviews, always check the language of comments and logs**
|
||||
2. **New code must follow English comments and logs standards**
|
||||
3. **When refactoring existing code, update comments and logs to English simultaneously**
|
||||
4. **All error messages must use the i18n system for internationalization**
|
||||
|
||||
## Validation i18n Rules
|
||||
|
||||
### class-validator Limitation
|
||||
The `@MinLength`, `@MaxLength`, `@IsEmail`, etc. decorators from `class-validator` have a **static `message` property** that cannot access NestJS's `I18nService` at runtime. Therefore:
|
||||
|
||||
- **DO NOT** use hardcoded messages in validation decorators like:
|
||||
```typescript
|
||||
@MinLength(8, { message: 'Password must be at least 8 characters long' })
|
||||
```
|
||||
|
||||
- **DO** perform validation in the controller layer with i18n support:
|
||||
```typescript
|
||||
if (password.length < 6) {
|
||||
throw new BadRequestException(this.i18nService.getErrorMessage('passwordMinLength'));
|
||||
}
|
||||
```
|
||||
|
||||
- **OR** remove the decorator and rely on controller-level validation only
|
||||
|
||||
### Adding New Validation Rules
|
||||
When adding new validation to DTOs, ensure validation messages are internationalized by:
|
||||
1. Adding the i18n key to `server/src/i18n/messages.ts`
|
||||
2. Adding validation logic in the controller or service layer using `I18nService`
|
||||
|
||||
@@ -1,130 +0,0 @@
|
||||
# 飞书机器人快速参考
|
||||
|
||||
## 一、当前状态
|
||||
|
||||
### 飞书机器人知识库对接
|
||||
- **现状**:使用默认知识库(用户所有文件)
|
||||
- **原因**:`selectedFiles` 和 `selectedGroups` 参数为 `undefined`
|
||||
- **文件**:`server/src/feishu/feishu.service.ts` (line 311-331)
|
||||
|
||||
### 人才测评模块
|
||||
- **位置**:`server/src/assessment/`
|
||||
- **功能**:基于知识库生成问题、评估答案、生成报告
|
||||
- **接口**:REST API + SSE 流式更新
|
||||
|
||||
---
|
||||
|
||||
## 二、快速命令
|
||||
|
||||
### 飞书机器人命令
|
||||
```
|
||||
/assessment start [kbId|templateId] # 开始测评
|
||||
/assessment answer [answer] # 提交答案
|
||||
/assessment status # 查看状态
|
||||
/assessment result # 获取结果
|
||||
/assessment help # 帮助信息
|
||||
```
|
||||
|
||||
### 直接回复(无需命令前缀)
|
||||
```
|
||||
# 直接回复答案,系统自动识别
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、配置示例
|
||||
|
||||
### 创建带知识库配置的机器人
|
||||
```bash
|
||||
POST /feishu/bots
|
||||
{
|
||||
"appId": "cli_xxx",
|
||||
"appSecret": "xxx",
|
||||
"botName": "测评机器人",
|
||||
"knowledgeBaseId": "kb_xxx", # 特定知识库
|
||||
"knowledgeGroupId": "group_xxx" # 或知识组
|
||||
}
|
||||
```
|
||||
|
||||
### 更新知识库配置
|
||||
```bash
|
||||
PATCH /feishu/bots/:id/knowledge
|
||||
{
|
||||
"knowledgeBaseId": "kb_xxx",
|
||||
"knowledgeGroupId": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、文件位置
|
||||
|
||||
### 核心文件
|
||||
```
|
||||
server/src/feishu/
|
||||
├── feishu.service.ts # 主服务
|
||||
├── feishu.controller.ts # 控制器
|
||||
├── feishu-ws.manager.ts # WebSocket 管理
|
||||
├── entities/
|
||||
│ ├── feishu-bot.entity.ts # 机器人实体
|
||||
│ └── feishu-assessment-session.entity.ts # 测评会话实体
|
||||
├── dto/
|
||||
│ ├── create-bot.dto.ts # 创建机器人 DTO
|
||||
│ └── assessment-command.dto.ts # 命令 DTO
|
||||
└── services/
|
||||
├── assessment-command.parser.ts # 命令解析器
|
||||
└── feishu-assessment.service.ts # 测评服务
|
||||
```
|
||||
|
||||
### 数据库迁移
|
||||
```
|
||||
server/src/migrations/
|
||||
├── XXXXXX-AddFeishuBotKnowledgeFields.ts # 添加知识库字段
|
||||
└── XXXXXX-CreateFeishuAssessmentSessionTable.ts # 创建测评会话表
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、实施检查清单
|
||||
|
||||
### 阶段 1:基础架构
|
||||
- [ ] 创建数据库迁移脚本
|
||||
- [ ] 更新 FeishuBot 实体
|
||||
- [ ] 更新 CreateFeishuBotDto
|
||||
- [ ] 修改 FeishuService.processChatMessage()
|
||||
|
||||
### 阶段 2:测评集成
|
||||
- [ ] 创建 FeishuAssessmentSession 实体
|
||||
- [ ] 实现命令解析器
|
||||
- [ ] 实现 FeishuAssessmentService
|
||||
- [ ] 集成到 FeishuService
|
||||
- [ ] 设计飞书卡片模板
|
||||
|
||||
### 阶段 3:测试
|
||||
- [ ] 单元测试
|
||||
- [ ] 集成测试
|
||||
- [ ] 文档更新
|
||||
|
||||
---
|
||||
|
||||
## 六、常见问题
|
||||
|
||||
### Q1: 如何让飞书机器人只搜索特定知识库?
|
||||
**A**: 在创建机器人时设置 `knowledgeBaseId` 字段
|
||||
|
||||
### Q2: 如何切换知识库?
|
||||
**A**: 使用 PATCH `/feishu/bots/:id/knowledge` 接口更新配置
|
||||
|
||||
### Q3: 测评命令不生效怎么办?
|
||||
**A**: 检查命令格式是否正确,确保以 `/assessment` 或 `/测评` 开头
|
||||
|
||||
### Q4: 如何查看测评进度?
|
||||
**A**: 发送 `/assessment status` 或 `/assessment result`
|
||||
|
||||
---
|
||||
|
||||
## 七、参考文档
|
||||
|
||||
- 完整设计文档: `feishu-assessment-integration-design.md`
|
||||
- 设计摘要: `feishu-assessment-integration-summary.md`
|
||||
- 飞书开放平台: https://open.feishu.cn/document
|
||||
@@ -1,10 +0,0 @@
|
||||
# Admin Account
|
||||
|
||||
Created: 2026-05-12
|
||||
|
||||
## Default Login
|
||||
- **Username:** admin
|
||||
- **Password:** admin123
|
||||
|
||||
> Note: Password is randomly generated on first server start.
|
||||
> Last reset: 2026-05-15 (reset to admin123 for testing)
|
||||
@@ -1,531 +0,0 @@
|
||||
# AuraK 系统调试检查清单
|
||||
|
||||
> **版本**: 2.0
|
||||
> **更新日期**: 2026-05-14
|
||||
> **文档状态**: ✅ 已完成全面验证
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [数据库问题](#一数据库问题)
|
||||
2. [API 前后端一致性](#二api-前后端一致性)
|
||||
3. [题库模块检查点](#三题库模块检查点)
|
||||
4. [评估流程检查点](#四评估流程检查点)
|
||||
5. [模型配置检查点](#五模型配置检查点)
|
||||
6. [角色与用户故事验证](#六角色与用户故事验证)
|
||||
7. [画面功能验证](#七画面功能验证)
|
||||
8. [调试技巧](#八调试技巧)
|
||||
9. [重启前检查清单](#九重启前检查清单)
|
||||
10. [典型问题模式](#十典型问题模式)
|
||||
|
||||
---
|
||||
|
||||
## 一、数据库问题
|
||||
|
||||
### 1.1 SQLite 类型兼容
|
||||
- [x] Entity 使用 `simple-enum` 而非 `enum`
|
||||
- [x] 移除 `@Column` 的 `default` 值(SQLite不支持enum默认值)
|
||||
- [x] 所有 `@Column` 必须指定 `type`(如 `type: 'text'`)
|
||||
|
||||
**检查命令**:
|
||||
```bash
|
||||
# 检查所有 Entity 的 @Column 是否有 type
|
||||
grep -r "@Column({ name:" server/src/assessment/entities/ | grep -v "type:"
|
||||
```
|
||||
|
||||
**修复记录** (2026-05-14):
|
||||
- `assessment-session.entity.ts`: user_id, tenant_id, knowledge_base_id, knowledge_group_id, thread_id, template_id 添加 `type: 'text'`
|
||||
- `assessment-question.entity.ts`: session_id 添加 `type: 'text'`
|
||||
- `assessment-answer.entity.ts`: question_id 添加 `type: 'text'`
|
||||
- `assessment-certificate.entity.ts`: user_id, session_id, template_id, level, qr_code 添加 `type: 'text'`
|
||||
- `question-bank.entity.ts`: 移除 status 字段的 default 值
|
||||
|
||||
### 1.2 Null 值处理
|
||||
- [x] 查询时处理 null:`WHERE column IS NULL` 而非 `= NULL`
|
||||
- [x] Service 方法处理 `tenantId: null` 情况
|
||||
- [x] Entity 字段标记 `nullable: true`
|
||||
|
||||
**检查要点**:
|
||||
```typescript
|
||||
// 错误
|
||||
const result = await repo.findOne({ where: { tenantId: null } });
|
||||
|
||||
// 正确
|
||||
const result = await repo.findOne({ where: { tenantId: IsNull() } });
|
||||
// 或
|
||||
const result = await repo.findOne({ where: { tenantId: undefined } });
|
||||
```
|
||||
|
||||
### 1.3 数据库重置
|
||||
- [ ] 删除数据库后重新创建会导致所有数据丢失
|
||||
- [ ] 确认是否有备份或可以恢复
|
||||
|
||||
---
|
||||
|
||||
## 二、API 前后端一致性
|
||||
|
||||
### 2.1 HTTP 方法
|
||||
- [x] POST 创建资源
|
||||
- [x] PUT 更新资源
|
||||
- [x] GET 获取资源
|
||||
- [x] DELETE 删除资源
|
||||
|
||||
### 2.2 端点匹配
|
||||
- [x] 前端 service 调用的端点与后端 controller 一致
|
||||
- [x] 特别注意:后端用 PUT 但前端用 POST 的情况
|
||||
- [x] 检查新增的 API 路由是否已添加
|
||||
|
||||
**API 匹配检查表**:
|
||||
|
||||
| 前端方法 | 后端端点 | 状态 |
|
||||
|---------|---------|------|
|
||||
| startSession | POST /assessment/start | ✅ |
|
||||
| submitAnswerStream | POST /assessment/:id/answer | ✅ |
|
||||
| getSessionState | GET /assessment/:id/state | ✅ |
|
||||
| deleteSession | DELETE /assessment/:id | ✅ |
|
||||
| getHistory | GET /assessment/history | ✅ |
|
||||
| getCertificate | GET /assessment/:id/certificate | ✅ |
|
||||
| exportPdf | GET /assessment/:id/export/pdf | ✅ |
|
||||
| exportExcel | GET /assessment/:id/export/excel | ✅ |
|
||||
| checkTimeLimits | GET /assessment/:id/time-check | ✅ |
|
||||
| getStats | GET /assessment/stats | ✅ |
|
||||
| getRadarStats | GET /assessment/stats/radar | ✅ |
|
||||
| getTrendStats | GET /assessment/stats/trend | ✅ |
|
||||
| reviewAssessment | PUT /assessment/:id/review | ✅ |
|
||||
| forceEnd | POST /assessment/:id/force-end | ✅ |
|
||||
| templateService.getAll | GET /assessment/templates | ✅ |
|
||||
| questionBankService.getBanks | GET /question-banks | ✅ |
|
||||
| questionBankService.submitForReview | PUT /question-banks/:id/submit | ✅ |
|
||||
| questionBankService.approveBank | PUT /question-banks/:id/review | ✅ |
|
||||
| questionBankService.publishBank | PUT /question-banks/:id/publish | ✅ |
|
||||
| questionBankService.generateQuestions | POST /question-banks/:id/generate | ✅ |
|
||||
|
||||
### 2.3 路由传参
|
||||
- [x] RESTful 路径参数:`:id`, `:bankId`
|
||||
- [x] Query 参数:`?page=1&limit=10`
|
||||
- [x] Body 参数:JSON 请求体
|
||||
|
||||
**检查命令**:
|
||||
```bash
|
||||
# 检查后端路由
|
||||
grep -r "@Get\|@Post\|@Put\|@Delete" server/src/assessment/*controller.ts
|
||||
|
||||
# 检查前端调用
|
||||
grep -r "apiClient\|service\." web/components/views/
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、题库模块检查点
|
||||
|
||||
### 3.1 后端 Entity
|
||||
- [x] `QuestionBank` - simple-enum 类型,无默认值
|
||||
- [x] `QuestionBankItem` - 所有 enum 字段使用 simple-enum
|
||||
- [x] `status` 字段必须有默认值(在 service 层设置)
|
||||
|
||||
**验证命令**:
|
||||
```bash
|
||||
grep -A5 "enum:" server/src/assessment/entities/question-bank*.ts
|
||||
```
|
||||
|
||||
### 3.2 后端 Service
|
||||
- [x] `create()` - 验证 name 不为空
|
||||
- [x] `addItem()` - 验证 questionText 不为空,设置 status 默认值
|
||||
- [x] `generateQuestions()` - AI 生成时设置 status
|
||||
- [x] `findAll()` - 处理 tenantId 为 null 的查询
|
||||
- [x] `create()` - 处理 tenantId 为 null 的创建
|
||||
|
||||
**检查代码模式**:
|
||||
```typescript
|
||||
// create 方法必须验证
|
||||
if (!createDto.name || !createDto.name.trim()) {
|
||||
throw new Error('Question bank name is required');
|
||||
}
|
||||
|
||||
// addItem 必须设置默认值
|
||||
status: QuestionBankItemStatus.PENDING_REVIEW
|
||||
```
|
||||
|
||||
### 3.3 后端 Controller
|
||||
- [x] GET `/items` 路由存在
|
||||
- [x] 路由方法与 service 方法匹配(PUT vs POST)
|
||||
|
||||
### 3.4 前端 Service
|
||||
- [x] `submitForReview` - 使用 PUT
|
||||
- [x] `approveBank/rejectBank` - 使用 `/review` 端点
|
||||
- [x] `publishBank` - 使用 PUT
|
||||
- [x] `getBankItems` - 调用正确的端点
|
||||
|
||||
### 3.5 前端 Component
|
||||
- [x] 组件已正确 export
|
||||
- [x] 路由已添加到 index.tsx
|
||||
- [x] Service 调用正确
|
||||
|
||||
---
|
||||
|
||||
## 四、评估流程检查点
|
||||
|
||||
### 4.1 状态机 (LangGraph)
|
||||
- [x] 变量作用域:避免 if/else 块内定义,return 中使用
|
||||
- [x] 数组空值:`questions || []` 防护
|
||||
- [x] 负数处理:`Math.max(0, remaining)`
|
||||
|
||||
**修复记录** (2026-05-14):
|
||||
- `builder.ts`: 添加 `Math.max(0, state.currentQuestionIndex || 0)` 防护负数
|
||||
|
||||
**检查代码**:
|
||||
```typescript
|
||||
// builder.ts 中必须使用
|
||||
const currentIndex = Math.max(0, state.currentQuestionIndex || 0);
|
||||
```
|
||||
|
||||
### 4.2 API 一致性
|
||||
- [x] 前端使用 `/answer` 还是 `/answer-stream`
|
||||
- [x] 后端响应格式与前端期望一致
|
||||
|
||||
**决策**: 使用非流式 API `/answer`,前端已同步
|
||||
|
||||
---
|
||||
|
||||
## 五、模型配置检查点
|
||||
|
||||
### 5.1 LLM 配置
|
||||
- [x] Base URL 正确(官方API vs 本地部署)
|
||||
- [x] Model ID 正确
|
||||
- [x] API Key 配置
|
||||
|
||||
### 5.2 Embedding 配置
|
||||
- [x] 向量维度匹配(Ollama nomic-embed-text 为 768维)
|
||||
- [x] 服务可访问(IP/端口映射)
|
||||
|
||||
---
|
||||
|
||||
## 六、角色与用户故事验证
|
||||
|
||||
### 6.1 角色定义
|
||||
| 角色 | 说明 | 权限范围 |
|
||||
|------|------|---------|
|
||||
| 普通用户 (User) | 被评估者 | 自己的评估 |
|
||||
| 管理员 (Admin) | 系统管理 | 全部 |
|
||||
| 审核员 (Reviewer) | 题目审核 | 题库/题目审核 |
|
||||
| 租户管理员 (Tenant Admin) | 租户管理 | 租户内 |
|
||||
|
||||
### 6.2 用户故事完成度
|
||||
|
||||
| 角色 | 用户故事数 | 已实现 | 闭环率 |
|
||||
|------|-----------|--------|-------|
|
||||
| 普通用户 | 12 | 12 | 100% |
|
||||
| 管理员 | 19 | 19 | 100% |
|
||||
| 审核员 | 4 | 4 | 100% |
|
||||
| 租户管理员 | 5 | 5 | 100% |
|
||||
|
||||
### 6.3 用户故事检查表
|
||||
|
||||
#### 普通用户 (User)
|
||||
- [x] 选择评估范围(知识组/模板)
|
||||
- [x] 开始评估
|
||||
- [x] 回答问题
|
||||
- [x] 查看实时反馈
|
||||
- [x] 查看剩余时间
|
||||
- [x] 查看评估历史(最新3条)
|
||||
- [x] 查看历史详情
|
||||
- [x] 评估完成查看报告
|
||||
- [x] 导出PDF报告
|
||||
- [x] 导出Excel报告
|
||||
- [x] 查看证书
|
||||
- [x] 删除自己的评估
|
||||
|
||||
#### 管理员 (Admin)
|
||||
- [x] 模板CRUD
|
||||
- [x] 题库CRUD
|
||||
- [x] AI生成题目
|
||||
- [x] 批量审核题目
|
||||
- [x] 审核评估(调整分数)
|
||||
- [x] 强制结束评估
|
||||
- [x] 查看统计数据
|
||||
- [x] 导出CSV
|
||||
- [x] 验证证书
|
||||
|
||||
### 6.4 权限验证检查
|
||||
- [x] 后端权限控制正确 (role 检查)
|
||||
- [x] 前端画面访问控制正确
|
||||
|
||||
---
|
||||
|
||||
## 七、画面功能验证
|
||||
|
||||
### 7.1 画面清单
|
||||
|
||||
| 画面 | 文件路径 | 功能 |
|
||||
|------|---------|------|
|
||||
| AssessmentView | components/views/AssessmentView.tsx | 主评估界面 |
|
||||
| AssessmentStatsView | components/views/AssessmentStatsView.tsx | 统计数据 |
|
||||
| AssessmentTemplateManager | components/views/AssessmentTemplateManager.tsx | 模板管理 |
|
||||
| QuestionBankView | components/views/QuestionBankView.tsx | 题库列表 |
|
||||
| QuestionBankDetailView | components/views/QuestionBankDetailView.tsx | 题库详情 |
|
||||
|
||||
### 7.2 按钮与功能验证
|
||||
|
||||
| 画面 | 按钮数量 | 输入框数量 | 功能闭环 | 状态 |
|
||||
|------|---------|-----------|---------|------|
|
||||
| AssessmentView | 14 | 1 | ✅ | ✅ |
|
||||
| QuestionBankView | 10 | 3 | ✅ | ✅ |
|
||||
| QuestionBankDetailView | 10 | 8 | ✅ | ✅ |
|
||||
| AssessmentTemplateManager | 6 | 6 | ✅ | ✅ |
|
||||
| AssessmentStatsView | 2 | 4 | ✅ | ✅ |
|
||||
|
||||
### 7.3 输入框检查
|
||||
|
||||
| 画面 | 输入框 | onChange | onKeyDown | 验证 | 状态 |
|
||||
|------|-------|----------|-----------|------|------|
|
||||
| AssessmentView | 答案输入 | ✅ | Enter提交 | - | ✅ |
|
||||
| QuestionBankView | 名称/描述/模板 | ✅ | - | 验证 | ✅ |
|
||||
| QuestionBankDetailView | 题目属性 | ✅ | - | 转换 | ✅ |
|
||||
| AssessmentTemplateManager | 模板字段 | ✅ | - | 解析 | ✅ |
|
||||
| AssessmentStatsView | 日期/筛选 | ✅ | - | - | ✅ |
|
||||
|
||||
### 7.4 参数传递链验证
|
||||
|
||||
```
|
||||
开始评估: startSession(selectedGroup, language, selectedTemplate)
|
||||
↓
|
||||
回答问题: submitAnswerStream(session.id, answer, language)
|
||||
↓
|
||||
时间检查: checkTimeLimits(session.id) [定期]
|
||||
↓
|
||||
获取证书: getCertificate(session.id)
|
||||
↓
|
||||
导出报告: exportExcel/Pdf(session.id)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、调试技巧
|
||||
|
||||
### 8.1 日志添加
|
||||
- [x] 后端:console.log 在关键方法
|
||||
- [x] 前端:console.log 在 API 调用前后
|
||||
- [x] 日志包含关键变量值
|
||||
|
||||
**关键日志位置**:
|
||||
- `assessment.service.ts`: startSession, submitAnswer, checkTimeLimits
|
||||
- `assessment.controller.ts`: 所有方法入口
|
||||
- `AssessmentView.tsx`: handleStartAssessment, handleSubmitAnswer
|
||||
|
||||
### 8.2 检查步骤
|
||||
1. 查看 Docker logs:`docker compose logs server --tail 50`
|
||||
2. 查看前端 Console(F12)
|
||||
3. 查看 Network 面板请求响应
|
||||
4. 直接调用 API 测试
|
||||
|
||||
### 8.3 常见症状
|
||||
- [x] 弹窗显示成功但数据未更新 → API 可能失败,检查返回数据格式
|
||||
- [x] 页面空白无数据 → 检查 API 是否被调用,参数是否正确
|
||||
- [x] 403 权限错误 → 检查用户角色是否匹配
|
||||
|
||||
---
|
||||
|
||||
## 九、重启前检查清单
|
||||
|
||||
### 代码层面
|
||||
- [x] 所有修改的文件已保存
|
||||
- [x] 没有语法错误
|
||||
- [x] import 语句正确
|
||||
- [x] Entity @Column 类型完整
|
||||
|
||||
### 构建层面
|
||||
- [ ] `docker compose build` 成功
|
||||
- [ ] 无新增的编译错误
|
||||
|
||||
### 测试层面
|
||||
- [ ] 服务启动成功
|
||||
- [ ] 登录功能正常
|
||||
- [ ] 目标功能可访问
|
||||
|
||||
---
|
||||
|
||||
## 十、典型问题模式
|
||||
|
||||
### 问题1:新增功能不工作
|
||||
**检查顺序:**
|
||||
1. [x] 后端 entity 是否注册到 app.module
|
||||
2. [x] 后端 service 是否在 module 中提供
|
||||
3. [x] 后端 controller 是否有对应路由
|
||||
4. [x] 前端 service 是否调用正确端点
|
||||
5. [x] 前端 component 是否正确 import 和 export
|
||||
6. [x] 前端 route 是否添加
|
||||
|
||||
### 问题2:数据创建成功但查询不到
|
||||
**检查顺序:**
|
||||
1. [x] tenantId 是否正确设置
|
||||
2. [x] 查询条件是否匹配(== vs IS NULL)
|
||||
3. [x] 权限是否正确
|
||||
|
||||
### 问题3:类型不匹配
|
||||
**检查顺序:**
|
||||
1. [x] 后端 entity 类型
|
||||
2. [x] 后端 DTO 类型
|
||||
3. [x] 前端 service 接口类型
|
||||
4. [x] 前端 types 定义
|
||||
5. [x] API 响应格式
|
||||
|
||||
---
|
||||
|
||||
## 十一、题库生成与关联功能深度检查 (2026-05-14)
|
||||
|
||||
### 11.1 题库生成 (generateQuestions)
|
||||
|
||||
**检查项**:
|
||||
- [x] 输入验证 - count 范围检查 (1-50)
|
||||
- [x] 输入验证 - knowledgeBaseContent 最小长度检查
|
||||
- [x] 错误处理 - JSON 解析失败处理
|
||||
- [x] 性能优化 - 批量保存而非逐个保存
|
||||
|
||||
**修复记录**:
|
||||
- 添加 count 范围验证: `if (count <= 0 || count > 50) throw new Error(...)`
|
||||
- 添加 content 最小长度验证: `if (!knowledgeBaseContent || content.trim().length < 10)`
|
||||
- 修改为批量保存: `items.push(item)` → `await this.itemRepository.save(items)`
|
||||
|
||||
### 11.2 题目选择 (selectQuestions)
|
||||
|
||||
**检查项**:
|
||||
- [x] 循环条件逻辑 - 正确终止条件
|
||||
- [x] 随机选择算法 - Fisher-Yates shuffle
|
||||
- [x] 维度分布 - 均匀轮询
|
||||
|
||||
**修复记录**:
|
||||
- 重写循环逻辑: `while (selected.length < count && availableItems.length > 0)`
|
||||
- 添加 shuffle 方法: `private shuffleArray<T>(array: T[]): T[]`
|
||||
- 添加循环次数上限: `if (dimIdx >= DIMENSIONS.length * 3) break`
|
||||
|
||||
### 11.3 题库状态管理
|
||||
|
||||
**检查项**:
|
||||
- [x] submitForReview - DRAFT 状态检查
|
||||
- [x] review - PENDING_REVIEW 状态检查
|
||||
- [x] publish - PUBLISHED/REJECTED 状态检查
|
||||
|
||||
**状态**: ✅ 逻辑正确
|
||||
|
||||
### 11.4 评估启动与题目关联
|
||||
|
||||
**检查项**:
|
||||
- [x] 题库查询逻辑 - PUBLISHED 状态过滤
|
||||
- [x] 题目数量检查 - 不足时回退到 LLM 生成
|
||||
- [x] 题目选择调用 - selectQuestions 方法
|
||||
|
||||
**状态**: ✅ 逻辑正确
|
||||
|
||||
### 11.5 LangGraph 题目生成节点
|
||||
|
||||
**检查项**:
|
||||
- [x] 已有足够题目时跳过生成
|
||||
- [x] 已有题目传递到状态
|
||||
|
||||
**修复记录**:
|
||||
- 添加跳过逻辑:
|
||||
```typescript
|
||||
if (existingQuestions.length >= limitCount) {
|
||||
console.log('[GeneratorNode] Skipping generation - enough questions from bank');
|
||||
return { questions: existingQuestions };
|
||||
}
|
||||
```
|
||||
|
||||
### 11.6 其他关联功能检查
|
||||
|
||||
| 功能模块 | 检查结果 | 备注 |
|
||||
|---------|---------|------|
|
||||
| 批量审核 (batchReviewItems) | ✅ | 状态更新、comment 追加 |
|
||||
| 证书生成 (generateCertificate) | ✅ | 等级判定、已有证书复用 |
|
||||
| 数据导出 (exportToExcel/Pdf) | ⚠️ | question 无 order 字段 |
|
||||
| 时间控制 (checkTimeLimits) | ✅ | 计算逻辑正确 |
|
||||
| 评估审核 (reviewAssessment) | ⚠️ | 审核后 passed 未更新 → 已修复 |
|
||||
| 统计功能 (getStats/radar/trend) | ✅ | 权限过滤、数据聚合 |
|
||||
| 权限控制 (isAdmin) | ✅ | 角色检查正确 |
|
||||
| 错误处理 | ✅ | NotFound/Forbidden/Error |
|
||||
| 空值处理 | ✅ | 默认值防护完善 |
|
||||
|
||||
### 11.7 本次修复汇总
|
||||
|
||||
| # | 问题 | 位置 | 严重程度 | 状态 |
|
||||
|---|------|------|---------|------|
|
||||
| 1 | selectQuestions 循环条件错误 | question-bank.service.ts | 高 | ✅ |
|
||||
| 2 | 随机选择算法不均匀 | question-bank.service.ts | 中 | ✅ |
|
||||
| 3 | count 无上限检查 | question-bank.service.ts | 中 | ✅ |
|
||||
| 4 | 空 content 仍调用 LLM | question-bank.service.ts | 中 | ✅ |
|
||||
| 5 | 逐个保存而非批量 | question-bank.service.ts | 低 | ✅ |
|
||||
| 6 | 已有足够题目时仍生成 | generator.node.ts | 高 | ✅ |
|
||||
| 7 | 审核后 passed 未更新 | assessment.service.ts | 中 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 附录:相关文件位置
|
||||
|
||||
### 后端核心
|
||||
- `server/src/app.module.ts` - Entity 注册
|
||||
- `server/src/assessment/assessment.module.ts` - 模块配置
|
||||
- `server/src/assessment/entities/` - 数据实体
|
||||
- `server/src/assessment/assessment.service.ts` - 业务逻辑
|
||||
- `server/src/assessment/assessment.controller.ts` - API 路由
|
||||
- `server/src/assessment/graph/` - LangGraph 状态机
|
||||
- `server/src/assessment/graph/nodes/generator.node.ts` - 题目生成节点
|
||||
- `server/src/assessment/graph/nodes/interviewer.node.ts` - 题目展示节点
|
||||
- `server/src/assessment/graph/nodes/grader.node.ts` - 评分节点
|
||||
- `server/src/assessment/services/question-bank.service.ts` - 题库服务
|
||||
- `server/src/assessment/services/export.service.ts` - 导出服务
|
||||
|
||||
### 前端核心
|
||||
- `web/index.tsx` - 路由配置
|
||||
- `web/services/assessmentService.ts` - 评估API调用
|
||||
- `web/services/questionBankService.ts` - 题库API调用
|
||||
- `web/services/templateService.ts` - 模板API调用
|
||||
- `web/components/views/AssessmentView.tsx` - 评估页面
|
||||
- `web/components/views/AssessmentStatsView.tsx` - 统计页面
|
||||
- `web/components/views/QuestionBankView.tsx` - 题库列表
|
||||
- `web/components/views/QuestionBankDetailView.tsx` - 题库详情
|
||||
- `web/components/views/AssessmentTemplateManager.tsx` - 模板管理
|
||||
- `web/types.ts` - 类型定义
|
||||
|
||||
---
|
||||
|
||||
## 十二、代码整合性验证报告 (2026-05-15)
|
||||
|
||||
### 12.1 用户故事验证 (33条)
|
||||
|
||||
| 分类 | 总数 | 通过 | 修复后通过 |
|
||||
|------|------|------|-----------|
|
||||
| 普通用户 (US-01~10) | 10 | 9 | ✅ 10 (修复历史限制) |
|
||||
| 管理员 (AM-01~21) | 21 | 19 | ✅ 21 (修复证书端点) |
|
||||
| 审核员 (RV-01~02) | 2 | 2 | ✅ 2 |
|
||||
|
||||
### 12.2 修复的代码缺口 (9项)
|
||||
|
||||
| # | 问题 | 严重度 | 修复内容 |
|
||||
|---|------|--------|---------|
|
||||
| 1 | getUserHistory 返回100条而非3条 | P0 | `take: 100` → `take: 3` |
|
||||
| 2 | verifyCertificate 空@UseGuards() | P0 | 改为 `@Public()` 装饰器 |
|
||||
| 3 | QuestionBank.status 无默认值 | P0 | 添加 `default: DRAFT` |
|
||||
| 4 | getPublicCertificate 未公开 | P1 | 添加 `@Public()` 装饰器 |
|
||||
| 5 | AssessmentStatsView 硬编码isAdmin=true | P1 | 改为 `user.role === 'SUPER_ADMIN'` |
|
||||
| 6 | 创建题库带templateId报500 | P0 | 修复create方法+唯一约束检查 |
|
||||
| 7 | 异常消息未透传前端 | P0 | throw Error→BadRequestException |
|
||||
| 8 | 前端"Failed to generate"不显示真实错误 | P0 | 解析response body中的message |
|
||||
| 9 | SSE答案使用GET+QueryParam | P2 | 已记录待后续优化 |
|
||||
|
||||
### 12.3 已验证的业务闭环
|
||||
|
||||
```
|
||||
模板创建 → 题库创建(AI生成/手动添加) → 提交审核 → 审核通过 → 发布题库
|
||||
→ 创建评估 → AI生成题目 → 用户答题 → AI评分 → 生成报告 → 审核评估
|
||||
→ 生成证书(通过时) → 导出Excel/PDF
|
||||
```
|
||||
|
||||
### 12.4 剩余建议 (P2)
|
||||
|
||||
| # | 问题 | 建议 |
|
||||
|---|------|------|
|
||||
| 1 | PDF导出实为纯文本 | 使用pdfkit等库生成真正的PDF |
|
||||
| 2 | difficultyDistribution类型不一致 | 统一使用 `{standard,advanced,specialist}` |
|
||||
| 3 | 强制结束评估清零分数 | 保留已有分数,记录`forceEnded`标志 |
|
||||
| 4 | 批量审核拒绝项设为PENDING_REVIEW | 改为REJECTED状态或明确语义 |
|
||||
@@ -1,68 +0,0 @@
|
||||
# Feature Design: Automatic Title Generation (feat-auto-title-generation)
|
||||
|
||||
## 1. Overview
|
||||
This feature automatically generates meaningful titles for uploaded documents and chat sessions using AI. It aims to replace generic filenames and "New Conversation" labels with content-aware titles, improving user experience and organization.
|
||||
|
||||
## 2. Requirements
|
||||
|
||||
### 2.1 Document Title Generation
|
||||
- **Trigger**: Automatically triggered after text extraction (Fast or Precise mode).
|
||||
- **Process**:
|
||||
1. Extract a sample of the document content (first 2,000 - 3,000 characters).
|
||||
2. Send the content to the default LLM with a specific generation prompt.
|
||||
3. Update the `KnowledgeBase` record with the generated title.
|
||||
- **Rules**:
|
||||
- The title should be concise (less than 50 characters).
|
||||
- It should be in the user's preferred language (defaulting to the detected document language if possible).
|
||||
- Output should be "raw" (no preamble like "The title is...").
|
||||
|
||||
### 2.2 Chat Title Generation
|
||||
- **Trigger**: Triggered after the first user message and its corresponding assistant response are recorded.
|
||||
- **Process**:
|
||||
1. Collect the initial message pair.
|
||||
2. Send the pair to the default LLM with a generation prompt.
|
||||
3. Update the `SearchHistory` record's `title` field.
|
||||
- **Rules**: Same as document titles.
|
||||
|
||||
## 3. Technical Design
|
||||
|
||||
### 3.1 Data Model Changes
|
||||
- **KnowledgeBase Entity**: Add a `title` field (nullable, optional). If empty, fallback to `originalName`.
|
||||
- **SearchHistory Entity**: No changes required (has `title`).
|
||||
|
||||
### 3.2 Backend Implementation
|
||||
|
||||
#### KnowledgeBaseService
|
||||
- Add `generateTitle(kbId: string)` method.
|
||||
- Hook into `processFile` after `updateStatus(kbId, FileStatus.EXTRACTED)`.
|
||||
|
||||
#### ChatService / SearchHistoryService
|
||||
- Add logic to check if the session title is still the default (usually the first message snippet) and trigger `generateTitle(historyId: string)` after the first assistant response.
|
||||
|
||||
#### Prompt Design
|
||||
- **Document Prompt**:
|
||||
```text
|
||||
You are a document analyzer. Read the provided text and generate a concise, professional title (max 50 chars).
|
||||
Return ONLY the title.
|
||||
Language: {userLanguage}
|
||||
Text: {contentSample}
|
||||
```
|
||||
- **Chat Prompt**:
|
||||
```text
|
||||
Based on the following conversation snippet, generate a short, descriptive title (max 50 chars) that summarizes the topic.
|
||||
Return ONLY the title.
|
||||
Language: {userLanguage}
|
||||
Snippet:
|
||||
User: {userMessage}
|
||||
AI: {aiResponse}
|
||||
```
|
||||
|
||||
## 4. Verification Plan
|
||||
|
||||
### Automated Tests
|
||||
- Integration tests in `KnowledgeBaseService` to verify the title field is updated after processing.
|
||||
- Mock LLM responses to ensure the title update logic works.
|
||||
|
||||
### Manual Verification
|
||||
- Upload various files (PDF, Word, TXT) and verify the displayed title in the knowledge base list.
|
||||
- Start a new chat, send a message, and check the sidebar for the updated session title.
|
||||
@@ -1,59 +0,0 @@
|
||||
# Design: Cross-Document Comparison (Agentic Workflow)
|
||||
|
||||
## 1. Background & Problem
|
||||
Users often need to compare multiple documents (e.g., "Compare the financial reports of Q1 and Q2" or "Differences between Product A and Product B specs").
|
||||
Standard RAG retrieves chunks based on semantic similarity to the query. While "Multi-Query" helps, standard RAG might:
|
||||
1. Retrieve too many chunks from one document and miss the other.
|
||||
2. Fail to align comparable attributes (e.g., comparing "revenue" in Doc A with "profit" in Doc B).
|
||||
3. Produce a generic text answer instead of a structured comparison.
|
||||
|
||||
## 2. Solution: Agentic Comparison Workflow
|
||||
We will implement a specialized workflow (or "Light Agent") that:
|
||||
1. **Analyzes the Request**: Identifies the subjects to compare (e.g., "Q1 Report", "Q2 Report") and the dimensions (e.g., "Revenue", "Risks").
|
||||
2. **Targeted Retrieval**:
|
||||
- Explicitly filters/searches for Doc A.
|
||||
- Explicitly filters/searches for Doc B.
|
||||
3. **Structured Synthesis**: Generates the answer, potentially forcing a Markdown Table format for clarity.
|
||||
|
||||
## 3. Technical Architecture
|
||||
|
||||
### 3.1 Backend (`ComparisonService` or extension to `RagService`)
|
||||
- **Intent Detection**: Modify `ChatService` or `RagService` to detect comparison intent (can utilize LLM or simple heuristics + keywords).
|
||||
- **Planning**: If comparison is detected:
|
||||
1. Identify Target Files: Resolve file names/IDs from the query (e.g., "Q1" -> matches file "2024_Q1_Report.pdf").
|
||||
2. Dimension Extraction: What to compare? (e.g., "summary", "key metrics").
|
||||
3. Execution:
|
||||
- Run Search on File A with query "key metrics".
|
||||
- Run Search on File B with query "key metrics".
|
||||
- Combine context.
|
||||
- **Prompting**: Use a prompt optimized for comparison (e.g., "Generate a comparison table...").
|
||||
|
||||
### 3.2 Frontend (`ChatInterface`)
|
||||
- **UI Trigger**: (Optional) specific "Compare" button, or just natural language.
|
||||
- **Visuals**: Render the response standard markdown (which supports tables).
|
||||
- **Source Attribution**: Ensure citations map back to the correct respective documents.
|
||||
|
||||
## 4. Implementation Steps
|
||||
|
||||
1. **Intent & Entity Extraction (Simple Version)**:
|
||||
- In `RagService`, add a step `detectComparisonIntent(query)`.
|
||||
- Return `subjects: string[]` (approximate filenames) and `dimensions: string`.
|
||||
|
||||
2. **Targeted Search**:
|
||||
- Use `elasticsearchService` to search *specifically* within the resolved file IDs (if we can map names to IDs).
|
||||
- Fall back to broad search if file mapping fails.
|
||||
|
||||
3. **Comparison Prompt**:
|
||||
- Update `rag.service.ts` to use a `comparisonPromise` if intent is detected.
|
||||
|
||||
## 5. Risks & limitations
|
||||
- **File Name Matching**: Mapping user spoken "Q1" to "2024_Q1_Report_Final.pdf" is hard without fuzzy matching or LLM resolution.
|
||||
- *Mitigation*: Use a lightweight LLM call or fuzzy search on the file list to resolve IDs.
|
||||
- **Latency**: Two searches + entity resolution might add latency.
|
||||
- *Mitigation*: Run searches in parallel.
|
||||
|
||||
## 6. MVP Scope
|
||||
- Automated detection of "Compare A and B".
|
||||
- Attempt to identify if A and B refer to specific files in the selected knowledge base.
|
||||
- If identified, restrict search scopes accordingly (or boost them).
|
||||
- Generate a table response.
|
||||
@@ -1,37 +0,0 @@
|
||||
# Feature Design: Highlight Jump (Precise Sourcing)
|
||||
|
||||
## Problem Statement
|
||||
Currently, when a user clicks a citation in the chat, they can see the source text in a drawer or open the PDF. However, the PDF opens to the first page (or just the file) without pinpointing the exact location of the referenced information. This forces the user to manually search for the content.
|
||||
|
||||
## Proposed Solution
|
||||
Implement "Highlight Jump" functionality:
|
||||
1. **Page Jump**: When opening a citation, the PDF viewer should immediately jump to the specific page number containing the chunk.
|
||||
2. **Text Highlighting**: The specific text segment used in the citation should be highlighted visually on the PDF page.
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Frontend
|
||||
|
||||
#### 1. `PDFPreview.tsx`
|
||||
- **Enable Text Layer**: Currently, `PDFPreview` renders only to a `<canvas>`. We must enable `pdf.js` **Text Layer** rendering on top of the canvas. This allows text selection and searching.
|
||||
- **New Props**:
|
||||
- `initialPage`: Already exists? Need to verify it works reliably.
|
||||
- `highlightText`: A string (the chunk content) to search for and highlight.
|
||||
- **Highlight Logic**:
|
||||
- On page load, if `highlightText` is provided, search for this text in the Text Layer.
|
||||
- Apply a visual highlight (e.g., yellow background) to the matching DOM elements in the text layer.
|
||||
- Scroll the highlighted element into view.
|
||||
|
||||
#### 2. `SourcePreviewDrawer.tsx`
|
||||
- Pass the `pageNumber` and `content` (as `highlightText`) to the `onOpenFile` callback.
|
||||
- Update the "Open File" button to trigger this with the correct metadata.
|
||||
|
||||
#### 3. `ChatInterface.tsx` / `ChatView.tsx`
|
||||
- Ensure the state that manages the open PDF preview receives the `pageNumber` and `highlightText` from the source.
|
||||
|
||||
### Backend
|
||||
- **No changes required** if `RagSearchResult` already contains `pageNumber`. (Verified: It does).
|
||||
|
||||
## Limitations
|
||||
- **OCR Files**: If the file was indexed via OCR (images), `pdf.js` might not extract a text layer that matches exactly what Tika extracted, or might have no text layer. In this case, we fallback to just Page Jump.
|
||||
- **Text Mismatch**: If the chunk text is slightly different from the PDF text layer (due to cleaning/normalization during indexing), exact string matching might fail. We will try to match a substring or a fuzzy match if possible, but exact match of the first ~50 chars is a good starting point.
|
||||
@@ -1,52 +0,0 @@
|
||||
# Feature Design: Query Expansion & HyDE Integration
|
||||
|
||||
This document outlines the design for improving search relevance in Lumina using Query Expansion (Multi-Query) and Hypothetical Document Embeddings (HyDE).
|
||||
|
||||
## Problem Statement
|
||||
The current search implementation relies on the user's original query. Simple vector search can sometimes fail to match relevant documents due to:
|
||||
1. **Keyword Mismatch**: The user might use different terminology than the document.
|
||||
2. **Semantic Gap**: The query might be too brief to capture the full semantic context required for a good vector match.
|
||||
|
||||
## Proposed Solution
|
||||
|
||||
### 1. Query Expansion (Multi-Query)
|
||||
We will use an LLM to generate 3 unique variations of the user's query. This helps to:
|
||||
- Capture different facets of the user's intent.
|
||||
- Increase the probability of hitting relevant segments in the knowledge base.
|
||||
|
||||
### 2. HyDE (Hypothetical Document Embeddings)
|
||||
We will use an LLM to generate a brief "hypothetical" answer to the user's query.
|
||||
- Instead of embedding the question, we embed the hypothetical answer.
|
||||
- This often results in better vector matches because we are comparing "answer-like" vectors with "document-like" segments.
|
||||
|
||||
## Technical Implementation
|
||||
|
||||
### Backend Changes
|
||||
|
||||
#### `RagService` (server/src/rag/rag.service.ts)
|
||||
- **New Methods**:
|
||||
- `expandQuery(query: string, userId: string): Promise<string[]>`: Generates 3 variations of the query.
|
||||
- `generateHyDE(query: string, userId: string): Promise<string>`: Generates a hypothetical document.
|
||||
- **Update `searchKnowledge`**:
|
||||
- Add `enableQueryExpansion` and `enableHyDE` parameters.
|
||||
- Implement logic to handle multiple search requests (concurrently) and deduplicate results.
|
||||
|
||||
#### `ChatService` (server/src/chat/chat.service.ts)
|
||||
- Pass the new search options from user settings or request parameters.
|
||||
|
||||
### Frontend Changes
|
||||
|
||||
#### `types.ts` (web/types.ts)
|
||||
- Update `AppSettings` to include `enableQueryExpansion` and `enableHyDE`.
|
||||
|
||||
#### `SettingsDrawer.tsx`
|
||||
- Add UI toggles for these new search enhancement features.
|
||||
|
||||
## Verification Plan
|
||||
|
||||
### Backend Logs
|
||||
- Verify that LLM calls for expansion and HyDE are being made.
|
||||
- Log the generated queries and hypothetical documents for debugging.
|
||||
|
||||
### Manual Verification
|
||||
- Compare search results with and without these features enabled for complex queries.
|
||||
@@ -1,213 +0,0 @@
|
||||
# Docker 混合模式开发指南
|
||||
|
||||
## 概述
|
||||
|
||||
采用 **Docker 运行基础设施 + 本地热重载开发** 的混合模式,解决传统 Docker Compose 全量构建慢、调试不便的问题。
|
||||
|
||||
| 服务 | 运行方式 | 说明 |
|
||||
|------|---------|------|
|
||||
| es (Elasticsearch) | Docker | 基础服务,稳定不动 |
|
||||
| tika (文档解析) | Docker | 基础服务,稳定不动 |
|
||||
| libreoffice (文档转换) | Docker | 基础服务,稳定不动 |
|
||||
| server (NestJS后端) | 本地 `npm run start:dev` | 代码改动秒级热重载 |
|
||||
| web (React前端) | 本地 `npm run dev` | 代码改动秒级热重载 |
|
||||
|
||||
---
|
||||
|
||||
## 先决条件
|
||||
|
||||
- Docker Desktop 已安装并运行
|
||||
- Node.js >= 20
|
||||
- 项目代码已克隆到本地 `D:\AuraK`
|
||||
|
||||
---
|
||||
|
||||
## 启动步骤
|
||||
|
||||
### Step 1: 启动基础设施 (Docker)
|
||||
|
||||
```bash
|
||||
# 进入项目目录
|
||||
cd D:\AuraK
|
||||
|
||||
# 只启动基础设施服务 (es, tika, libreoffice)
|
||||
docker compose up es tika libreoffice -d
|
||||
|
||||
# 确认所有服务正常运行
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
预期输出:
|
||||
```
|
||||
NAME IMAGE STATUS
|
||||
aurak-es elasticsearch:9.2.1 Up (healthy)
|
||||
aurak-tika apache/tika:latest Up
|
||||
aurak-libreoffice aurak-libreoffice Up
|
||||
```
|
||||
|
||||
### Step 2: 启动后端 (本地)
|
||||
|
||||
```bash
|
||||
# 打开新终端
|
||||
cd D:\AuraK\server
|
||||
|
||||
# 确认 .env 中 Docker 服务地址为 127.0.0.1
|
||||
# ELASTICSEARCH_HOST=http://127.0.0.1:9200
|
||||
# TIKA_HOST=http://127.0.0.1:9998
|
||||
# LIBREOFFICE_URL=http://127.0.0.1:8100
|
||||
|
||||
# 安装依赖(首次或依赖变更时)
|
||||
npm install
|
||||
|
||||
# 启动开发模式(热重载)
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
后端启动成功标志:`Nest application successfully started`,监听 `http://localhost:3001`
|
||||
|
||||
### Step 3: 启动前端 (本地)
|
||||
|
||||
```bash
|
||||
# 打开新终端
|
||||
cd D:\AuraK\web
|
||||
|
||||
# 确认 .env 配置
|
||||
# VITE_PORT=13001
|
||||
# VITE_BACKEND_URL=http://localhost:3001
|
||||
|
||||
# 安装依赖(首次或依赖变更时)
|
||||
npm install
|
||||
|
||||
# 启动开发模式(热重载)
|
||||
npm run dev
|
||||
```
|
||||
|
||||
前端启动成功标志:`Local: http://localhost:13001/`
|
||||
|
||||
### Step 4: 访问系统
|
||||
|
||||
打开浏览器访问 `http://localhost:13001`
|
||||
|
||||
---
|
||||
|
||||
## 重启场景
|
||||
|
||||
### 只需重启后端
|
||||
```bash
|
||||
# 在 server/ 终端按 Ctrl+C,然后重新运行
|
||||
npm run start:dev
|
||||
```
|
||||
|
||||
### 只需重启前端
|
||||
```bash
|
||||
# 在 web/ 终端按 Ctrl+C,然后重新运行
|
||||
npm run dev
|
||||
```
|
||||
|
||||
### 只需重启某个 Docker 服务
|
||||
```bash
|
||||
# 重启单个服务
|
||||
docker compose restart es
|
||||
|
||||
# 重启所有 Docker 服务(不重建)
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
### 完全重建 Docker 服务
|
||||
```bash
|
||||
# 停止并移除所有 Docker 容器
|
||||
docker compose down
|
||||
|
||||
# 重新构建并启动
|
||||
docker compose up es tika libreoffice -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 停止
|
||||
|
||||
```bash
|
||||
# 停止 Docker 基础设施
|
||||
cd D:\AuraK
|
||||
docker compose down
|
||||
|
||||
# 分别停止本地进程 (Ctrl+C)
|
||||
# server 终端: Ctrl+C
|
||||
# web 终端: Ctrl+C
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 环境配置参考
|
||||
|
||||
### server/.env (后端)
|
||||
```
|
||||
PORT=3001
|
||||
DATABASE_PATH=./data/metadata.db
|
||||
ELASTICSEARCH_HOST=http://127.0.0.1:9200
|
||||
TIKA_HOST=http://127.0.0.1:9998
|
||||
LIBREOFFICE_URL=http://127.0.0.1:8100
|
||||
JWT_SECRET=123456789
|
||||
UPLOAD_FILE_PATH=./uploads
|
||||
MAX_FILE_SIZE=104857600
|
||||
TEMP_DIR=./temp
|
||||
DEFAULT_VECTOR_DIMENSIONS=2048
|
||||
MAX_CHUNK_SIZE=8191
|
||||
MAX_OVERLAP_SIZE=200
|
||||
MAX_MEMORY_USAGE_MB=1024
|
||||
CHUNK_BATCH_SIZE=100
|
||||
GC_THRESHOLD_MB=800
|
||||
```
|
||||
|
||||
### web/.env (前端)
|
||||
```
|
||||
VITE_API_BASE_URL=/api
|
||||
VITE_ALLOWED_HOSTS=localhost,127.0.0.1,0.0.0.0
|
||||
VITE_PORT=13001
|
||||
VITE_HOST=0.0.0.0
|
||||
VITE_BACKEND_URL=http://localhost:3001
|
||||
VITE_DEFAULT_LANGUAGE=zh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Elasticsearch 内存不足
|
||||
```
|
||||
ES_JAVA_OPTS 已在 docker-compose.yml 中限制为 512MB
|
||||
若仍不足,可修改 docker-compose.yml:
|
||||
- ES_JAVA_OPTS=-Xms256m -Xmx256m
|
||||
```
|
||||
|
||||
### 端口冲突
|
||||
```
|
||||
es:9200 → 本地映射 9200
|
||||
tika:9998 → 本地映射 9998
|
||||
libreoffice:8100 → 本地映射 8100
|
||||
server:3001 → 本地直接启动
|
||||
web:13001 → 本地直接启动
|
||||
```
|
||||
|
||||
### Docker Desktop 未运行
|
||||
```bash
|
||||
# 如果 docker 命令报错,请先启动 Docker Desktop
|
||||
# 或确认 Docker Desktop 已开启 WSL2 集成
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 生产部署
|
||||
|
||||
需要全量 Docker 构建时:
|
||||
```bash
|
||||
# 完整构建所有服务
|
||||
docker compose build
|
||||
|
||||
# 完整启动所有服务(包含 server/web)
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-05-14
|
||||
@@ -1,297 +0,0 @@
|
||||
# 飞书机器人与人才测评集成 - 实现总结
|
||||
|
||||
> **文档版本**: v1.0
|
||||
> **创建日期**: 2026-03-17
|
||||
> **状态**: 实现完成
|
||||
|
||||
---
|
||||
|
||||
## 一、实现概述
|
||||
|
||||
### 已完成的功能
|
||||
1. ✅ **知识库选择机制** - 飞书机器人支持配置特定知识库或知识组
|
||||
2. ✅ **命令解析器** - 支持多种语言的测评命令识别
|
||||
3. ✅ **测评会话管理** - 完整的会话生命周期管理
|
||||
4. ✅ **飞书卡片交互** - 友好的问题展示和结果报告
|
||||
|
||||
### 架构图
|
||||
```
|
||||
用户消息
|
||||
↓
|
||||
FeishuController._handleMessage()
|
||||
↓
|
||||
[命令识别] → 是测评命令? → FeishuAssessmentService.handleCommand()
|
||||
↓ ↓
|
||||
否 命令解析器
|
||||
↓ ↓
|
||||
FeishuService.processChatMessage() 执行对应操作
|
||||
↓ ↓
|
||||
ChatService.streamChat() [开始/回答/状态/结果]
|
||||
↓ ↓
|
||||
RAG搜索 + LLM生成 AssessmentService
|
||||
↓ ↓
|
||||
飞书消息回复 飞书消息回复
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 二、数据库变更
|
||||
|
||||
### 1. FeishuBot 实体新增字段
|
||||
**文件**: `server/src/feishu/entities/feishu-bot.entity.ts`
|
||||
|
||||
```typescript
|
||||
@Column({ name: 'knowledge_base_id', nullable: true, length: 36 })
|
||||
knowledgeBaseId: string;
|
||||
|
||||
@Column({ name: 'knowledge_group_id', nullable: true, length: 36 })
|
||||
knowledgeGroupId: string;
|
||||
```
|
||||
|
||||
### 2. 新增测评会话表
|
||||
**文件**: `server/src/feishu/entities/feishu-assessment-session.entity.ts`
|
||||
|
||||
```sql
|
||||
CREATE TABLE feishu_assessment_sessions (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
bot_id VARCHAR(36) NOT NULL,
|
||||
open_id VARCHAR(255) NOT NULL,
|
||||
assessment_session_id VARCHAR(36) NOT NULL,
|
||||
status ENUM('active', 'completed', 'cancelled') DEFAULT 'active',
|
||||
current_question_index INT DEFAULT 0,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_bot_open (bot_id, open_id),
|
||||
INDEX idx_assessment_session (assessment_session_id),
|
||||
CONSTRAINT fk_feishu_assessment_bot
|
||||
FOREIGN KEY (bot_id)
|
||||
REFERENCES feishu_bots(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
### 3. 迁移脚本
|
||||
- `1773200000000-AddFeishuBotKnowledgeFields.ts` - 添加知识库字段
|
||||
- `1773200000001-CreateFeishuAssessmentSessionTable.ts` - 创建测评会话表
|
||||
|
||||
---
|
||||
|
||||
## 三、核心组件实现
|
||||
|
||||
### 1. 命令解析器 (AssessmentCommandParser)
|
||||
**文件**: `server/src/feishu/services/assessment-command.parser.ts`
|
||||
|
||||
**功能**:
|
||||
- 识别测评命令前缀:`/assessment`, `/测评`, `/eval`, `/测评评估`
|
||||
- 支持多语言命令:start/开始, answer/回答, status/状态, result/结果
|
||||
- 解析命令参数
|
||||
|
||||
**示例**:
|
||||
```typescript
|
||||
const parser = new AssessmentCommandParser();
|
||||
const command = parser.parse('/assessment start kb_xxx');
|
||||
// 结果: { type: 'start', parameters: ['kb_xxx'], ... }
|
||||
```
|
||||
|
||||
### 2. 测评服务 (FeishuAssessmentService)
|
||||
**文件**: `server/src/feishu/services/feishu-assessment.service.ts`
|
||||
|
||||
**核心方法**:
|
||||
- `handleCommand()` - 处理测评命令
|
||||
- `startAssessment()` - 开始测评会话
|
||||
- `submitAnswer()` - 提交答案
|
||||
- `getStatus()` - 获取测评状态
|
||||
- `getResult()` - 获取测评结果
|
||||
- `cancelAssessment()` - 取消测评
|
||||
|
||||
**会话流程**:
|
||||
1. 用户发送 `/assessment start`
|
||||
2. 系统创建测评会话,发送第一个问题卡片
|
||||
3. 用户回复答案(直接回复或 `/assessment answer`)
|
||||
4. 系统评估答案,发送下一个问题
|
||||
5. 重复步骤3-4直到完成
|
||||
6. 系统发送测评结果报告
|
||||
|
||||
### 3. 集成到 FeishuService
|
||||
**文件**: `server/src/feishu/feishu.service.ts`
|
||||
|
||||
**新增方法**:
|
||||
```typescript
|
||||
isAssessmentCommand(message: string): boolean {
|
||||
const trimmed = message.trim().toLowerCase();
|
||||
const commandPrefixes = ['/assessment', '/测评', '/eval', '/测评评估'];
|
||||
return commandPrefixes.some(prefix => trimmed.startsWith(prefix.toLowerCase()));
|
||||
}
|
||||
```
|
||||
|
||||
### 4. 集成到 FeishuController
|
||||
**文件**: `server/src/feishu/feishu.controller.ts`
|
||||
|
||||
**修改 _handleMessage 方法**:
|
||||
```typescript
|
||||
if (this.feishuService.isAssessmentCommand(userText)) {
|
||||
// 委托给测评服务
|
||||
await this.feishuAssessmentService.handleCommand(bot, openId, userText);
|
||||
} else {
|
||||
// 使用默认聊天处理
|
||||
await this.feishuService.processChatMessage(bot, openId, messageId, userText);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、API 接口更新
|
||||
|
||||
### 1. 创建/更新飞书机器人
|
||||
**端点**: `POST /feishu/bots`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"appId": "cli_xxx",
|
||||
"appSecret": "xxx",
|
||||
"botName": "测评机器人",
|
||||
"knowledgeBaseId": "kb_xxx", // 可选:特定知识库
|
||||
"knowledgeGroupId": "group_xxx" // 可选:知识组
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 更新知识库配置
|
||||
**端点**: `PATCH /feishu/bots/:id/knowledge`
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"knowledgeBaseId": "kb_xxx",
|
||||
"knowledgeGroupId": null
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、命令参考
|
||||
|
||||
### 支持的命令前缀
|
||||
- `/assessment`
|
||||
- `/测评`
|
||||
- `/eval`
|
||||
- `/测评评估`
|
||||
|
||||
### 命令列表
|
||||
| 命令 | 参数 | 说明 |
|
||||
|------|------|------|
|
||||
| `start [kbId\|templateId]` | 可选 | 开始测评 |
|
||||
| `answer [answer]` | 必需 | 提交答案 |
|
||||
| `status` | - | 查看状态 |
|
||||
| `result` | - | 获取结果 |
|
||||
| `help` | - | 显示帮助 |
|
||||
| `cancel` | - | 取消测评 |
|
||||
|
||||
### 使用示例
|
||||
```
|
||||
用户: /assessment start
|
||||
系统: [发送第一个问题卡片]
|
||||
|
||||
用户: 这是我的答案
|
||||
系统: [评估答案并发送下一个问题]
|
||||
|
||||
用户: /assessment status
|
||||
系统: 测评状态: 进度 3/10, 状态: 进行中
|
||||
|
||||
用户: /assessment result
|
||||
系统: [发送测评结果报告]
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、文件清单
|
||||
|
||||
### 新增文件
|
||||
1. `server/src/feishu/dto/assessment-command.dto.ts` - 命令DTO
|
||||
2. `server/src/feishu/entities/feishu-assessment-session.entity.ts` - 测评会话实体
|
||||
3. `server/src/feishu/services/assessment-command.parser.ts` - 命令解析器
|
||||
4. `server/src/feishu/services/feishu-assessment.service.ts` - 测评服务
|
||||
5. `server/src/migrations/1773200000000-AddFeishuBotKnowledgeFields.ts` - 迁移脚本1
|
||||
6. `server/src/migrations/1773200000001-CreateFeishuAssessmentSessionTable.ts` - 迁移脚本2
|
||||
|
||||
### 修改文件
|
||||
1. `server/src/feishu/entities/feishu-bot.entity.ts` - 添加知识库字段
|
||||
2. `server/src/feishu/dto/create-bot.dto.ts` - 添加知识库配置字段
|
||||
3. `server/src/feishu/feishu.service.ts` - 添加命令识别方法
|
||||
4. `server/src/feishu/feishu.controller.ts` - 集成测评服务
|
||||
5. `server/src/feishu/feishu.module.ts` - 注册新服务
|
||||
|
||||
---
|
||||
|
||||
## 七、实施步骤
|
||||
|
||||
### 阶段 1: 基础架构 ✅
|
||||
- [x] 创建数据库迁移脚本
|
||||
- [x] 更新 FeishuBot 实体和 DTO
|
||||
- [x] 修改 FeishuService 支持知识库选择
|
||||
|
||||
### 阶段 2: 测评集成 ✅
|
||||
- [x] 创建测评会话实体
|
||||
- [x] 实现命令解析器
|
||||
- [x] 实现测评服务
|
||||
- [x] 集成到 FeishuService 和 Controller
|
||||
|
||||
### 阶段 3: 测试优化 ⏳
|
||||
- [ ] 运行数据库迁移
|
||||
- [ ] 测试命令解析功能
|
||||
- [ ] 测试完整测评流程
|
||||
- [ ] 性能测试和优化
|
||||
|
||||
---
|
||||
|
||||
## 八、安全考虑
|
||||
|
||||
1. **多租户隔离**:所有查询包含 `userId` 和 `tenantId` 过滤
|
||||
2. **命令验证**:白名单命令验证,防止注入攻击
|
||||
3. **会话超时**:建议设置测评会话超时时间(如 24 小时)
|
||||
4. **数据隐私**:测评结果仅对授权用户可见
|
||||
|
||||
---
|
||||
|
||||
## 九、下一步工作
|
||||
|
||||
1. **运行数据库迁移**
|
||||
```bash
|
||||
yarn migration:run
|
||||
```
|
||||
|
||||
2. **测试命令解析**
|
||||
```bash
|
||||
node test-feishu-assessment.js
|
||||
```
|
||||
|
||||
3. **集成测试**
|
||||
- 创建飞书机器人
|
||||
- 配置知识库
|
||||
- 发送 `/assessment start` 命令
|
||||
- 完成测评流程
|
||||
|
||||
4. **文档完善**
|
||||
- 更新用户使用文档
|
||||
- 添加 API 文档
|
||||
- 编写故障排除指南
|
||||
|
||||
---
|
||||
|
||||
## 十、总结
|
||||
|
||||
本次实现完成了飞书机器人与人才测评的完整集成:
|
||||
|
||||
1. **知识库选择**:飞书机器人现在可以配置特定知识库或知识组
|
||||
2. **命令解析**:支持多语言的测评命令识别
|
||||
3. **会话管理**:完整的测评会话生命周期管理
|
||||
4. **交互体验**:友好的飞书卡片交互
|
||||
|
||||
系统架构清晰,代码结构良好,易于维护和扩展。
|
||||
|
||||
---
|
||||
|
||||
**相关文档**:
|
||||
- [完整设计文档](./feishu-assessment-integration-design.md)
|
||||
- [设计摘要](./feishu-assessment-integration-summary.md)
|
||||
- [快速参考](./QUICK-REFERENCE.md)
|
||||
@@ -1,1341 +0,0 @@
|
||||
# 飞书机器人与人才测评集成设计文档
|
||||
|
||||
> **文档版本**: v1.0
|
||||
> **创建日期**: 2026-03-17
|
||||
> **作者**: AI Assistant
|
||||
> **状态**: Draft
|
||||
|
||||
---
|
||||
|
||||
## 目录
|
||||
|
||||
1. [概述](#概述)
|
||||
2. [现状分析](#现状分析)
|
||||
3. [需求分析](#需求分析)
|
||||
4. [详细设计方案](#详细设计方案)
|
||||
5. [API 接口设计](#api-接口设计)
|
||||
6. [数据库设计](#数据库设计)
|
||||
7. [实施计划](#实施计划)
|
||||
8. [安全考虑](#安全考虑)
|
||||
9. [附录](#附录)
|
||||
|
||||
---
|
||||
|
||||
## 概述
|
||||
|
||||
### 背景
|
||||
本项目是一个基于 RAG(检索增强生成)的问答系统,支持多知识库管理。飞书机器人作为外部接入点,目前与聊天系统集成,但知识库选择机制不明确。用户希望:
|
||||
1. 明确飞书机器人当前对接的知识库
|
||||
2. 将飞书机器人与人才测评模块集成
|
||||
|
||||
### 设计目标
|
||||
- 明确飞书机器人的知识库选择机制
|
||||
- 实现飞书机器人与人才测评的完整集成
|
||||
- 保持多租户隔离和系统安全性
|
||||
- 提供友好的用户交互体验
|
||||
|
||||
---
|
||||
|
||||
## 现状分析
|
||||
|
||||
### 1. 飞书机器人知识库对接现状
|
||||
|
||||
#### 当前实现位置
|
||||
- **主服务**: `D:\aura\AuraK\server\src\feishu\feishu.service.ts`
|
||||
- **控制器**: `D:\aura\AuraK\server\src\feishu\feishu.controller.ts`
|
||||
- **WebSocket 管理**: `D:\aura\AuraK\server\src\feishu\feishu-ws.manager.ts`
|
||||
|
||||
#### 集成方式
|
||||
飞书机器人通过以下两种方式接收消息:
|
||||
1. **Webhook**:飞书开放平台推送事件
|
||||
2. **WebSocket**:实时消息推送(推荐,性能更好)
|
||||
|
||||
#### 知识库选择逻辑(关键代码)
|
||||
```typescript
|
||||
// feishu.service.ts (line 311-331)
|
||||
const stream = this.chatService.streamChat(
|
||||
userMessage,
|
||||
[],
|
||||
userId,
|
||||
llmModel as any,
|
||||
language,
|
||||
undefined, // selectedEmbeddingId - 未指定
|
||||
undefined, // selectedGroups - 未指定
|
||||
undefined, // selectedFiles - 未指定 ← 关键点
|
||||
undefined, // historyId
|
||||
false, // enableRerank
|
||||
// ... 其他参数
|
||||
tenantId,
|
||||
);
|
||||
```
|
||||
|
||||
**结论**:飞书机器人当前使用**默认知识库**(用户的所有文件),因为 `selectedFiles` 和 `selectedGroups` 都是 `undefined`。
|
||||
|
||||
#### 数据库实体
|
||||
```typescript
|
||||
// feishu-bot.entity.ts
|
||||
@Entity('feishu_bots')
|
||||
export class FeishuBot {
|
||||
id: string;
|
||||
userId: string;
|
||||
appId: string;
|
||||
appSecret: string;
|
||||
botName?: string;
|
||||
enabled: boolean;
|
||||
isDefault: boolean;
|
||||
useWebSocket: boolean;
|
||||
// ❌ 缺少知识库配置字段
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 人才测评模块现状
|
||||
|
||||
#### 模块位置
|
||||
- **主服务**: `D:\aura\AuraK\server\src\assessment\assessment.service.ts`
|
||||
- **控制器**: `D:\aura\AuraK\server\src\assessment\assessment.controller.ts`
|
||||
- **实体**: `D:\aura\AuraK\server\src\assessment\entities\`
|
||||
|
||||
#### 核心功能
|
||||
1. **会话管理**:创建、查询、删除测评会话
|
||||
2. **问题生成**:基于知识库内容生成测评问题
|
||||
3. **问答交互**:用户回答问题,系统评估并生成下一个问题
|
||||
4. **报告生成**:测评完成后生成详细报告和评分
|
||||
5. **流式支持**:实时更新测评进度
|
||||
|
||||
#### 关键接口
|
||||
```typescript
|
||||
// assessment.controller.ts
|
||||
POST /assessment/start // 开始测评会话
|
||||
POST /assessment/:id/answer // 提交答案
|
||||
SSE /assessment/:id/start-stream // 流式获取初始问题
|
||||
SSE /assessment/:id/answer-stream // 流式获取评估结果
|
||||
GET /assessment/:id/state // 获取会话状态
|
||||
GET /assessment // 获取历史记录
|
||||
```
|
||||
|
||||
#### 集成点
|
||||
- 使用 `KnowledgeBaseService` 和 `KnowledgeGroupService` 获取内容
|
||||
- 使用 `RagService` 进行混合搜索
|
||||
- 使用 `ChatService` 进行 LLM 交互
|
||||
- 使用 LangGraph 构建评估图算法
|
||||
|
||||
---
|
||||
|
||||
## 需求分析
|
||||
|
||||
### 用户需求
|
||||
1. **明确知识库选择机制**
|
||||
- 飞书机器人当前对接哪个知识库?
|
||||
- 如何配置飞书机器人使用特定知识库?
|
||||
|
||||
2. **飞书机器人与人才测评集成**
|
||||
- 通过飞书机器人启动测评
|
||||
- 通过飞书机器人回答测评问题
|
||||
- 通过飞书机器人获取测评结果
|
||||
|
||||
### 功能需求
|
||||
1. **知识库配置功能**
|
||||
- 支持为每个飞书机器人配置特定知识库或知识组
|
||||
- 支持动态切换知识库
|
||||
|
||||
2. **测评命令支持**
|
||||
- `/assessment start [kbId|templateId]` - 开始测评
|
||||
- `/assessment answer [answer]` - 回答问题
|
||||
- `/assessment status` - 查看状态
|
||||
- `/assessment result` - 获取结果
|
||||
|
||||
3. **交互体验优化**
|
||||
- 使用飞书卡片展示问题
|
||||
- 实时更新测评进度
|
||||
- 友好的错误提示
|
||||
|
||||
### 非功能需求
|
||||
1. **安全性**:多租户隔离,防止越权访问
|
||||
2. **性能**:WebSocket 实时推送,避免超时
|
||||
3. **可扩展性**:支持未来新增测评类型
|
||||
4. **兼容性**:不影响现有聊天功能
|
||||
|
||||
---
|
||||
|
||||
## 详细设计方案
|
||||
|
||||
### 方案 1:飞书机器人知识库选择机制
|
||||
|
||||
#### 设计思路
|
||||
在 `FeishuBot` 实体中增加知识库配置字段,支持以下模式:
|
||||
1. **默认模式**:使用用户所有文件(当前行为)
|
||||
2. **特定知识库**:只搜索指定知识库的文件
|
||||
3. **知识组**:搜索知识组下的所有文件
|
||||
|
||||
#### 数据库变更
|
||||
|
||||
##### 1.1 新增字段到 FeishuBot 实体
|
||||
```typescript
|
||||
// D:\aura\AuraK\server\src\feishu\entities\feishu-bot.entity.ts
|
||||
|
||||
@Entity('feishu_bots')
|
||||
export class FeishuBot {
|
||||
// ... 现有字段保持不变
|
||||
|
||||
@Column({ name: 'knowledge_base_id', nullable: true, length: 36 })
|
||||
knowledgeBaseId: string;
|
||||
|
||||
@Column({ name: 'knowledge_group_id', nullable: true, length: 36 })
|
||||
knowledgeGroupId: string;
|
||||
|
||||
@ManyToOne(() => KnowledgeBase, { onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'knowledge_base_id' })
|
||||
knowledgeBase?: KnowledgeBase;
|
||||
|
||||
@ManyToOne(() => KnowledgeGroup, { onDelete: 'SET NULL' })
|
||||
@JoinColumn({ name: 'knowledge_group_id' })
|
||||
knowledgeGroup?: KnowledgeGroup;
|
||||
}
|
||||
```
|
||||
|
||||
##### 1.2 创建数据库迁移
|
||||
```typescript
|
||||
// D:\aura\AuraK\server\src\migrations\XXXXXX-AddFeishuBotKnowledgeFields.ts
|
||||
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class AddFeishuBotKnowledgeFieldsXXXXXX implements MigrationInterface {
|
||||
name = 'AddFeishuBotKnowledgeFields';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE feishu_bots
|
||||
ADD COLUMN knowledge_base_id VARCHAR(36) NULL,
|
||||
ADD COLUMN knowledge_group_id VARCHAR(36) NULL,
|
||||
ADD CONSTRAINT fk_feishu_bot_knowledge_base
|
||||
FOREIGN KEY (knowledge_base_id)
|
||||
REFERENCES knowledge_bases(id)
|
||||
ON DELETE SET NULL,
|
||||
ADD CONSTRAINT fk_feishu_bot_knowledge_group
|
||||
FOREIGN KEY (knowledge_group_id)
|
||||
REFERENCES knowledge_groups(id)
|
||||
ON DELETE SET NULL;
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE feishu_bots
|
||||
DROP FOREIGN KEY fk_feishu_bot_knowledge_base,
|
||||
DROP FOREIGN KEY fk_feishu_bot_knowledge_group,
|
||||
DROP COLUMN knowledge_base_id,
|
||||
DROP COLUMN knowledge_group_id;
|
||||
`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 更新 DTO
|
||||
```typescript
|
||||
// D:\aura\AuraK\server\src\feishu\dto\create-bot.dto.ts
|
||||
|
||||
export class CreateFeishuBotDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
appId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
appSecret: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
botName?: string;
|
||||
|
||||
// 新增知识库配置字段
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
knowledgeBaseId?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
knowledgeGroupId?: string;
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.4 修改 FeishuService
|
||||
|
||||
##### 1.4.1 更新创建机器人方法
|
||||
```typescript
|
||||
// feishu.service.ts
|
||||
|
||||
async createBot(userId: string, dto: CreateFeishuBotDto): Promise<FeishuBot> {
|
||||
const existing = await this.botRepository.findOne({
|
||||
where: { userId, appId: dto.appId },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
Object.assign(existing, dto);
|
||||
return this.botRepository.save(existing);
|
||||
}
|
||||
|
||||
const bot = this.botRepository.create({ userId, ...dto });
|
||||
return this.botRepository.save(bot);
|
||||
}
|
||||
```
|
||||
|
||||
##### 1.4.2 修改消息处理逻辑
|
||||
```typescript
|
||||
// feishu.service.ts
|
||||
|
||||
async processChatMessage(
|
||||
bot: FeishuBot,
|
||||
openId: string,
|
||||
messageId: string,
|
||||
userMessage: string,
|
||||
): Promise<void> {
|
||||
// ... 前面的代码保持不变
|
||||
|
||||
// 确定搜索范围
|
||||
let selectedFiles: string[] | undefined;
|
||||
let selectedGroups: string[] | undefined;
|
||||
|
||||
// 如果配置了特定知识库,获取该知识库的文件ID
|
||||
if (bot.knowledgeBaseId) {
|
||||
selectedFiles = await this.getFilesByKnowledgeBase(
|
||||
bot.knowledgeBaseId,
|
||||
userId,
|
||||
tenantId
|
||||
);
|
||||
}
|
||||
// 如果配置了知识组,使用知识组
|
||||
else if (bot.knowledgeGroupId) {
|
||||
selectedGroups = [bot.knowledgeGroupId];
|
||||
}
|
||||
// 否则使用默认(所有文件)
|
||||
|
||||
const stream = this.chatService.streamChat(
|
||||
userMessage,
|
||||
[],
|
||||
userId,
|
||||
llmModel as any,
|
||||
language,
|
||||
undefined, // selectedEmbeddingId
|
||||
selectedGroups, // 改为使用配置的知识组
|
||||
selectedFiles, // 改为使用配置的知识库文件
|
||||
undefined, // historyId
|
||||
false, // enableRerank
|
||||
undefined, // selectedRerankId
|
||||
undefined, // temperature
|
||||
undefined, // maxTokens
|
||||
10, // topK
|
||||
0.7, // similarityThreshold
|
||||
undefined, // rerankSimilarityThreshold
|
||||
undefined, // enableQueryExpansion
|
||||
undefined, // enableHyDE
|
||||
tenantId,
|
||||
);
|
||||
|
||||
// ... 后续处理保持不变
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取知识库下的所有文件ID
|
||||
*/
|
||||
private async getFilesByKnowledgeBase(
|
||||
knowledgeBaseId: string,
|
||||
userId: string,
|
||||
tenantId: string
|
||||
): Promise<string[]> {
|
||||
try {
|
||||
// 调用 KnowledgeBaseService 获取文件列表
|
||||
const kb = await this.knowledgeBaseService.findOne(knowledgeBaseId, userId, tenantId);
|
||||
if (!kb) {
|
||||
this.logger.warn(`Knowledge base not found: ${knowledgeBaseId}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
// 假设 KnowledgeBase 有 files 字段或通过关联表获取
|
||||
// 这里需要根据实际的 KnowledgeBase 实体结构调整
|
||||
return kb.files?.map(f => f.id) || [];
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to get files from knowledge base: ${knowledgeBaseId}`, error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 方案 2:飞书机器人与人才测评集成
|
||||
|
||||
#### 设计思路
|
||||
通过自然语言命令触发测评功能,支持以下场景:
|
||||
1. 用户发送 `/assessment start` 启动测评
|
||||
2. 系统发送问题卡片
|
||||
3. 用户回复答案
|
||||
4. 系统评估并发送下一个问题
|
||||
5. 测评完成发送结果报告
|
||||
|
||||
#### 2.1 命令解析机制
|
||||
|
||||
##### 2.1.1 命令类型定义
|
||||
```typescript
|
||||
// D:\aura\AuraK\server\src\feishu\dto\assessment-command.dto.ts
|
||||
|
||||
export enum AssessmentCommandType {
|
||||
START = 'start',
|
||||
ANSWER = 'answer',
|
||||
STATUS = 'status',
|
||||
RESULT = 'result',
|
||||
HELP = 'help',
|
||||
}
|
||||
|
||||
export interface AssessmentCommand {
|
||||
type: AssessmentCommandType;
|
||||
parameters: string[];
|
||||
rawMessage: string;
|
||||
}
|
||||
```
|
||||
|
||||
##### 2.1.2 命令解析器
|
||||
```typescript
|
||||
// D:\aura\AuraK\server\src\feishu\services\assessment-command.parser.ts
|
||||
|
||||
@Injectable()
|
||||
export class AssessmentCommandParser {
|
||||
private readonly commandPrefixes = ['/assessment', '/测评', '/eval'];
|
||||
|
||||
parse(message: string): AssessmentCommand | null {
|
||||
const trimmed = message.trim();
|
||||
|
||||
// 检查是否是测评命令
|
||||
const isCommand = this.commandPrefixes.some(prefix =>
|
||||
trimmed.toLowerCase().startsWith(prefix)
|
||||
);
|
||||
|
||||
if (!isCommand) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// 解析命令
|
||||
const parts = trimmed.split(/\s+/);
|
||||
const commandType = parts[1]?.toLowerCase();
|
||||
|
||||
switch (commandType) {
|
||||
case 'start':
|
||||
return {
|
||||
type: AssessmentCommandType.START,
|
||||
parameters: parts.slice(2),
|
||||
rawMessage: message,
|
||||
};
|
||||
case 'answer':
|
||||
return {
|
||||
type: AssessmentCommandType.ANSWER,
|
||||
parameters: [parts.slice(2).join(' ')],
|
||||
rawMessage: message,
|
||||
};
|
||||
case 'status':
|
||||
return {
|
||||
type: AssessmentCommandType.STATUS,
|
||||
parameters: [],
|
||||
rawMessage: message,
|
||||
};
|
||||
case 'result':
|
||||
return {
|
||||
type: AssessmentCommandType.RESULT,
|
||||
parameters: [],
|
||||
rawMessage: message,
|
||||
};
|
||||
case 'help':
|
||||
case '?':
|
||||
return {
|
||||
type: AssessmentCommandType.HELP,
|
||||
parameters: [],
|
||||
rawMessage: message,
|
||||
};
|
||||
default:
|
||||
return {
|
||||
type: AssessmentCommandType.HELP,
|
||||
parameters: [],
|
||||
rawMessage: message,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 测评会话管理
|
||||
|
||||
##### 2.2.1 数据库实体
|
||||
```typescript
|
||||
// D:\aura\AuraK\server\src\feishu\entities\feishu-assessment-session.entity.ts
|
||||
|
||||
@Entity('feishu_assessment_sessions')
|
||||
export class FeishuAssessmentSession {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'bot_id' })
|
||||
botId: string;
|
||||
|
||||
@Column({ name: 'open_id' })
|
||||
openId: string;
|
||||
|
||||
@Column({ name: 'assessment_session_id' })
|
||||
assessmentSessionId: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: ['active', 'completed', 'cancelled'],
|
||||
default: 'active'
|
||||
})
|
||||
status: 'active' | 'completed' | 'cancelled';
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
|
||||
// 关联关系
|
||||
@ManyToOne(() => FeishuBot, { onDelete: 'CASCADE' })
|
||||
@JoinColumn({ name: 'bot_id' })
|
||||
bot: FeishuBot;
|
||||
}
|
||||
```
|
||||
|
||||
##### 2.2.2 迁移脚本
|
||||
```typescript
|
||||
// D:\aura\AuraK\server\src\migrations\XXXXXX-CreateFeishuAssessmentSessionTable.ts
|
||||
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class CreateFeishuAssessmentSessionTableXXXXXX implements MigrationInterface {
|
||||
name = 'CreateFeishuAssessmentSessionTable';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
CREATE TABLE feishu_assessment_sessions (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
bot_id VARCHAR(36) NOT NULL,
|
||||
open_id VARCHAR(255) NOT NULL,
|
||||
assessment_session_id VARCHAR(36) NOT NULL,
|
||||
status ENUM('active', 'completed', 'cancelled') DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_bot_open (bot_id, open_id),
|
||||
INDEX idx_assessment_session (assessment_session_id),
|
||||
CONSTRAINT fk_feishu_assessment_bot
|
||||
FOREIGN KEY (bot_id)
|
||||
REFERENCES feishu_bots(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`
|
||||
DROP TABLE feishu_assessment_sessions;
|
||||
`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 服务层实现
|
||||
|
||||
##### 2.3.1 飞书测评服务
|
||||
```typescript
|
||||
// D:\aura\AuraK\server\src\feishu\services\feishu-assessment.service.ts
|
||||
|
||||
@Injectable()
|
||||
export class FeishuAssessmentService {
|
||||
private readonly logger = new Logger(FeishuAssessmentService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(FeishuAssessmentSession)
|
||||
private sessionRepository: Repository<FeishuAssessmentSession>,
|
||||
private assessmentService: AssessmentService,
|
||||
private feishuService: FeishuService,
|
||||
private commandParser: AssessmentCommandParser,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 处理测评命令
|
||||
*/
|
||||
async handleCommand(
|
||||
bot: FeishuBot,
|
||||
openId: string,
|
||||
message: string,
|
||||
): Promise<void> {
|
||||
const command = this.commandParser.parse(message);
|
||||
|
||||
if (!command) {
|
||||
// 不是测评命令,使用默认聊天处理
|
||||
await this.feishuService.processChatMessage(bot, openId, '', message);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
switch (command.type) {
|
||||
case AssessmentCommandType.START:
|
||||
await this.startAssessment(bot, openId, command.parameters);
|
||||
break;
|
||||
case AssessmentCommandType.ANSWER:
|
||||
await this.submitAnswer(bot, openId, command.parameters[0]);
|
||||
break;
|
||||
case AssessmentCommandType.STATUS:
|
||||
await this.getStatus(bot, openId);
|
||||
break;
|
||||
case AssessmentCommandType.RESULT:
|
||||
await this.getResult(bot, openId);
|
||||
break;
|
||||
case AssessmentCommandType.HELP:
|
||||
await this.sendHelp(bot, openId);
|
||||
break;
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to handle assessment command: ${error.message}`, error);
|
||||
await this.feishuService.sendTextMessage(
|
||||
bot,
|
||||
'open_id',
|
||||
openId,
|
||||
`处理测评命令时出错: ${error.message}`
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始测评
|
||||
*/
|
||||
async startAssessment(
|
||||
bot: FeishuBot,
|
||||
openId: string,
|
||||
parameters: string[],
|
||||
): Promise<void> {
|
||||
// 检查是否已有进行中的测评
|
||||
const existingSession = await this.getActiveSession(bot.id, openId);
|
||||
if (existingSession) {
|
||||
await this.feishuService.sendTextMessage(
|
||||
bot,
|
||||
'open_id',
|
||||
openId,
|
||||
'您已有进行中的测评会话,请先完成当前测评。'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析参数
|
||||
const [kbIdOrTemplateId, secondParam] = parameters;
|
||||
let knowledgeBaseId: string | undefined;
|
||||
let templateId: string | undefined;
|
||||
|
||||
// 判断是知识库ID还是模板ID
|
||||
if (kbIdOrTemplateId) {
|
||||
// 这里可以根据实际需求判断参数类型
|
||||
// 简单实现:如果参数是UUID格式,假设是模板ID
|
||||
if (kbIdOrTemplateId.length === 36) {
|
||||
templateId = kbIdOrTemplateId;
|
||||
} else {
|
||||
// 否则尝试作为知识库ID
|
||||
knowledgeBaseId = kbIdOrTemplateId;
|
||||
}
|
||||
}
|
||||
|
||||
// 使用机器人配置的知识库(如果未指定)
|
||||
if (!knowledgeBaseId && !templateId && bot.knowledgeBaseId) {
|
||||
knowledgeBaseId = bot.knowledgeBaseId;
|
||||
}
|
||||
|
||||
this.logger.log(`Starting assessment: bot=${bot.id}, openId=${openId}, kb=${knowledgeBaseId}, template=${templateId}`);
|
||||
|
||||
// 创建测评会话
|
||||
const session = await this.assessmentService.startSession(
|
||||
bot.userId,
|
||||
knowledgeBaseId,
|
||||
bot.user?.tenantId || 'default',
|
||||
'zh',
|
||||
templateId,
|
||||
);
|
||||
|
||||
// 存储飞书会话关联
|
||||
const feishuSession = this.sessionRepository.create({
|
||||
botId: bot.id,
|
||||
openId,
|
||||
assessmentSessionId: session.id,
|
||||
status: 'active',
|
||||
});
|
||||
await this.sessionRepository.save(feishuSession);
|
||||
|
||||
// 发送第一个问题
|
||||
if (session.questions_json && session.questions_json.length > 0) {
|
||||
const firstQuestion = session.questions_json[0];
|
||||
const card = this.buildQuestionCard(firstQuestion, session.id, 1, session.questions_json.length);
|
||||
await this.feishuService.sendCardMessage(bot, 'open_id', openId, card);
|
||||
} else {
|
||||
await this.feishuService.sendTextMessage(
|
||||
bot,
|
||||
'open_id',
|
||||
openId,
|
||||
'测评会话已创建,但未能生成问题。'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交答案
|
||||
*/
|
||||
async submitAnswer(
|
||||
bot: FeishuBot,
|
||||
openId: string,
|
||||
answer: string,
|
||||
): Promise<void> {
|
||||
const session = await this.getActiveSession(bot.id, openId);
|
||||
|
||||
if (!session) {
|
||||
await this.feishuService.sendTextMessage(
|
||||
bot,
|
||||
'open_id',
|
||||
openId,
|
||||
'没有进行中的测评会话。请发送 /assessment start 开始测评。'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
this.logger.log(`Submitting answer for session ${session.assessmentSessionId}`);
|
||||
|
||||
// 提交答案到测评服务
|
||||
const result = await this.assessmentService.submitAnswer(
|
||||
session.assessmentSessionId,
|
||||
bot.userId,
|
||||
answer,
|
||||
'zh',
|
||||
);
|
||||
|
||||
// 更新飞书会话状态
|
||||
if (result.report) {
|
||||
session.status = 'completed';
|
||||
await this.sessionRepository.save(session);
|
||||
|
||||
// 发送测评结果
|
||||
await this.sendAssessmentResult(bot, openId, result);
|
||||
} else if (result.questions && result.questions.length > 0) {
|
||||
// 发送下一个问题
|
||||
const currentQuestionIndex = result.currentQuestionIndex || 0;
|
||||
const nextQuestion = result.questions[currentQuestionIndex];
|
||||
const totalQuestions = result.questions.length;
|
||||
|
||||
const card = this.buildQuestionCard(
|
||||
nextQuestion,
|
||||
session.assessmentSessionId,
|
||||
currentQuestionIndex + 1,
|
||||
totalQuestions
|
||||
);
|
||||
await this.feishuService.sendCardMessage(bot, 'open_id', openId, card);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测评状态
|
||||
*/
|
||||
async getStatus(bot: FeishuBot, openId: string): Promise<void> {
|
||||
const session = await this.getActiveSession(bot.id, openId);
|
||||
|
||||
if (!session) {
|
||||
await this.feishuService.sendTextMessage(
|
||||
bot,
|
||||
'open_id',
|
||||
openId,
|
||||
'没有进行中的测评会话。'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const assessmentState = await this.assessmentService.getSessionState(
|
||||
session.assessmentSessionId,
|
||||
bot.userId
|
||||
);
|
||||
|
||||
const currentQuestionIndex = assessmentState.currentQuestionIndex || 0;
|
||||
const totalQuestions = assessmentState.questions?.length || 0;
|
||||
|
||||
const message = `测评状态:\n` +
|
||||
`- 进度: ${currentQuestionIndex + 1}/${totalQuestions}\n` +
|
||||
`- 状态: ${session.status}\n` +
|
||||
`- 开始时间: ${session.createdAt.toLocaleString('zh-CN')}`;
|
||||
|
||||
await this.feishuService.sendTextMessage(bot, 'open_id', openId, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取测评结果
|
||||
*/
|
||||
async getResult(bot: FeishuBot, openId: string): Promise<void> {
|
||||
const session = await this.getActiveSession(bot.id, openId);
|
||||
|
||||
if (!session) {
|
||||
await this.feishuService.sendTextMessage(
|
||||
bot,
|
||||
'open_id',
|
||||
openId,
|
||||
'没有进行中的测评会话。'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (session.status !== 'completed') {
|
||||
await this.feishuService.sendTextMessage(
|
||||
bot,
|
||||
'open_id',
|
||||
openId,
|
||||
'测评尚未完成,请先完成所有问题。'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
const assessmentState = await this.assessmentService.getSessionState(
|
||||
session.assessmentSessionId,
|
||||
bot.userId
|
||||
);
|
||||
|
||||
await this.sendAssessmentResult(bot, openId, assessmentState);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送帮助信息
|
||||
*/
|
||||
async sendHelp(bot: FeishuBot, openId: string): Promise<void> {
|
||||
const helpText = `
|
||||
**人才测评机器人帮助**
|
||||
|
||||
命令格式:
|
||||
- `/assessment start [kbId|templateId]` - 开始测评
|
||||
- `/assessment answer [answer]` - 提交答案
|
||||
- `/assessment status` - 查看测评状态
|
||||
- `/assessment result` - 获取测评结果
|
||||
- `/assessment help` - 显示帮助
|
||||
|
||||
说明:
|
||||
- 如果未指定知识库/模板,将使用机器人配置的默认知识库
|
||||
- 也可直接回复答案,无需命令前缀
|
||||
`.trim();
|
||||
|
||||
await this.feishuService.sendTextMessage(bot, 'open_id', openId, helpText);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取活跃会话
|
||||
*/
|
||||
private async getActiveSession(
|
||||
botId: string,
|
||||
openId: string,
|
||||
): Promise<FeishuAssessmentSession | null> {
|
||||
return this.sessionRepository.findOne({
|
||||
where: {
|
||||
botId,
|
||||
openId,
|
||||
status: 'active',
|
||||
},
|
||||
order: { createdAt: 'DESC' },
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建问题卡片
|
||||
*/
|
||||
private buildQuestionCard(
|
||||
question: any,
|
||||
sessionId: string,
|
||||
currentIndex: number,
|
||||
totalQuestions: number,
|
||||
): any {
|
||||
return {
|
||||
config: { wide_screen_mode: true },
|
||||
header: {
|
||||
template: 'blue',
|
||||
title: {
|
||||
content: `人才测评 (${currentIndex}/${totalQuestions})`,
|
||||
tag: 'plain_text',
|
||||
},
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
content: `**问题 ${currentIndex}:** ${question.text || question.content}`,
|
||||
tag: 'lark_md',
|
||||
},
|
||||
},
|
||||
...(question.options ? [
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
content: `选项:\n${question.options.map((opt: string, i: number) =>
|
||||
`${String.fromCharCode(65 + i)}. ${opt}`
|
||||
).join('\n')}`,
|
||||
tag: 'lark_md',
|
||||
},
|
||||
}
|
||||
] : []),
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
content: `难度: ${question.difficulty || '普通'} | 分值: ${question.score || 1}`,
|
||||
tag: 'lark_md',
|
||||
},
|
||||
},
|
||||
{
|
||||
tag: 'hr',
|
||||
},
|
||||
{
|
||||
tag: 'note',
|
||||
elements: [
|
||||
{
|
||||
content: `直接回复答案或使用 /assessment answer [你的答案]`,
|
||||
tag: 'plain_text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送测评结果
|
||||
*/
|
||||
private async sendAssessmentResult(
|
||||
bot: FeishuBot,
|
||||
openId: string,
|
||||
result: any,
|
||||
): Promise<void> {
|
||||
const report = result.report || result.finalReport;
|
||||
const score = result.finalScore || result.score;
|
||||
|
||||
const resultCard = {
|
||||
config: { wide_screen_mode: true },
|
||||
header: {
|
||||
template: 'green',
|
||||
title: {
|
||||
content: '测评完成',
|
||||
tag: 'plain_text',
|
||||
},
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
content: `**测评结果**`,
|
||||
tag: 'lark_md',
|
||||
},
|
||||
},
|
||||
...(score !== undefined ? [
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
content: `**总分**: ${score.toFixed(1)}`,
|
||||
tag: 'lark_md',
|
||||
},
|
||||
}
|
||||
] : []),
|
||||
...(report ? [
|
||||
{
|
||||
tag: 'div',
|
||||
text: {
|
||||
content: `**报告**:\n${report}`,
|
||||
tag: 'lark_md',
|
||||
},
|
||||
}
|
||||
] : []),
|
||||
{
|
||||
tag: 'hr',
|
||||
},
|
||||
{
|
||||
tag: 'note',
|
||||
elements: [
|
||||
{
|
||||
content: `发送 /assessment start 开始新的测评`,
|
||||
tag: 'plain_text',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
await this.feishuService.sendCardMessage(bot, 'open_id', openId, resultCard);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.4 集成到 FeishuService
|
||||
|
||||
##### 2.4.1 修改消息处理
|
||||
```typescript
|
||||
// feishu.service.ts
|
||||
|
||||
// 新增字段
|
||||
private feishuAssessmentService: FeishuAssessmentService;
|
||||
|
||||
// 在构造函数后初始化
|
||||
setFeishuAssessmentService(service: FeishuAssessmentService): void {
|
||||
this.feishuAssessmentService = service;
|
||||
}
|
||||
|
||||
// 修改 _handleMessage 方法
|
||||
private async _handleMessage(bot: any, event: any): Promise<void> {
|
||||
const message = event?.message;
|
||||
if (!message) return;
|
||||
|
||||
const messageId = message.message_id;
|
||||
const openId = event?.sender?.sender_id?.open_id;
|
||||
|
||||
if (!openId) {
|
||||
this.logger.warn('No sender open_id found in Feishu event');
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析文本内容
|
||||
let userText = '';
|
||||
try {
|
||||
const content = JSON.parse(message.content || '{}');
|
||||
userText = content.text || '';
|
||||
} catch {
|
||||
this.logger.warn('Failed to parse Feishu message content');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userText.trim()) return;
|
||||
|
||||
try {
|
||||
// 检查是否是测评命令
|
||||
if (this.isAssessmentCommand(userText)) {
|
||||
// 委托给测评服务处理
|
||||
await this.feishuAssessmentService.handleCommand(bot, openId, userText);
|
||||
} else {
|
||||
// 默认使用知识库问答
|
||||
await this.processChatMessage(bot, openId, messageId, userText);
|
||||
}
|
||||
} catch (error) {
|
||||
this.logger.error('Message handling failed', error);
|
||||
try {
|
||||
await this.sendTextMessage(
|
||||
bot,
|
||||
'open_id',
|
||||
openId,
|
||||
'抱歉,处理您的消息时遇到了错误,请稍后重试。',
|
||||
);
|
||||
} catch (sendError) {
|
||||
this.logger.error('Failed to send error message to Feishu', sendError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private isAssessmentCommand(message: string): boolean {
|
||||
const trimmed = message.trim().toLowerCase();
|
||||
return trimmed.startsWith('/assessment') ||
|
||||
trimmed.startsWith('/测评') ||
|
||||
trimmed.startsWith('/eval');
|
||||
}
|
||||
```
|
||||
|
||||
##### 2.4.2 模块初始化
|
||||
```typescript
|
||||
// D:\aura\AuraK\server\src\feishu\feishu.module.ts
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([
|
||||
FeishuBot,
|
||||
FeishuAssessmentSession,
|
||||
]),
|
||||
forwardRef(() => ChatModule),
|
||||
forwardRef(() => AssessmentModule),
|
||||
forwardRef(() => KnowledgeBaseModule),
|
||||
],
|
||||
controllers: [FeishuController],
|
||||
providers: [
|
||||
FeishuService,
|
||||
FeishuWsManager,
|
||||
FeishuAssessmentService,
|
||||
AssessmentCommandParser,
|
||||
],
|
||||
exports: [FeishuService, FeishuAssessmentService],
|
||||
})
|
||||
export class FeishuModule {}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## API 接口设计
|
||||
|
||||
### 1. 飞书机器人管理接口
|
||||
|
||||
#### 1.1 创建/更新飞书机器人
|
||||
```http
|
||||
POST /feishu/bots
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"appId": "cli_xxx",
|
||||
"appSecret": "xxx",
|
||||
"botName": "测评机器人",
|
||||
"knowledgeBaseId": "kb_xxx", // 可选:特定知识库
|
||||
"knowledgeGroupId": "group_xxx" // 可选:知识组
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"id": "bot_xxx",
|
||||
"appId": "cli_xxx",
|
||||
"botName": "测评机器人",
|
||||
"webhookUrl": "/api/feishu/webhook/cli_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.2 更新知识库配置
|
||||
```http
|
||||
PATCH /feishu/bots/:id/knowledge
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"knowledgeBaseId": "kb_xxx",
|
||||
"knowledgeGroupId": null
|
||||
}
|
||||
```
|
||||
|
||||
#### 1.3 获取机器人列表
|
||||
```http
|
||||
GET /feishu/bots
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id": "bot_xxx",
|
||||
"appId": "cli_xxx",
|
||||
"botName": "测评机器人",
|
||||
"enabled": true,
|
||||
"knowledgeBaseId": "kb_xxx",
|
||||
"knowledgeGroupName": "产品文档"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### 2. 测评会话接口(可选)
|
||||
|
||||
#### 2.1 通过飞书启动测评
|
||||
```http
|
||||
POST /feishu/assessment/start
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"botId": "bot_xxx",
|
||||
"openId": "ou_xxx",
|
||||
"knowledgeBaseId": "kb_xxx",
|
||||
"templateId": "tmpl_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_xxx",
|
||||
"question": {
|
||||
"id": "q_xxx",
|
||||
"text": "问题内容",
|
||||
"difficulty": "普通"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2 提交测评答案
|
||||
```http
|
||||
POST /feishu/assessment/answer
|
||||
```
|
||||
|
||||
**请求体**:
|
||||
```json
|
||||
{
|
||||
"botId": "bot_xxx",
|
||||
"openId": "ou_xxx",
|
||||
"answer": "用户答案"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.3 获取测评状态
|
||||
```http
|
||||
GET /feishu/assessment/status/:botId/:openId
|
||||
```
|
||||
|
||||
**响应**:
|
||||
```json
|
||||
{
|
||||
"sessionId": "sess_xxx",
|
||||
"status": "active",
|
||||
"currentQuestion": 3,
|
||||
"totalQuestions": 10,
|
||||
"startTime": "2026-03-17T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 数据库设计
|
||||
|
||||
### 实体关系图
|
||||
|
||||
```
|
||||
┌─────────────────┐
|
||||
│ FeishuBot │
|
||||
│─────────────────│
|
||||
│ id │◄──────┐
|
||||
│ userId │ │
|
||||
│ appId │ │ 1..*
|
||||
│ knowledgeBaseId │───────┼──────┐
|
||||
│ knowledgeGroupId│ │ │
|
||||
└─────────────────┘ │ │
|
||||
│ │
|
||||
│ │
|
||||
┌─────────────────────────┼──────┼─────────────────────┐
|
||||
│ │ │ │
|
||||
│ ┌─────────────────────┐ │ │ ┌─────────────────┐ │
|
||||
│ │ FeishuAssessment │ │ │ │ KnowledgeBase │ │
|
||||
│ │ Session │ │ │ └─────────────────┘ │
|
||||
│ │─────────────────────│ │ │ │
|
||||
│ │ id │ │ │ ┌─────────────────┐ │
|
||||
│ │ botId │─┼──────┼─┤ KnowledgeGroup │ │
|
||||
│ │ openId │ │ │ └─────────────────┘ │
|
||||
│ │ assessmentSessionId │ │ │ │
|
||||
│ │ status │ │ │ │
|
||||
│ └─────────────────────┘ │ │ │
|
||||
└─────────────────────────┴──────┴─────────────────────┘
|
||||
│
|
||||
│ 1..*
|
||||
┌─────────────────────────┼─────────────────────────┐
|
||||
│ │ │
|
||||
│ ┌─────────────────────┐ │ ┌─────────────────────┐ │
|
||||
│ │ AssessmentSession │ │ │ AssessmentResult │ │
|
||||
│ │─────────────────────│ │ │─────────────────────│ │
|
||||
│ │ id │ │ │ id │ │
|
||||
│ │ userId │ │ │ sessionId │ │
|
||||
│ │ knowledgeBaseId │ │ │ report │ │
|
||||
│ │ questions_json │ │ │ score │ │
|
||||
│ │ finalScore │ │ │ ... │ │
|
||||
│ └─────────────────────┘ │ └─────────────────────┘ │
|
||||
└─────────────────────────┴─────────────────────────┘
|
||||
```
|
||||
|
||||
### 数据表结构
|
||||
|
||||
#### feishu_assessment_sessions
|
||||
```sql
|
||||
CREATE TABLE feishu_assessment_sessions (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
bot_id VARCHAR(36) NOT NULL,
|
||||
open_id VARCHAR(255) NOT NULL,
|
||||
assessment_session_id VARCHAR(36) NOT NULL,
|
||||
status ENUM('active', 'completed', 'cancelled') DEFAULT 'active',
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
INDEX idx_bot_open (bot_id, open_id),
|
||||
INDEX idx_assessment_session (assessment_session_id),
|
||||
CONSTRAINT fk_feishu_assessment_bot
|
||||
FOREIGN KEY (bot_id)
|
||||
REFERENCES feishu_bots(id)
|
||||
ON DELETE CASCADE
|
||||
);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施计划
|
||||
|
||||
### 阶段 1:基础架构(1-2 天)
|
||||
|
||||
#### 任务清单
|
||||
- [ ] 1.1 创建数据库迁移脚本
|
||||
- [ ] 1.2 更新 FeishuBot 实体和 DTO
|
||||
- [ ] 1.3 修改 FeishuService 支持知识库选择
|
||||
- [ ] 1.4 更新飞书机器人创建/更新接口
|
||||
|
||||
**交付物**:
|
||||
- 数据库迁移脚本
|
||||
- 更新后的实体和 DTO
|
||||
- 修改后的 FeishuService
|
||||
|
||||
### 阶段 2:测评集成(2-3 天)
|
||||
|
||||
#### 任务清单
|
||||
- [ ] 2.1 创建 FeishuAssessmentSession 实体和迁移
|
||||
- [ ] 2.2 实现命令解析器
|
||||
- [ ] 2.3 实现 FeishuAssessmentService
|
||||
- [ ] 2.4 集成到 FeishuService
|
||||
- [ ] 2.5 设计并实现飞书卡片模板
|
||||
|
||||
**交付物**:
|
||||
- 测评会话实体和迁移
|
||||
- 命令解析器
|
||||
- 测评服务实现
|
||||
- 飞书卡片设计
|
||||
|
||||
### 阶段 3:测试优化(1-2 天)
|
||||
|
||||
#### 任务清单
|
||||
- [ ] 3.1 单元测试
|
||||
- [ ] 3.2 集成测试
|
||||
- [ ] 3.3 性能测试
|
||||
- [ ] 3.4 文档编写
|
||||
|
||||
**交付物**:
|
||||
- 测试用例和测试报告
|
||||
- 性能测试结果
|
||||
- 用户使用文档
|
||||
|
||||
---
|
||||
|
||||
## 安全考虑
|
||||
|
||||
### 1. 多租户隔离
|
||||
- **机制**:所有查询必须包含 `userId` 和 `tenantId` 过滤
|
||||
- **实现**:在 `FeishuBot` 实体中关联 `User` 实体,确保机器人只能访问所属用户的数据
|
||||
|
||||
### 2. 命令验证
|
||||
- **机制**:白名单命令验证,防止恶意命令注入
|
||||
- **实现**:命令解析器只识别预定义的命令格式
|
||||
|
||||
### 3. 会话超时
|
||||
- **机制**:测评会话设置超时时间(如 24 小时)
|
||||
- **实现**:定时清理过期会话
|
||||
|
||||
### 4. 数据隐私
|
||||
- **机制**:测评结果仅对授权用户可见
|
||||
- **实现**:所有接口使用 JWT 认证,验证用户权限
|
||||
|
||||
### 5. 敏感信息保护
|
||||
- **机制**:不存储明文的 App Secret
|
||||
- **实现**:加密存储 App Secret,使用时解密
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### A. 参考资料
|
||||
- [飞书开放平台文档](https://open.feishu.cn/document)
|
||||
- [RAG 系统架构设计](./rag-architecture.md)
|
||||
- [人才测评模块文档](./assessment-module.md)
|
||||
|
||||
### B. 术语表
|
||||
- **RAG**:检索增强生成 (Retrieval-Augmented Generation)
|
||||
- **FeishuBot**:飞书机器人实体
|
||||
- **KnowledgeBase**:知识库实体
|
||||
- **AssessmentSession**:测评会话实体
|
||||
|
||||
### C. 变更记录
|
||||
| 版本 | 日期 | 修改内容 | 作者 |
|
||||
|------|------|----------|------|
|
||||
| v1.0 | 2026-03-17 | 初始版本 | AI Assistant |
|
||||
|
||||
---
|
||||
|
||||
**文档结束**
|
||||
@@ -1,208 +0,0 @@
|
||||
# 飞书机器人与人才测评集成 - 设计摘要
|
||||
|
||||
> **文档版本**: v1.0
|
||||
> **创建日期**: 2026-03-17
|
||||
> **完整文档**: [feishu-assessment-integration-design.md](./feishu-assessment-integration-design.md)
|
||||
|
||||
---
|
||||
|
||||
## 一、核心问题
|
||||
|
||||
### 1. 飞书机器人当前对接哪个知识库?
|
||||
**答案**:当前使用**默认知识库**(用户的所有文件)
|
||||
|
||||
**原因**:在 `feishu.service.ts` 中调用 `chatService.streamChat()` 时,`selectedFiles` 和 `selectedGroups` 参数均为 `undefined`,导致搜索用户所有文件。
|
||||
|
||||
### 2. 如何与人才测评对接?
|
||||
**答案**:通过**自然语言命令**触发测评功能
|
||||
|
||||
**命令格式**:
|
||||
- `/assessment start [kbId|templateId]` - 开始测评
|
||||
- `/assessment answer [answer]` - 提交答案
|
||||
- `/assessment status` - 查看状态
|
||||
- `/assessment result` - 获取结果
|
||||
|
||||
---
|
||||
|
||||
## 二、设计方案
|
||||
|
||||
### 方案 1:知识库选择机制
|
||||
|
||||
#### 数据库变更
|
||||
在 `FeishuBot` 实体中新增字段:
|
||||
```typescript
|
||||
knowledgeBaseId: string; // 特定知识库ID
|
||||
knowledgeGroupId: string; // 知识组ID
|
||||
```
|
||||
|
||||
#### 选择逻辑
|
||||
1. **配置了 knowledgeBaseId** → 搜索该知识库的文件
|
||||
2. **配置了 knowledgeGroupId** → 搜索该知识组的文件
|
||||
3. **都未配置** → 搜索用户所有文件(默认行为)
|
||||
|
||||
### 方案 2:测评集成
|
||||
|
||||
#### 架构设计
|
||||
```
|
||||
用户消息 → 命令解析器 → 测评服务 → 人才测评模块
|
||||
↓
|
||||
聊天服务(非测评消息)
|
||||
```
|
||||
|
||||
#### 核心组件
|
||||
1. **AssessmentCommandParser** - 命令解析器
|
||||
2. **FeishuAssessmentService** - 测评服务
|
||||
3. **FeishuAssessmentSession** - 会话实体
|
||||
|
||||
#### 交互流程
|
||||
```
|
||||
1. 用户: /assessment start
|
||||
2. 系统: 创建测评会话,发送第一个问题卡片
|
||||
3. 用户: 回复答案
|
||||
4. 系统: 评估答案,发送下一个问题
|
||||
5. ... 循环直到测评完成
|
||||
6. 系统: 发送测评结果报告
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、实施步骤
|
||||
|
||||
### 阶段 1:基础架构(1-2 天)
|
||||
- [ ] 添加数据库字段和迁移脚本
|
||||
- [ ] 更新 FeishuBot 实体和 DTO
|
||||
- [ ] 修改 FeishuService 支持知识库选择
|
||||
|
||||
### 阶段 2:测评集成(2-3 天)
|
||||
- [ ] 创建测评会话实体和迁移
|
||||
- [ ] 实现命令解析器
|
||||
- [ ] 实现测评服务
|
||||
- [ ] 设计飞书卡片模板
|
||||
|
||||
### 阶段 3:测试优化(1-2 天)
|
||||
- [ ] 单元测试和集成测试
|
||||
- [ ] 性能测试
|
||||
- [ ] 文档编写
|
||||
|
||||
---
|
||||
|
||||
## 四、关键代码示例
|
||||
|
||||
### 4.1 知识库选择逻辑
|
||||
```typescript
|
||||
// feishu.service.ts
|
||||
async processChatMessage(bot: FeishuBot, ...) {
|
||||
let selectedFiles: string[] | undefined;
|
||||
let selectedGroups: string[] | undefined;
|
||||
|
||||
if (bot.knowledgeBaseId) {
|
||||
selectedFiles = await this.getFilesByKnowledgeBase(bot.knowledgeBaseId, ...);
|
||||
} else if (bot.knowledgeGroupId) {
|
||||
selectedGroups = [bot.knowledgeGroupId];
|
||||
}
|
||||
|
||||
const stream = this.chatService.streamChat(
|
||||
userMessage,
|
||||
[],
|
||||
userId,
|
||||
llmModel,
|
||||
language,
|
||||
undefined,
|
||||
selectedGroups, // 使用配置的知识组
|
||||
selectedFiles, // 使用配置的知识库文件
|
||||
// ...
|
||||
);
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 命令处理逻辑
|
||||
```typescript
|
||||
// feishu-assessment.service.ts
|
||||
async handleCommand(bot: FeishuBot, openId: string, message: string) {
|
||||
const command = this.commandParser.parse(message);
|
||||
|
||||
if (!command) {
|
||||
// 非测评命令,使用默认聊天
|
||||
await this.feishuService.processChatMessage(bot, openId, '', message);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (command.type) {
|
||||
case AssessmentCommandType.START:
|
||||
await this.startAssessment(bot, openId, command.parameters);
|
||||
break;
|
||||
case AssessmentCommandType.ANSWER:
|
||||
await this.submitAnswer(bot, openId, command.parameters[0]);
|
||||
break;
|
||||
// ...
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、API 接口
|
||||
|
||||
### 5.1 飞书机器人管理
|
||||
```http
|
||||
POST /feishu/bots
|
||||
{
|
||||
"appId": "cli_xxx",
|
||||
"appSecret": "xxx",
|
||||
"knowledgeBaseId": "kb_xxx", // 可选
|
||||
"knowledgeGroupId": "group_xxx" // 可选
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 测评会话管理
|
||||
```http
|
||||
POST /feishu/assessment/start
|
||||
{
|
||||
"botId": "bot_xxx",
|
||||
"openId": "ou_xxx",
|
||||
"knowledgeBaseId": "kb_xxx",
|
||||
"templateId": "tmpl_xxx"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、安全考虑
|
||||
|
||||
1. **多租户隔离**:所有查询必须包含 `userId` 和 `tenantId`
|
||||
2. **命令验证**:白名单命令验证,防止注入
|
||||
3. **会话超时**:测评会话设置 24 小时超时
|
||||
4. **数据隐私**:测评结果仅对授权用户可见
|
||||
|
||||
---
|
||||
|
||||
## 七、文件清单
|
||||
|
||||
### 需要创建的文件
|
||||
1. `server/src/feishu/entities/feishu-assessment-session.entity.ts`
|
||||
2. `server/src/feishu/dto/assessment-command.dto.ts`
|
||||
3. `server/src/feishu/services/assessment-command.parser.ts`
|
||||
4. `server/src/feishu/services/feishu-assessment.service.ts`
|
||||
5. `server/src/migrations/XXXXXX-AddFeishuBotKnowledgeFields.ts`
|
||||
6. `server/src/migrations/XXXXXX-CreateFeishuAssessmentSessionTable.ts`
|
||||
|
||||
### 需要修改的文件
|
||||
1. `server/src/feishu/entities/feishu-bot.entity.ts`
|
||||
2. `server/src/feishu/dto/create-bot.dto.ts`
|
||||
3. `server/src/feishu/feishu.service.ts`
|
||||
4. `server/src/feishu/feishu.module.ts`
|
||||
|
||||
---
|
||||
|
||||
## 八、总结
|
||||
|
||||
| 问题 | 答案 |
|
||||
|------|------|
|
||||
| 飞书机器人当前对接哪个知识库? | 默认知识库(用户所有文件) |
|
||||
| 如何配置特定知识库? | 在 FeishuBot 实体中设置 knowledgeBaseId 或 knowledgeGroupId |
|
||||
| 如何与人才测评对接? | 通过 `/assessment` 命令触发测评功能 |
|
||||
| 实施周期 | 5-7 天 |
|
||||
|
||||
---
|
||||
|
||||
**完整设计文档**: [feishu-assessment-integration-design.md](./feishu-assessment-integration-design.md)
|
||||
@@ -1,58 +0,0 @@
|
||||
# Git 配置指南
|
||||
|
||||
## GitTea 仓库信息
|
||||
|
||||
- **仓库地址**: https://gittea.dev/hangshuo652/aurak.git
|
||||
- **分支**: master
|
||||
- **用户名**: hangshuo652
|
||||
|
||||
## 获取 Access Token
|
||||
|
||||
1. 登录 Gitee: https://gitee.com
|
||||
2. 点击头像 → 设置 → 安全设置 → 个人访问令牌
|
||||
3. 生成新令牌,勾选权限:
|
||||
- `project` (项目)
|
||||
- `repo` (仓库)
|
||||
- `pull_request` (Pull Request)
|
||||
- `push` (推送)
|
||||
4. 复制生成的 token
|
||||
|
||||
## 配置远程仓库
|
||||
|
||||
```bash
|
||||
# 查看当前 remote
|
||||
git remote -v
|
||||
|
||||
# 更新 remote(替换为你的 token)
|
||||
git remote set-url origin https://用户名:token@gitee.com/用户名/仓库名.git
|
||||
```
|
||||
|
||||
## 常用 Git 命令
|
||||
|
||||
```bash
|
||||
# 查看状态
|
||||
git status
|
||||
|
||||
# 添加所有更改
|
||||
git add .
|
||||
|
||||
# 提交更改
|
||||
git commit -m "描述"
|
||||
|
||||
# 推送到远程
|
||||
git push
|
||||
|
||||
# 拉取最新代码
|
||||
git pull
|
||||
```
|
||||
|
||||
## 当前环境
|
||||
|
||||
- 工作目录: D:/AuraK
|
||||
- Node.js + NestJS 后端
|
||||
- React + TypeScript 前端
|
||||
- Docker Compose 部署
|
||||
|
||||
---
|
||||
|
||||
**最后更新**: 2026-05-14
|
||||
@@ -1,405 +0,0 @@
|
||||
# L1 人才育成测评系统设计文档
|
||||
|
||||
## 文档信息
|
||||
|
||||
- **版本**: 1.1
|
||||
- **日期**: 2026-04-18
|
||||
- **状态**: 评审通过
|
||||
- **关联**: AI 人才育成计划
|
||||
|
||||
---
|
||||
|
||||
## 1. 业务背景
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
针对讯和 AI 人才育成计划的 L1 级别(认知与起步),建立系统化的在线评估认证体系。
|
||||
|
||||
### 1.2 L1 级别定义
|
||||
|
||||
- **目标**: 建立安全使用习惯,理解基本概念
|
||||
- **面向**: 全员
|
||||
- **内容**: AI基础概念、安全合规、Prompt入门、IDE基本操作
|
||||
- **认证**: 在线考试 ≥90分
|
||||
|
||||
---
|
||||
|
||||
## 2. 评估范围
|
||||
|
||||
### 2.1 五门课程
|
||||
|
||||
| 序号 | 课程名称 | 对应维度 | 知识组 |
|
||||
|------|----------|----------|--------|
|
||||
| 1 | 工作能力-安全 | 工作能力 | AI基础概念 + 安全合规 |
|
||||
| 2 | 技术能力-LLM | 技术能力 | 大语言模型原理 |
|
||||
| 3 | 技术能力-提示词 | 技术能力 | 提示词工程 |
|
||||
| 4 | IDE协作能力 | IDE协作 | IDE操作、代码辅助 |
|
||||
| 5 | AI开发范式 | AI开发范式 | Flow-State理解 |
|
||||
|
||||
### 2.2 知识组说明
|
||||
|
||||
知识组存储在 KnowledgeGroup 中,每个组包含:
|
||||
- 课程名称
|
||||
- 相关学习资料(文档、PDF等)
|
||||
- 评估要点说明
|
||||
|
||||
---
|
||||
|
||||
## 3. 评估流程设计
|
||||
|
||||
### 3.1 基本流程
|
||||
|
||||
```
|
||||
发起评估 → 知识组检索 → 生成问题 → 用户回答 → 即时评分/追问 → 全部完成 → 生成报告 → 发放证书
|
||||
```
|
||||
|
||||
### 3.2 评估步骤
|
||||
|
||||
1. **发起评估**: 用户选择 L1 认证评估,系统自动关联五个知识组
|
||||
2. **问题生成**: 基于五个知识组内容生成 8-10 道理解性问题
|
||||
3. **答题交互**: 用户逐题回答,AI 判断理解深度,如不足则追问确认
|
||||
4. **即时反馈**: 每题回答后即时反馈分数和简短评语
|
||||
5. **最终报告**: 全部完成后生成综合报告
|
||||
6. **证书发放**: 通过者发放电子证书
|
||||
|
||||
### 3.3 追问策略
|
||||
|
||||
- 每题最多追问 1-2 次以确认理解深度
|
||||
- 根据回答情况动态调整追问深度
|
||||
- 超过追问次数后强制进入下一题
|
||||
|
||||
---
|
||||
|
||||
## 4. 评分设计
|
||||
|
||||
### 4.1 权重配置
|
||||
|
||||
| 维度 | 权重 | 分值 |
|
||||
|------|------|------|
|
||||
| 技术能力-提示词 | 50% | 50分 |
|
||||
| 其他四门 | 50% | 50分 |
|
||||
|
||||
### 4.2 理解深度层级
|
||||
|
||||
| 分数 | 层级 | 说明 |
|
||||
|------|------|------|
|
||||
| 1-3 | 基础 | 知道概念,无法应用 |
|
||||
| 4-6 | 理解 | 理解含义,可简单应用 |
|
||||
| 7-8 | 应用 | 理解本质,能解释原因 |
|
||||
| 9-10 | 创新 | 能迁移到新场景 |
|
||||
|
||||
### 4.3 维度计分规则
|
||||
|
||||
- **题目维度归属**:生成问题时确定维度归属(从哪个知识组出题就属于哪个维度)
|
||||
- **dimensionScores 生成**:每道题记录所属维度,积分时按维度汇总
|
||||
|
||||
### 4.4 权重计算
|
||||
|
||||
| 维度 | 权重 | 分值 | 说明 |
|
||||
|------|------|------|------|
|
||||
| 技术能力-提示词 | 50% | 50分 | 4-5道题 |
|
||||
| 其他四门 | 50% | 50分 | 4-5道题,平均分配 |
|
||||
|
||||
- **计算公式**:
|
||||
- 各维度得分 = 该维度所有题目平均分(0-10)
|
||||
- 其他四门平均分 = (LLM + IDE + 开发范式 + 工作能力) / 4
|
||||
- 总分 = 提示词平均分 × 0.5 + 其他四门平均分 × 0.5
|
||||
- 最终分数 = 总分 × 10(转换为100分制)
|
||||
|
||||
- **示例**:
|
||||
- 提示词4题得分:10, 8, 9, 7 → 平均8.5
|
||||
- 其他四门各1题得分:9, 7, 8, 6 → 平均7.5
|
||||
- 总分 = 8.5×0.5 + 7.5×0.5 = 8.0
|
||||
- 最终分数 = 8.0×10 = 80分 → 未通过(需≥90)
|
||||
|
||||
### 4.5 通过标准
|
||||
|
||||
- 总分 ≥ 90 分即可通过
|
||||
- 各维度分别计分,支持薄弱环节识别
|
||||
|
||||
### 4.6 题目数量
|
||||
|
||||
- 总计 8-10 道题
|
||||
- 提示词相关 4-5 道(50分)
|
||||
- 其他四门 4-5 道(50分)
|
||||
|
||||
---
|
||||
|
||||
## 5. 模板设计
|
||||
|
||||
### 5.1 评估模板字段
|
||||
|
||||
| 字段 | 类型 | 说明 | 默认值 |
|
||||
|------|------|------|--------|
|
||||
| name | string | 模板名称 | L1-AI人才育成认证 |
|
||||
| description | string | 描述 | L1级别能力认证评估 |
|
||||
| linkedGroups | string[] | 关联知识组ID列表 | [] |
|
||||
| weightConfig | JSON | 权重配置 | {"prompt":50,"other":50} |
|
||||
| difficultyConfig | JSON | 难度配置 | {"standard":60,"advanced":30,"specialist":10} |
|
||||
| questionCount | number | 题目数量 | 8-10 |
|
||||
| questionCountMin | number | 最小题数 | 8 |
|
||||
| questionCountMax | number | 最大题数 | 10 |
|
||||
| passingScore | number | 通过分数 | 90 |
|
||||
| style | string | 评估风格 | conversation |
|
||||
| isActive | boolean | 是否启用 | true |
|
||||
|
||||
### 5.2 模板配置(扩展现有 AssessmentTemplate)
|
||||
|
||||
```typescript
|
||||
// 新增字段
|
||||
@Column({ type: 'simple-json', name: 'linked_group_ids', nullable: true })
|
||||
linkedGroupIds: string[]; // 支持多知识组
|
||||
|
||||
@Column({ type: 'simple-json', name: 'weight_config', nullable: true })
|
||||
weightConfig: {
|
||||
prompt: number; // 提示词权重 默认50
|
||||
other: number; // 其他维度权重 默认50
|
||||
};
|
||||
|
||||
@Column({ type: 'simple-json', name: 'difficulty_config', nullable: true })
|
||||
difficultyConfig: {
|
||||
standard: number; // 基础题比例 默认60
|
||||
advanced: number; // 理解题比例 默认30
|
||||
specialist: number; // 创新题比例 默认10
|
||||
};
|
||||
|
||||
@Column({ type: 'int', name: 'question_count_min', default: 8 })
|
||||
questionCountMin: number;
|
||||
|
||||
@Column({ type: 'int', name: 'question_count_max', default: 10 })
|
||||
questionCountMax: number;
|
||||
|
||||
@Column({ type: 'int', name: 'passing_score', default: 90 })
|
||||
passingScore: number;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. 角色权限设计
|
||||
|
||||
### 6.1 角色说明
|
||||
|
||||
基于现有角色体系扩展,管理员角色(admin/super_admin)具备模板和题目调整权限。
|
||||
|
||||
| 角色 | 说明 |
|
||||
|------|------|
|
||||
| user | 普通员工/学员 |
|
||||
| admin | 管理员(含题目/模板调整权限) |
|
||||
| super_admin | 超级管理员(含全部权限) |
|
||||
| instructor | 讲师(可选,复查功能) |
|
||||
|
||||
### 6.2 权限矩阵
|
||||
|
||||
| 功能 | 学员 | 管理员 | 超级管理员 | 讲师 |
|
||||
|------|------|--------|------------|-----------|------|
|
||||
| 发起评估 | ✓ | ✓ | ✓ | - |
|
||||
| 查看个人报告 | ✓ | ✓ | ✓ | - |
|
||||
| 查看全部报告 | - | ✓ | ✓ | ✓ |
|
||||
| 创建/编辑模板 | - | ✓ | ✓ | - |
|
||||
| 调整题目配置 | - | ✓ | ✓ | - |
|
||||
| 配置知识组 | - | ✓ | ✓ | - |
|
||||
| 导出统计数据 | - | ✓ | ✓ | - |
|
||||
| 人工复查 | - | - | ✓ | ✓ |
|
||||
| 手动调整分数 | - | - | ✓ | ✓ |
|
||||
| 系统配置 | - | - | ✓ | - |
|
||||
| 用户管理 | - | - | ✓ | - |
|
||||
|
||||
---
|
||||
|
||||
## 7. 评估结果设计
|
||||
|
||||
### 7.1 报告结构
|
||||
|
||||
```json
|
||||
{
|
||||
"sessionId": "uuid",
|
||||
"userId": "uuid",
|
||||
"templateId": "uuid",
|
||||
"status": "COMPLETED",
|
||||
"totalScore": 95,
|
||||
"passed": true,
|
||||
"dimensionScores": {
|
||||
"prompt": 48,
|
||||
"llm": 18,
|
||||
"ide": 14,
|
||||
"devPattern": 10,
|
||||
"workCapability": 5
|
||||
},
|
||||
"radarData": {
|
||||
"prompt": 9.6,
|
||||
"llm": 9.0,
|
||||
"ide": 7.0,
|
||||
"devPattern": 5.0,
|
||||
"workCapability": 5.0
|
||||
},
|
||||
"level": "Proficient",
|
||||
"questions": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"dimension": "prompt",
|
||||
"questionText": "...",
|
||||
"score": 9,
|
||||
"followUpCount": 1,
|
||||
"feedback": "..."
|
||||
}
|
||||
],
|
||||
"report": "总体评价...",
|
||||
"suggestions": ["改进建议1", "改进建议2"],
|
||||
"certifiedAt": "2026-04-18T10:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 雷达图数据
|
||||
|
||||
五维度得分(归一化到 0-10):
|
||||
1. 提示词工程 (prompt) - 出题自提示词知识组
|
||||
2. LLM原理 (llm) - 出题自LLM知识组
|
||||
3. IDE协作 (ide) - 出题自IDE知识组
|
||||
4. AI开发范式 (devPattern) - 出题自开发范式知识组
|
||||
5. 工作能力 (workCapability) - 出题自工作能力知识组
|
||||
|
||||
**维度归属规则**:生成问题时从哪个知识组检索内容,该题就属于哪个维度。
|
||||
|
||||
### 7.3 证书设计
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| certificateId | 证书唯一ID |
|
||||
| userId | 持有者ID |
|
||||
| templateId | 评估模板ID |
|
||||
| level | 通过级别 |
|
||||
| totalScore | 总分 |
|
||||
| issuedAt | 发放时间 |
|
||||
| expiresAt | 有效期(null=永久) |
|
||||
| qrCode | 防伪二维码 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 功能设计
|
||||
|
||||
### 8.1 用户功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 发起评估 | 选择模板,开始评估流程 |
|
||||
| 继续评估 | 断点续答 |
|
||||
| 查看历史 | 查看历次评估记录和报告 |
|
||||
| 下载证书 | 下载电子证书PDF |
|
||||
| 重考 | 通过后重新考试 |
|
||||
|
||||
### 8.2 管理员功能
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 模板管理 | 创建/编辑/启用禁用模板 |
|
||||
| 知识组配置 | 关联评估模板与知识组 |
|
||||
| 评估记录 | 查看所有用户评估记录 |
|
||||
| 统计分析 | 查看通过率、各维度得分分布 |
|
||||
| 导出报表 | 导出CSV/Excel |
|
||||
|
||||
### 8.3 讲师功能(可选)
|
||||
|
||||
| 功能 | 说明 |
|
||||
|------|------|
|
||||
| 人工复查 | 查看并复查评估结果 |
|
||||
| 手动评分 | 调整AI评分 |
|
||||
| 添加评语 | 添加人工评语 |
|
||||
|
||||
---
|
||||
|
||||
## 9. 技术设计
|
||||
|
||||
### 9.1 现有架构复用
|
||||
|
||||
基于 AuraK 现有评估系统扩展:
|
||||
|
||||
```
|
||||
AssessmentTemplate (扩展)
|
||||
↓
|
||||
AssessmentSession (扩展)
|
||||
↓
|
||||
LangGraph: generator → interviewer → grader → analyzer
|
||||
```
|
||||
|
||||
### 9.2 新增数据库表
|
||||
|
||||
```sql
|
||||
-- 证书表
|
||||
CREATE TABLE assessment_certificates (
|
||||
id UUID PRIMARY KEY,
|
||||
user_id UUID NOT NULL,
|
||||
session_id UUID NOT NULL,
|
||||
template_id UUID NOT NULL,
|
||||
level VARCHAR(50),
|
||||
total_score FLOAT,
|
||||
qr_code VARCHAR(255),
|
||||
issued_at TIMESTAMP,
|
||||
created_at TIMESTAMP
|
||||
);
|
||||
```
|
||||
|
||||
### 9.3 新增 API 接口
|
||||
|
||||
| 接口 | 方法 | 说明 | 权限 |
|
||||
|------|------|------|------|
|
||||
| /templates | GET | 模板列表 | 公开 |
|
||||
| /templates | POST | 创建模板 | 管理员 |
|
||||
| /templates/:id | PUT | 更新模板 | 管理员 |
|
||||
| /templates/:id (软删除) | DELETE | 禁用模板 | 管理员 |
|
||||
| /certificates/:sessionId | GET | 获取证书 | 所有者 |
|
||||
| /certificates/:sessionId/download | GET | 下载证书PDF | 所有者 |
|
||||
| /certificates/verify | POST | 二维码验真 | 公开 |
|
||||
| /admin/statistics | GET | 统计报表 | 管理员 |
|
||||
|
||||
### 9.4 前端页面
|
||||
|
||||
| 页面 | 说明 |
|
||||
|------|------|
|
||||
| /assessment/templates | 模板管理 |
|
||||
| /assessment/history | 评估历史 |
|
||||
| /assessment/report/:id | 评估报告详情 |
|
||||
| /assessment/certificate/:id | 证书查看/下载 |
|
||||
| /admin/statistics | 管理统计 |
|
||||
|
||||
---
|
||||
|
||||
## 10. 实施计划
|
||||
|
||||
### 10.1 优先级
|
||||
|
||||
| 优先级 | 模块 | 说明 |
|
||||
|--------|------|------|
|
||||
| P0 | 模板扩展 | 支持多知识组、权重配置 |
|
||||
| P0 | 评估流程 | 适配L1五课程流程 |
|
||||
| P0 | 评分逻辑 | 调整追问策略、加权计分 |
|
||||
| P1 | 报告生成 | 结构化报告、雷达图数据 |
|
||||
| P1 | 证书功能 | 证书生成、下载 |
|
||||
| P2 | 统计分析 | 管理后台统计 |
|
||||
| P2 | 人工复查 | 讲师功能 |
|
||||
|
||||
### 10.2 里程碑
|
||||
|
||||
1. **M1**: 模板扩展 + 基本评估流程(1周)
|
||||
2. **M2**: 报告生成 + 证书功能(1周)
|
||||
3. **M3**: 管理后台 + 统计分析(1周)
|
||||
4. **M4**: 测试优化 + 上线(1周)
|
||||
|
||||
---
|
||||
|
||||
## 11. 附录
|
||||
|
||||
### 11.1 参考文档
|
||||
|
||||
- AI人才育成计划.md
|
||||
- 提示词工程.md
|
||||
- 大语言模型入门.md
|
||||
- AI安全使用指南.md
|
||||
- Open Code使用指南.md
|
||||
- GitHub Copilot使用指南.md
|
||||
- Claude Code使用指南.md
|
||||
- L1-开发范式是什么.md
|
||||
|
||||
### 11.2 现有代码参考
|
||||
|
||||
- `server/src/assessment/` - 评估模块
|
||||
- `web/components/views/AssessmentView.tsx` - 前端评估界面
|
||||
@@ -1,490 +0,0 @@
|
||||
# L1人才育成评估系统实施计划
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 实现L1人才育成评估系统,支持五门课程考核、多知识组关联、加权计分、证书生成
|
||||
|
||||
**Architecture:** 基于现有AuraK评估系统(LangGraph状态机)扩展,新增模板多知识组支持、维度计分、证书功能
|
||||
|
||||
**Tech Stack:** NestJS + TypeORM + LangGraph + React
|
||||
|
||||
---
|
||||
|
||||
## Phase 1: 模板扩展(P0)
|
||||
|
||||
### Task 1.1: 扩展 AssessmentTemplate 实体
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/entities/assessment-template.entity.ts:1-80`
|
||||
|
||||
**Step 1: Add new columns**
|
||||
|
||||
```typescript
|
||||
// Add after existing columns (line ~79)
|
||||
@Column({ type: 'simple-json', name: 'linked_group_ids', nullable: true })
|
||||
linkedGroupIds: string[];
|
||||
|
||||
@Column({ type: 'simple-json', name: 'weight_config', nullable: true })
|
||||
weightConfig: {
|
||||
prompt: number;
|
||||
other: number;
|
||||
};
|
||||
|
||||
@Column({ type: 'simple-json', name: 'difficulty_config', nullable: true })
|
||||
difficultyConfig: {
|
||||
standard: number;
|
||||
advanced: number;
|
||||
specialist: number;
|
||||
};
|
||||
|
||||
@Column({ type: 'int', name: 'question_count_min', default: 8 })
|
||||
questionCountMin: number;
|
||||
|
||||
@Column({ type: 'int', name: 'question_count_max', default: 10 })
|
||||
questionCountMax: number;
|
||||
|
||||
@Column({ type: 'int', name: 'passing_score', default: 90 })
|
||||
passingScore: number;
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/assessment/entities/assessment-template.entity.ts
|
||||
git commit -m "feat: extend AssessmentTemplate with multi-group and weight config"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1.2: 创建数据库迁移
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/migrations/XXXXXX-add-template-extensions.sql`
|
||||
|
||||
**Step 1: Write migration**
|
||||
|
||||
```sql
|
||||
ALTER TABLE assessment_templates
|
||||
ADD COLUMN linked_group_ids TEXT,
|
||||
ADD COLUMN weight_config TEXT,
|
||||
ADD COLUMN difficulty_config TEXT,
|
||||
ADD COLUMN question_count_min INTEGER DEFAULT 8,
|
||||
ADD COLUMN question_count_max INTEGER DEFAULT 10,
|
||||
ADD COLUMN passing_score INTEGER DEFAULT 90;
|
||||
```
|
||||
|
||||
**Step 2: Test migration**
|
||||
|
||||
```bash
|
||||
cd server && npx typeorm query "SELECT linked_group_ids, weight_config FROM assessment_templates LIMIT 1"
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/migrations/
|
||||
git commit -m "db: add template extension columns"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 1.3: 更新 TemplateService CRUD
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/services/template.service.ts`
|
||||
|
||||
**Step 1: Add linkedGroupIds handling**
|
||||
|
||||
```typescript
|
||||
// In create() method, add:
|
||||
if (createTemplateDto.linkedGroupIds) {
|
||||
template.linkedGroupIds = createTemplateDto.linkedGroupIds;
|
||||
}
|
||||
if (createTemplateDto.weightConfig) {
|
||||
template.weightConfig = createTemplateDto.weightConfig;
|
||||
}
|
||||
// ... other fields
|
||||
```
|
||||
|
||||
**Step 2: Verify compilation**
|
||||
|
||||
```bash
|
||||
cd server && npx tsc --noEmit
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/assessment/services/template.service.ts
|
||||
git commit -m "feat: support linkedGroupIds in TemplateService"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 2: 评估流程适配(P0)
|
||||
|
||||
### Task 2.1: 修改 QuestionGenerator 支持多知识组
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/assessment.service.ts:266-368` (startSession method)
|
||||
|
||||
**Step 1: Support multi-group content retrieval**
|
||||
|
||||
```typescript
|
||||
// In startSession(), replace single content retrieval with multi-group:
|
||||
private async getMultiGroupContent(
|
||||
groupIds: string[],
|
||||
userId: string,
|
||||
tenantId: string,
|
||||
): Promise<string> {
|
||||
const contents: string[] = [];
|
||||
for (const groupId of groupIds) {
|
||||
const files = await this.groupService.getGroupFiles(groupId, userId, tenantId);
|
||||
const content = files.map(f => f.content).join('\n\n---\n\n');
|
||||
contents.push(content);
|
||||
}
|
||||
return contents.join('\n\n');
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Pass content with dimension tag**
|
||||
|
||||
```typescript
|
||||
// In getSessionContent, add dimension tag:
|
||||
// === Document: [工作能力] ===
|
||||
// ...content...
|
||||
```
|
||||
|
||||
**Step 3: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/assessment/assessment.service.ts
|
||||
git commit -m "feat: support multi-group content retrieval"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2.2: Generator 输出维度信息
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/graph/nodes/generator.node.ts:176-182`
|
||||
|
||||
**Step 1: Add dimension field**
|
||||
|
||||
```typescript
|
||||
const mappedNewQuestions = newQuestions.map((q: any) => ({
|
||||
id: (existingQuestions.length + 1).toString(),
|
||||
questionText: q.question_text,
|
||||
keyPoints: q.key_points,
|
||||
difficulty: q.difficulty,
|
||||
basis: q.basis,
|
||||
dimension: q.dimension || this.inferDimension(knowledgeBaseContent), // NEW FIELD
|
||||
}));
|
||||
|
||||
private inferDimension(content: string): string {
|
||||
// Detect dimension from content tags
|
||||
if (content.includes('[提示词]')) return 'prompt';
|
||||
if (content.includes('[LLM]')) return 'llm';
|
||||
// ... etc
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/assessment/graph/nodes/generator.node.ts
|
||||
git commit -m "feat: generator outputs dimension field"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 3: 评分逻辑(P0)
|
||||
|
||||
### Task 3.1: DimensionScores 计算
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/assessment.service.ts:600-630` (score calculation)
|
||||
|
||||
**Step 1: Calculate dimension scores**
|
||||
|
||||
```typescript
|
||||
// Replace simple weighted score with dimension-aware calculation:
|
||||
const calculateDimensionScores = (questions: any[], scores: Record<string, number>) => {
|
||||
const dimensionScores: Record<string, number> = {
|
||||
prompt: 0, llm: 0, ide: 0, devPattern: 0, workCapability: 0
|
||||
};
|
||||
const dimensionCounts: Record<string, number> = { prompt: 0, llm: 0, ide: 0, devPattern: 0, workCapability: 0 };
|
||||
|
||||
questions.forEach((q, idx) => {
|
||||
const dim = q.dimension || 'workCapability';
|
||||
dimensionScores[dim] += scores[q.id || idx.toString()] || 0;
|
||||
dimensionCounts[dim]++;
|
||||
});
|
||||
|
||||
// Average per dimension
|
||||
Object.keys(dimensionScores).forEach(d => {
|
||||
dimensionScores[d] = dimensionCounts[d] > 0
|
||||
? dimensionScores[d] / dimensionCounts[d]
|
||||
: 0;
|
||||
});
|
||||
|
||||
return dimensionScores;
|
||||
};
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/assessment/assessment.service.ts
|
||||
git commit -m "feat: calculate dimension scores"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3.2: 追问策略调整
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/graph/nodes/grader.node.ts:207`
|
||||
|
||||
**Step 1: Increase follow-up limit**
|
||||
|
||||
```typescript
|
||||
// Current: currentFollowUpCount >= 1
|
||||
// Change to:
|
||||
if (currentFollowUpCount >= 2 || result.score >= 8 || saysIDontKnow) {
|
||||
shouldFollowUp = false;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/assessment/graph/nodes/grader.node.ts
|
||||
git commit -m "feat: increase follow-up limit to 2"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 4: 报告生成(P1)
|
||||
|
||||
### Task 4.1: 扩展 Session 存储 dimension 数据
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/entities/assessment-session.entity.ts`
|
||||
|
||||
**Step 1: Add dimension fields**
|
||||
|
||||
```typescript
|
||||
@Column({ type: 'simple-json', name: 'dimension_scores', nullable: true })
|
||||
dimensionScores: Record<string, number>;
|
||||
|
||||
@Column({ type: 'simple-json', name: 'radar_data', nullable: true })
|
||||
radarData: Record<string, number>;
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/assessment/entities/assessment-session.entity.ts
|
||||
git commit -m "feat: add dimension scores to session entity"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4.2: Analyzer 生成结构化报告
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/graph/nodes/analyzer.node.ts`
|
||||
|
||||
**Step 1: Generate dimension-aware report**
|
||||
|
||||
```typescript
|
||||
// Modify system prompt to include:
|
||||
const dimensionSummary = Object.entries(dimensionScores)
|
||||
.map(([dim, score]) => `${dim}: ${score}/10`)
|
||||
.join('\n');
|
||||
|
||||
const reportPrompt = `...
|
||||
维度得分:
|
||||
${dimensionSummary}
|
||||
|
||||
请在报告中包含:
|
||||
1. 各维度得分分析
|
||||
2. 薄弱环节识别
|
||||
3. 针对性改进建议
|
||||
`;
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/assessment/graph/nodes/analyzer.node.ts
|
||||
git commit -m "feat: generate structured report with dimension analysis"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 5: 证书功能(P1)
|
||||
|
||||
### Task 5.1: 创建证书表
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/assessment/entities/assessment-certificate.entity.ts`
|
||||
|
||||
**Step 1: Define entity**
|
||||
|
||||
```typescript
|
||||
@Entity('assessment_certificates')
|
||||
export class AssessmentCertificate {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'user_id' })
|
||||
userId: string;
|
||||
|
||||
@Column({ name: 'session_id' })
|
||||
sessionId: string;
|
||||
|
||||
@Column({ name: 'template_id' })
|
||||
templateId: string;
|
||||
|
||||
@Column()
|
||||
level: string;
|
||||
|
||||
@Column({ type: 'float', name: 'total_score' })
|
||||
totalScore: number;
|
||||
|
||||
@Column({ name: 'qr_code', nullable: true })
|
||||
qrCode: string;
|
||||
|
||||
@CreateDateColumn({ name: 'issued_at' })
|
||||
issuedAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/assessment/entities/assessment-certificate.entity.ts
|
||||
git commit -m "feat: create certificate entity"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 5.2: 证书生成API
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/assessment.controller.ts`
|
||||
|
||||
**Step 1: Add certificate endpoints**
|
||||
|
||||
```typescript
|
||||
@Get('certificate/:sessionId')
|
||||
async getCertificate(
|
||||
@Param('sessionId') sessionId: string,
|
||||
@Req() req,
|
||||
): Promise<AssessmentCertificate> {
|
||||
// Generate or return existing certificate
|
||||
}
|
||||
|
||||
@Get('certificate/:sessionId/download')
|
||||
async downloadCertificate(
|
||||
@Param('sessionId') sessionId: string,
|
||||
): Promise<StreamableFile> {
|
||||
// Generate PDF with jsPDF
|
||||
}
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add server/src/assessment/assessment.controller.ts
|
||||
git commit -m "feat: add certificate endpoints"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Phase 6: 前端集成(P2)
|
||||
|
||||
### Task 6.1: 模板选择页面
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/components/views/AssessmentView.tsx`
|
||||
|
||||
**Step 1: Add template selection**
|
||||
|
||||
```typescript
|
||||
// Add state:
|
||||
const [templates, setTemplates] = useState<AssessmentTemplate[]>([]);
|
||||
|
||||
// Load templates on mount:
|
||||
useEffect(() => {
|
||||
const loadTemplates = async () => {
|
||||
const res = await assessmentService.getTemplates();
|
||||
setTemplates(res.data);
|
||||
};
|
||||
loadTemplates();
|
||||
}, []);
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add web/components/views/AssessmentView.tsx
|
||||
git commit -m "feat: add template selection to frontend"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 6.2: 报告页面的雷达图
|
||||
|
||||
**Files:**
|
||||
- Create: `web/components/RadarChart.tsx`
|
||||
|
||||
**Step 1: Create component**
|
||||
|
||||
```typescript
|
||||
export const RadarChart = ({ data }) => {
|
||||
// Use recharts or echarts for radar
|
||||
// Data: { prompt: 9.6, llm: 9.0, ide: 7.0, devPattern: 5.0, workCapability: 5.0 }
|
||||
};
|
||||
```
|
||||
|
||||
**Step 2: Commit**
|
||||
|
||||
```bash
|
||||
git add web/components/RadarChart.tsx
|
||||
git commit -m "feat: add radar chart component"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 实施顺序
|
||||
|
||||
1. Task 1.1 → 1.3 (模板扩展)
|
||||
2. Task 2.1 → 2.2 (评估流程)
|
||||
3. Task 3.1 → 3.2 (评分逻辑)
|
||||
4. Task 4.1 → 4.2 (报告)
|
||||
5. Task 5.1 → 5.2 (证书)
|
||||
6. Task 6.1 → 6.2 (前端)
|
||||
|
||||
**每个Task约需1-2小时**
|
||||
|
||||
---
|
||||
|
||||
## 依赖说明
|
||||
|
||||
- Phase 1 (模板) → Phase 2 → Phase 3 → Phase 4 → Phase 5
|
||||
- 前端(Phase 6) 可在后端基本完成后并行开发
|
||||
- 测试贯穿每个Task
|
||||
|
||||
---
|
||||
|
||||
## 验收标准
|
||||
|
||||
- [ ] 模板支持多知识组关联
|
||||
- [ ] 题目带dimension字段
|
||||
- [ ] 报告含dimensionScores和radarData
|
||||
- [ ] 可生成��书PDF
|
||||
- [ ] 前端显示雷达图
|
||||
- [ ] E2E测试通过
|
||||
@@ -1,593 +0,0 @@
|
||||
# AuraK 评估系统完整实施计划
|
||||
|
||||
> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
|
||||
|
||||
**Goal:** 构建完整的AI人才评估系统,包含题库管理、维度均衡抽取、审核流程、评估统计
|
||||
|
||||
**Architecture:**
|
||||
- 题库通过模板间接关联知识库分组
|
||||
- 评估时从题库维度均衡抽取,不足时实时生成备用
|
||||
- 管理员通过独立页面管理题库和查看统计
|
||||
|
||||
**Tech Stack:** NestJS + TypeORM + LangGraph + React + TypeScript
|
||||
|
||||
---
|
||||
|
||||
## 一、问题与方案对照表
|
||||
|
||||
| # | 待修复问题 | 解决思路 | 对应Task |
|
||||
|---|------------|----------|----------|
|
||||
| 1 | 评分计算错误 | 已修复 | ✅ |
|
||||
| 2 | 题目维度分配不均 | 已修复 | ✅ |
|
||||
| 3 | 多轮对话中断 | 已修复 | ✅ |
|
||||
| 4 | 计数器显示错误 | 已修复 | ✅ |
|
||||
| 5 | 历史问答查询 | 统计页面 | Task 4 |
|
||||
| 6 | 知识覆盖度来源 | 用户不 care | - |
|
||||
| 7 | 实时反馈无内容 | 用户确认已有内容 | - |
|
||||
| 8 | 预生成题目功能 | 题库+抽取 | Task 1,2,3 |
|
||||
| 9 | 题目生成不稳定 | 题库前置 | Task 1,7 |
|
||||
| 10 | 题目来源不是知识库 | 题库+范围控制 | Task 1,3 |
|
||||
| 11 | 选择题实际无选项 | 题库管理 | Task 1,5 |
|
||||
|
||||
---
|
||||
|
||||
## 二、系统架构
|
||||
|
||||
### 2.1 数据层级
|
||||
|
||||
```
|
||||
KnowledgeGroup(知识库分组)
|
||||
↓
|
||||
AssessmentTemplate(模板)→ 题目数量、知识库配置
|
||||
↓
|
||||
QuestionBank(题库)→ 审核状态
|
||||
↓
|
||||
QuestionBankItem(题目)→ 维度/难度/答案
|
||||
↓
|
||||
评估抽取使用
|
||||
```
|
||||
|
||||
### 2.2 评估流程
|
||||
|
||||
```
|
||||
开始评估
|
||||
↓
|
||||
1. 加载模板配置(题目数量、维度配置)
|
||||
↓
|
||||
2. 检查已发布题库题目数量
|
||||
↓
|
||||
充足 → 维度均衡随机抽取 → 进入答题
|
||||
不足 → 实时生成题目(备用) → 进入答题
|
||||
↓
|
||||
3. 用户答题 → 即时评分 → 实时反馈
|
||||
↓
|
||||
4. 追问(如Grader判断需要)
|
||||
↓
|
||||
5. 答题完成 → 生成报告 + 记录统计
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、数据模型
|
||||
|
||||
### 3.1 QuestionBank(题库)
|
||||
|
||||
```typescript
|
||||
// server/src/assessment/entities/question-bank.entity.ts
|
||||
@Entity('question_banks')
|
||||
export class QuestionBank {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'template_id' })
|
||||
templateId: string;
|
||||
|
||||
@ManyToOne(() => AssessmentTemplate)
|
||||
@JoinColumn({ name: 'template_id' })
|
||||
template: AssessmentTemplate;
|
||||
|
||||
@Column({ name: 'name' })
|
||||
name: string; // 题库名称
|
||||
|
||||
@Column({ type: 'text', description: '题库描述' })
|
||||
description: string;
|
||||
|
||||
@Column({ type: 'enum', enum: QuestionBankStatus, default: QuestionBankStatus.DRAFT })
|
||||
status: QuestionBankStatus; // DRAFT | PENDING_REVIEW | PUBLISHED
|
||||
|
||||
@Column({ name: 'created_by' })
|
||||
createdBy: string;
|
||||
|
||||
@Column({ name: 'reviewed_by', nullable: true })
|
||||
reviewedBy: string;
|
||||
|
||||
@Column({ name: 'reviewed_at', nullable: true })
|
||||
reviewedAt: Date;
|
||||
|
||||
@Column({ name: 'review_comment', nullable: true })
|
||||
reviewComment: string; // 审核意见
|
||||
|
||||
@OneToMany(() => QuestionBankItem, item => item.bank)
|
||||
items: QuestionBankItem[];
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
enum QuestionBankStatus {
|
||||
DRAFT = 'DRAFT', // 草稿
|
||||
PENDING_REVIEW = 'PENDING_REVIEW', // 待审核
|
||||
PUBLISHED = 'PUBLISHED', // 已发布
|
||||
REJECTED = 'REJECTED', // 审核拒绝
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 QuestionBankItem(题目)
|
||||
|
||||
```typescript
|
||||
// server/src/assessment/entities/question-bank-item.entity.ts
|
||||
@Entity('question_bank_items')
|
||||
export class QuestionBankItem {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'bank_id' })
|
||||
bankId: string;
|
||||
|
||||
@ManyToOne(() => QuestionBank, bank => bank.items)
|
||||
@JoinColumn({ name: 'bank_id' })
|
||||
bank: QuestionBank;
|
||||
|
||||
@Column({ type: 'text', name: 'question_text' })
|
||||
questionText: string;
|
||||
|
||||
@Column({ type: 'enum', enum: QuestionType })
|
||||
questionType: QuestionType; // SHORT_ANSWER | MULTIPLE_CHOICE | TRUE_FALSE
|
||||
|
||||
@Column({ type: 'simple-json', nullable: true })
|
||||
options: string[]; // 选择题选项 ["A. xxx", "B. xxx", ...]
|
||||
|
||||
@Column({ type: 'text', nullable: true })
|
||||
correctAnswer: string; // 正确答案
|
||||
|
||||
@Column({ type: 'simple-json', name: 'key_points' })
|
||||
keyPoints: string[]; // 关键点 ["point1", "point2"]
|
||||
|
||||
@Column({ type: 'enum', enum: QuestionDifficulty })
|
||||
difficulty: QuestionDifficulty; // STANDARD | ADVANCED | SPECIALIST
|
||||
|
||||
@Column({ type: 'enum', enum: QuestionDimension })
|
||||
dimension: QuestionDimension; // PROMPT | LLM | IDE | DEV_PATTERN | WORK_CAPABILITY
|
||||
|
||||
@Column({ type: 'text', name: ' basis', nullable: true })
|
||||
basis: string; // 出题依据
|
||||
|
||||
@Column({ name: 'created_by' })
|
||||
createdBy: string;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
}
|
||||
|
||||
enum QuestionType {
|
||||
SHORT_ANSWER = 'SHORT_ANSWER', // 简答题
|
||||
MULTIPLE_CHOICE = 'MULTIPLE_CHOICE', // 选择题
|
||||
TRUE_FALSE = 'TRUE_FALSE', // 判断题
|
||||
}
|
||||
|
||||
enum QuestionDifficulty {
|
||||
STANDARD = 'STANDARD', // 标准
|
||||
ADVANCED = 'ADVANCED', // 进阶
|
||||
SPECIALIST = 'SPECIALIST', // 专家
|
||||
}
|
||||
|
||||
enum QuestionDimension {
|
||||
PROMPT = 'PROMPT', // 提示词
|
||||
LLM = 'LLM', // LLM原理
|
||||
IDE = 'IDE', // IDE协作
|
||||
DEV_PATTERN = 'DEV_PATTERN', // 开发范式
|
||||
WORK_CAPABILITY = 'WORK_CAPABILITY', // 工作能力
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 评估统计
|
||||
|
||||
```typescript
|
||||
// API返回结构
|
||||
interface AssessmentStats {
|
||||
// 统计卡片
|
||||
totalAttempts: number; // 总考核次数
|
||||
highestScore: number; // 最高分
|
||||
averageScore: number; // 平均分
|
||||
completionRate: number; // 完成率
|
||||
|
||||
// 得分趋势(可选用于图表)
|
||||
scoreTrend: { date: string; score: number }[];
|
||||
|
||||
// 历史记录
|
||||
recentRecords: {
|
||||
id: string;
|
||||
knowledgeBase: string;
|
||||
template: string;
|
||||
score: number;
|
||||
status: 'IN_PROGRESS' | 'COMPLETED';
|
||||
createdAt: string;
|
||||
}[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、API设计
|
||||
|
||||
### 4.1 题库管理API
|
||||
|
||||
```typescript
|
||||
// 题库CRUD
|
||||
POST /api/question-banks // 创建题库
|
||||
GET /api/question-banks // 列表(带分页)
|
||||
GET /api/question-banks/:id // 详情(含题目)
|
||||
PUT /api/question-banks/:id // 更新题库信息
|
||||
DELETE /api/question-banks/:id // 删除题库
|
||||
|
||||
// 题目管理
|
||||
POST /api/question-banks/:bankId/items // 添加单题
|
||||
PUT /api/question-banks/:bankId/items/:id // 更新题目
|
||||
DELETE /api/question-banks/:bankId/items/:id // 删除题目
|
||||
|
||||
// 批量操作
|
||||
POST /api/question-banks/:bankId/batch-add // 批量添加题目(AI生成或Excel导入)
|
||||
POST /api/question-banks/:bankId/generate // AI批量生成待审核
|
||||
|
||||
// 审核流程
|
||||
PUT /api/question-banks/:id/submit // 提交审核(草稿→待审核)
|
||||
PUT /api/question-banks/:id/review // 审核(通过/拒绝,附带意见)
|
||||
PUT /api/question-banks/:id/publish // 发布(审核通过→已发布)
|
||||
PUT /api/question-banks/:id/unpublish // 下架
|
||||
|
||||
// 模板关联
|
||||
GET /api/question-banks/by-template/:templateId // 按模板查询题库
|
||||
```
|
||||
|
||||
### 4.2 统计API
|
||||
|
||||
```typescript
|
||||
// 评估统计
|
||||
GET /api/assessment/stats // 当前用户统计
|
||||
GET /api/assessment/stats/admin // 管理员统计(所有用户)
|
||||
Query: startDate, endDate, templateId, knowledgeGroupId
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、前端页面设计
|
||||
|
||||
### 5.1 题库管理页面(QuestionBankView)
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────────────────────────────────────┐
|
||||
│ 题库管理 [新建题库] │
|
||||
├────────────┬─────────────────────────────────────────────────────┤
|
||||
│ 我的题库 │ 筛选: [模板▼] [状态▼] [维度▼] [难度▼] [搜索...] │
|
||||
│ ├─────────────────────────────────────────────────────┤
|
||||
│ □ AI开发 │ ┌────┬────────┬────────┬──────┬────────────┐ │
|
||||
│ □ Python │ │选择│ 题目内容│ 题型 │ 维度 │ 难度 │ 操作│ │
|
||||
│ │ ├────┼────────┼────────┼──────┼────────────┤ │
|
||||
│ │ │ ○ │xxx │ 简答 │ llm │ 标准 │编辑│ │
|
||||
│ │ │ ○ │xxx │ 选择 │ ide │ 进阶 │编辑│ │
|
||||
│ │ │ ○ │xxx │ 判断 │ Work │ 专家 │删除│ │
|
||||
│ │ └────┴────────┴────────┴──────┴────────────┘ │
|
||||
│ │ [批量导入] │
|
||||
│ ├─────────────────────────────────────────────────────┤
|
||||
│ │ [AI批量生成] [提交审核] [全选] [取消选择] │
|
||||
└────────────┴─────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.2 题库详情/编辑面板
|
||||
|
||||
```
|
||||
┌────────────────────────────────────┐
|
||||
│ 编辑题目 [保存] │
|
||||
├────────────────────────────────────┤
|
||||
│ 题目内容 │
|
||||
│ ┌──────────────────────────────┐ │
|
||||
│ │ │ │
|
||||
│ └────────────────────────────┘ │
|
||||
│ │
|
||||
│ 题型: ●简答 ○选择 ○判断 │
|
||||
│ │
|
||||
│ [选择题时显示] │
|
||||
│ 选项: │
|
||||
│ A. ___________________ │
|
||||
│ B. ___________________ │
|
||||
│ C. ___________________ │
|
||||
│ D. ___________________ │
|
||||
│ │
|
||||
│ 正确答案: ________________ │
|
||||
│ │
|
||||
│ 关键点: │
|
||||
│ _________________________ │
|
||||
│ _________________________ │
|
||||
│ │
|
||||
│ 维度: [prompt▼] 难度: [标准▼] │
|
||||
│ │
|
||||
│ 出题依据: │
|
||||
│ _________________________ │
|
||||
└────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.3 统计页面(AssessmentStatsView)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 评估统计 [导出] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 筛选: [时间范围▼] [知识库▼] [重置] │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 总考核 │ 最高分 │ 平均分 │ 完成率 │
|
||||
│ 15 │ 9.2 │ 7.8 │ 80% │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 得分趋势(折线图 - 可选) │
|
||||
│ 10 ____ │
|
||||
│ 8 ____▓▓▓____▓▓ │
|
||||
│ 6 ____▓▓▓▓▓____▓▓▓▓___ │
|
||||
│ 4 _______________________________________ │
|
||||
│ 2 ____________________________________________│
|
||||
│ 04/10 04/12 04/14 04/16 04/18 │
|
||||
├─────────────────────────────────────────────────────────────────┤
|
||||
│ 历史记录 │
|
||||
│ 日期 知识库 模板 得分 状态 操作│
|
||||
│ 04/18 AI开发 AI基础 9.2 完成 查看 │
|
||||
│ 04/15 Python 编程基础 7.5 完成 查看 │
|
||||
│ 04/12 AI开发 AI基础 8.1 完成 查看 │
|
||||
│ 04/10 - LLM原理 - 进行中 查看 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、实施计划
|
||||
|
||||
### Task 1: 题库管理功能(核心)
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/assessment/entities/question-bank.entity.ts`
|
||||
- Create: `server/src/assessment/entities/question-bank-item.entity.ts`
|
||||
- Create: `server/src/assessment/question-bank.service.ts`
|
||||
- Create: `server/src/assessment/question-bank.controller.ts`
|
||||
- Modify: `server/src/assessment/assessment.module.ts` (import)
|
||||
- Create: 迁移文件 `add_question_banks.sql`
|
||||
|
||||
**实施步骤:**
|
||||
|
||||
1. 创建 QuestionBank 实体 + 迁移
|
||||
2. 创建 QuestionBankItem 实体 + 迁移
|
||||
3. 实现 QuestionBankService(CRUD)
|
||||
4. 实现 QuestionBankController(API)
|
||||
5. 注册模块
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 可以创建题库
|
||||
- [ ] 可以添加/编辑/删除题目
|
||||
- [ ] 可以设置题目维度/难度/答案
|
||||
- [ ] 支持审核流程(草稿→待审核→发布)
|
||||
|
||||
---
|
||||
|
||||
### Task 2: AI批量生成
|
||||
|
||||
**目标:** 调用AI批量生成题目,待人工审核
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/question-bank.service.ts` (generate)
|
||||
- Modify: `server/src/assessment/graph/nodes/generator.node.ts` (复用)
|
||||
|
||||
**实施步骤:**
|
||||
|
||||
1. 实现批量生成逻辑
|
||||
2. 调用generator node生成题目
|
||||
3. 保存为草稿状态
|
||||
4. 提供审核界面
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 点击生成按钮,调用AI生成题目
|
||||
- [ ] 生成的题目进入草稿状态
|
||||
- [ ] 可以批量审核通过
|
||||
|
||||
---
|
||||
|
||||
### Task 3: 题目抽取逻��
|
||||
|
||||
**目标:** 评估时从题库维度均衡随机抽取
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/question-bank.service.ts` (selectQuestions)
|
||||
|
||||
**实施步骤:**
|
||||
|
||||
1. 实现维度均衡抽取算法
|
||||
2. 处理题目不足情况
|
||||
3. 单元测试
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 抽取的题目维度大致均衡
|
||||
- [ ] 不重复抽取同一题目
|
||||
- [ ] 数量不足时有补充逻辑
|
||||
|
||||
---
|
||||
|
||||
### Task 4: 评估接入题库
|
||||
|
||||
**目标:** 评估优先使用题库题目
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/assessment.service.ts` (startSession)
|
||||
|
||||
**实施步骤:**
|
||||
|
||||
1. 修改startSession逻辑
|
||||
2. 先查询题库
|
||||
3. 题库不足时使用实时生成
|
||||
4. 测试完整流程
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 有题库时优先从题库抽取
|
||||
- [ ] 题库不足时使用实时生成
|
||||
- [ ] 两者都可正常工作
|
||||
|
||||
---
|
||||
|
||||
### Task 5: 统计页面
|
||||
|
||||
**目标:** 独立统计页面
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/assessment/assessment.controller.ts` (stats API)
|
||||
- Create: `web/src/services/assessmentStatsService.ts`
|
||||
- Create: `web/components/views/AssessmentStatsView.tsx`
|
||||
- Modify: `web/index.tsx` (路由)
|
||||
- Modify: `web/components/layouts/WorkspaceLayout.tsx` (侧边栏)
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 统计卡片显示正确
|
||||
- [ ] 支持时间筛选
|
||||
- [ ] 管理员可查看所有用户
|
||||
- [ ] 历史记录表格完整
|
||||
|
||||
---
|
||||
|
||||
### Task 6: 选择题渲染
|
||||
|
||||
**目标:** 前端支持选择题渲染
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/components/views/AssessmentView.tsx`
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 选择题显示选项按钮
|
||||
- [ ] 点击选项提交答案
|
||||
|
||||
---
|
||||
|
||||
### Task 7: 判断题渲染
|
||||
|
||||
**目标:** 前端支持判断题渲染
|
||||
|
||||
**Files:**
|
||||
- Modify: `web/components/views/AssessmentView.tsx`
|
||||
|
||||
**验收标准:**
|
||||
- [ ] 判断题显示True/False按钮
|
||||
- [ ] 点击提交答案
|
||||
|
||||
---
|
||||
|
||||
### Task 8: 稳定性优化
|
||||
|
||||
**目标:** 提高AI生成稳定性
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/assessment/graph/nodes/generator.node.ts`
|
||||
|
||||
**实施步骤:**
|
||||
|
||||
1. 添加重试逻辑
|
||||
2. 优化prompt
|
||||
3. 错误处理
|
||||
|
||||
---
|
||||
|
||||
## 七、执行顺序
|
||||
|
||||
```
|
||||
Task 1 (题库管理)
|
||||
↓
|
||||
Task 2 (AI批量生成)
|
||||
↓
|
||||
Task 3 (抽取逻辑)
|
||||
↓
|
||||
Task 4 (评估接入)
|
||||
↓
|
||||
Task 5 (统计页面)
|
||||
↓
|
||||
Task 6 (选择题)
|
||||
↓
|
||||
Task 7 (判断题)
|
||||
↓
|
||||
Task 8 (稳定性)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 八、技术要点
|
||||
|
||||
### 8.1 维度均衡算法
|
||||
|
||||
```typescript
|
||||
// 伪代码
|
||||
function selectQuestions(bankId: string, count: number): Question[] {
|
||||
const allItems = getPublishedItems(bankId);
|
||||
const dimensions = ['prompt', 'llm', 'ide', 'devPattern', 'workCapability'];
|
||||
const selected: Question[] = [];
|
||||
const usedIds = new Set<string>();
|
||||
|
||||
// 轮询从各维度选取
|
||||
let dimIdx = 0;
|
||||
while (selected.length < count && dimIdx < count * dimensions.length) {
|
||||
const dim = dimensions[dimIdx % dimensions.length];
|
||||
const available = allItems.filter(
|
||||
(i) => i.dimension === dim && !usedIds.has(i.id)
|
||||
);
|
||||
|
||||
if (available.length > 0) {
|
||||
const random = available[Math.floor(Math.random() * available.length)];
|
||||
selected.push(random);
|
||||
usedIds.add(random.id);
|
||||
}
|
||||
dimIdx++;
|
||||
}
|
||||
|
||||
// 补充不足的题目(随机)
|
||||
if (selected.length < count) {
|
||||
const remaining = allItems.filter((i) => !usedIds.has(i.id));
|
||||
// ... 随机补充
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
```
|
||||
|
||||
### 8.2 审核工作流
|
||||
|
||||
```
|
||||
草稿(DRAFT) → 提交审核 → 待审核(PENDING_REVIEW)
|
||||
↓
|
||||
审核通过 → 已发布(PUBLISHED)
|
||||
↓
|
||||
审核拒绝 → 草稿(DRAFT) + 意见
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 九、注意事项
|
||||
|
||||
1. **权限控制**:题库管理、统计只对管理员开放
|
||||
2. **数据隔离**:用户只能看到自己的评估记录,管理员可见所有
|
||||
3. **维度映射**:确保题目维度与评估维度一致
|
||||
4. **题库状态**:只有已发布的题目才能被抽取
|
||||
5. **回退机制**:题库不足时使用实时生成作为备用
|
||||
|
||||
---
|
||||
|
||||
## Plan Complete
|
||||
|
||||
**Two execution options:**
|
||||
|
||||
**1. Subagent-Driven (this session)** - 继续在此会话,使用 subagent 逐个执行任务
|
||||
|
||||
**2. Parallel Session (separate)** - 开新会话使用 executing-plans,分批执行
|
||||
|
||||
**Which approach?**
|
||||
@@ -1,517 +0,0 @@
|
||||
# AuraK 人才测评体系完整实施计划
|
||||
|
||||
> **文档状态**: 待评审
|
||||
> **创建日期**: 2026-04-23
|
||||
> **版本**: 1.0
|
||||
|
||||
---
|
||||
|
||||
## 一、系统概述
|
||||
|
||||
### 1.1 目标
|
||||
|
||||
构建一个完整的AI人才测评体系,实现"选→育→评→用"闭环。聚焦在**评(认证)阶段**,形成包含题库管理、评估执行、成绩管理、证书管理的完整系统。
|
||||
|
||||
### 1.2 核心流程
|
||||
|
||||
```
|
||||
人才输入 → 评估执行 → 结果输出 → 应用决策
|
||||
↑___________↓
|
||||
持续反馈
|
||||
```
|
||||
|
||||
### 1.3 评估发起模式
|
||||
|
||||
| 场景 | 发起方式 |
|
||||
|------|---------|
|
||||
| 新人入职认证 | 管理员发起(强制) |
|
||||
| 认证后自评 | 学员可自评 |
|
||||
|
||||
---
|
||||
|
||||
## 二、组织架构与权限设计
|
||||
|
||||
### 2.1 组织结构
|
||||
|
||||
```
|
||||
公司
|
||||
├── 本部A
|
||||
│ ├── 开发部
|
||||
│ └── 其他部门
|
||||
└── 本部B
|
||||
├── 开发部
|
||||
└── 其他部门
|
||||
```
|
||||
|
||||
### 2.2 角色权限矩阵
|
||||
|
||||
| 角色 | 查看自己 | 查看本部门 | 查看全公司 | 题库管理 | 发起评估 | 复查 |
|
||||
|------|---------|-----------|-----------|---------|---------|------|
|
||||
| 学员 | ✅ | ❌ | ❌ | ❌ | 自评 | ❌ |
|
||||
| 开发部长 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| 本部长 | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
|
||||
| 公司高管 | ✅ | ✅ | ✅ | ❌ | ❌ | ❌ |
|
||||
| 管理员 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| 讲师 | ✅ | ✅ | ✅ | ❌ | ❌ | ✅ |
|
||||
|
||||
### 2.3 权限规则
|
||||
|
||||
- 学员只能看自己的历史成绩
|
||||
- 各级管理者可查看下属员工成绩
|
||||
- 管理员可查看全部数据、管理题库
|
||||
- 讲师仅用于复查调整分数
|
||||
|
||||
---
|
||||
|
||||
## 三、模块A:题库管理
|
||||
|
||||
### 3.1 功能清单
|
||||
|
||||
| 功能 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 创建题库 | 关联知识库,设定题目范围 | P0 |
|
||||
| 单题管理 | 增删改查,支持简答/选择/判断 | P0 |
|
||||
| AI批量生成 | 按模板维度配置生成待审题目 | P0 |
|
||||
| 智能标注 | AI自动标注维度、难度 | P1 |
|
||||
| 审核流程 | 草稿→待审→发布/否决 | P0 |
|
||||
| 相似检测 | 检测与已有题目重复度 | P2 |
|
||||
| 查询统计 | 多条件筛选、维度分布、使用统计 | P1 |
|
||||
|
||||
### 3.2 题目属性
|
||||
|
||||
| 属性 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| questionText | string | 题干文字 |
|
||||
| questionType | enum | SHORT_ANSWER/MULTIPLE_CHOICE/TRUE_FALSE |
|
||||
| options | string[] | ABCD选项 |
|
||||
| correctAnswer | string | 正确答案 |
|
||||
| keyPoints | string[] | 评分要点 |
|
||||
| difficulty | enum | STANDARD/ADVANCED/SPECIALIST |
|
||||
| dimension | enum | PROMPT/LLM/IDE/DEV_PATTERN/WORK_CAPABILITY |
|
||||
| basis | string | 出题依据 |
|
||||
|
||||
### 3.3 题目与知识库关联
|
||||
|
||||
- **方案**:题目不单独关联知识库
|
||||
- 由模板指定知识库范围,范围内题目均可被抽取
|
||||
- 模板预设各维度的题目数量
|
||||
|
||||
### 3.4 审核流程
|
||||
|
||||
```
|
||||
草稿(DRAFT)
|
||||
↓ [提交审核]
|
||||
待审核(PENDING_REVIEW)
|
||||
↓ [通过] ↓ [否决]
|
||||
已发布(PUBLISHED) 草稿(DRAFT)
|
||||
+ 审核意见
|
||||
```
|
||||
|
||||
### 3.5 版本管理
|
||||
|
||||
- 简化版:仅记录最近一次修改时间和修改人
|
||||
|
||||
### 3.6 数据模型
|
||||
|
||||
```typescript
|
||||
// QuestionBank 实体
|
||||
{
|
||||
id: string;
|
||||
templateId: string; // 关联模板
|
||||
name: string; // 题库名称
|
||||
description: string; // 描述
|
||||
status: enum; // DRAFT/PENDING_REVIEW/PUBLISHED
|
||||
createdBy: string;
|
||||
reviewedBy: string; // 审核人
|
||||
reviewedAt: Date; // 审核时间
|
||||
reviewComment: string; // 审核意见
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}
|
||||
|
||||
// QuestionBankItem 实体
|
||||
{
|
||||
id: string;
|
||||
bankId: string; // 关联题库
|
||||
questionText: string; // 题干
|
||||
questionType: enum; // 题型
|
||||
options: string[]; // 选项
|
||||
correctAnswer: string; // 答案
|
||||
keyPoints: string[]; // 关键点
|
||||
difficulty: enum; // 难度
|
||||
dimension: enum; // 维度
|
||||
basis: string; // 出题依据
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.7 API设计
|
||||
|
||||
```typescript
|
||||
// 题库CRUD
|
||||
POST /api/question-banks // 创建题库
|
||||
GET /api/question-banks // 列表(分页)
|
||||
GET /api/question-banks/:id // 详情
|
||||
PUT /api/question-banks/:id // 更新
|
||||
DELETE /api/question-banks/:id // 删除
|
||||
|
||||
// 题目管理
|
||||
POST /api/question-banks/:bankId/items // 添加题目
|
||||
PUT /api/question-banks/:bankId/items/:id // 更新题目
|
||||
DELETE /api/question-banks/:bankId/items/:id // 删除题目
|
||||
|
||||
// 批量操作
|
||||
POST /api/question-banks/:bankId/generate // AI批量生成
|
||||
POST /api/question-banks/:bankId/batch-add // 批量导入
|
||||
|
||||
// 审核流程
|
||||
PUT /api/question-banks/:id/submit // 提交审核
|
||||
PUT /api/question-banks/:id/review // 审核
|
||||
PUT /api/question-banks/:id/publish // 发布
|
||||
PUT /api/question-banks/:id/unpublish // 下架
|
||||
|
||||
// 查询
|
||||
GET /api/question-banks/by-template/:templateId // 按模板查询
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 四、模块B:评估执行
|
||||
|
||||
### 4.1 功能清单
|
||||
|
||||
| 功能 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 题目抽取 | 按模板/维度均衡/高频优先抽取 | P0 |
|
||||
| 答题交互 | 展示题目、接收答案、即时反馈 | P0 |
|
||||
| AI评分 | 按关键点评分,0-10分 | P0 |
|
||||
| 追问机制 | 预置追问+超时降级 | P0 |
|
||||
| 时间控制 | 单题限时+总时长限制 | P1 |
|
||||
| 成绩判定 | ≥6分通过 | P0 |
|
||||
|
||||
### 4.2 题目抽取算法
|
||||
|
||||
```typescript
|
||||
function selectQuestions(bankId, templateConfig) {
|
||||
// 1. 按模板指定的知识库范围查已发布题目
|
||||
// 2. 按维度比例随机抽取
|
||||
// 3. 高频题目优先被抽(不限制重复次数)
|
||||
// 4. 不足时提示"题库不足"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 追问机制
|
||||
|
||||
| 方案 | 说明 |
|
||||
|------|------|
|
||||
| 预置追问 | 题库中预存追问内容 |
|
||||
| 实时补充 | 超时则降级跳过 |
|
||||
| 追问限制 | 最多2次追问 |
|
||||
|
||||
### 4.4 时间控制
|
||||
|
||||
| 限制 | 说明 | 默认值 |
|
||||
|------|------|--------|
|
||||
| 单题限时 | 超时强制下一题 | 300秒 |
|
||||
| 总时长限制 | 超时强制提交 | 1800秒 |
|
||||
|
||||
### 4.5 评估流程
|
||||
|
||||
```
|
||||
开始评估
|
||||
↓
|
||||
加载模板(数量/维度/时长)
|
||||
↓
|
||||
从题库抽取题目(维度均衡)
|
||||
↓
|
||||
展示第1题
|
||||
↓
|
||||
用户答题
|
||||
↓
|
||||
AI评分(按关键点0-10分)
|
||||
↓
|
||||
追问?(预置+超时降级)
|
||||
↓
|
||||
下一题(循环至最后一题)
|
||||
↓
|
||||
生成报告 + 判断通过/未通过(≥6分)
|
||||
↓
|
||||
通过 → 发放证书
|
||||
```
|
||||
|
||||
### 4.6 API设计
|
||||
|
||||
```typescript
|
||||
// 评估管理
|
||||
POST /api/assessment/start // 发起评估
|
||||
GET /api/assessment/:id/state // 获取状态
|
||||
POST /api/assessment/:id/answer // 提交答案
|
||||
|
||||
// 评估管理(管理员)
|
||||
GET /api/assessment // 所有评估列表
|
||||
GET /api/assessment/:id // 评估详情
|
||||
DELETE /api/assessment/:id // 删除评估
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 五、模块C:成绩管理
|
||||
|
||||
### 5.1 功能清单
|
||||
|
||||
| 功能 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 成绩查看 | 按权限查看成绩 | P0 |
|
||||
| 统计报表 | 通过率/分数/趋势/雷达图 | P1 |
|
||||
| 历史管理 | 保留最近3次 | P1 |
|
||||
| 导出功能 | Excel/PDF/CSV | P1 |
|
||||
| 复查功能 | 讲师调整分数 | P2 |
|
||||
|
||||
### 5.2 统计维度
|
||||
|
||||
| 维度 | 说明 |
|
||||
|------|------|
|
||||
| 通过人数/通过率 | 整体和分组 |
|
||||
| 平均分/最高分/最低分 | 按组统计 |
|
||||
| 各维度平均分 | 雷达图数据 |
|
||||
| 评估次数趋势 | 时序折线图 |
|
||||
|
||||
### 5.3 报表页面
|
||||
|
||||
```
|
||||
┌──────────────────────────────────────────┐
|
||||
│ 成绩统计 [导出] [筛选] │
|
||||
├──────────────────────────────────────────┤
|
||||
│ 总人数 │ 通过率 │ 平均分 │ 待审核 │
|
||||
│ 156 │ 82% │ 7.5 │ 3 │
|
||||
├──────────────────────────────────────────┤
|
||||
│ 雷达图(各维度平均分) │
|
||||
│ prompt ████████ 8.2 │
|
||||
│ llm ███████░░ 7.6 │
|
||||
│ ide ██████░░░ 6.8 │
|
||||
│ devPattern █████░░░░ 6.2 │
|
||||
│ workCap ██████░░░░ 6.5 │
|
||||
├──────────────────────────────────────────┤
|
||||
│ 成绩列表(按组织筛选) │
|
||||
│ 姓名 │ 部门 │ 分数 │ 状态 │ 时间 │ 操作 │
|
||||
│ [查看] │ [复查] │
|
||||
└────────────────���─────────────────────────┘
|
||||
```
|
||||
|
||||
### 5.4 API设计
|
||||
|
||||
```typescript
|
||||
// 成绩统计
|
||||
GET /api/assessment/stats // 当前用户统计
|
||||
GET /api/assessment/stats/admin // 管理员统计
|
||||
Query: startDate, endDate, templateId, groupId
|
||||
|
||||
// 复查
|
||||
PUT /api/assessment/:id/review // 调整分数
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 六、模块D:证书管理
|
||||
|
||||
### 6.1 功能清单
|
||||
|
||||
| 功能 | 说明 | 优先级 |
|
||||
|------|------|--------|
|
||||
| 证书生成 | 通过即发 | P1 |
|
||||
| 证书下载 | PDF导出 | P1 |
|
||||
| 证书验真 | 二维码/ID查询 | P1 |
|
||||
|
||||
### 6.2 证书内容
|
||||
|
||||
| 字段 | 说明 |
|
||||
|------|------|
|
||||
| certificateId | 唯一ID |
|
||||
| userId | 持证人 |
|
||||
| templateId | 评估模板 |
|
||||
| totalScore | 总分 |
|
||||
| passedAt | 通过时间 |
|
||||
| qrCode | 防伪二维码 |
|
||||
|
||||
### 6.3 有效期
|
||||
|
||||
- **永久有效**:一次通过,终身有效
|
||||
|
||||
### 6.4 证书样式
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────┐
|
||||
│ [公司LOGO] │
|
||||
│ │
|
||||
│ 能力认证证书 │
|
||||
│ │
|
||||
│ 兹证明 [学员姓名] 通过 │
|
||||
│ [评估模板名称] 能力评估 │
|
||||
│ 总分:[分数] 分 │
|
||||
│ │
|
||||
│ 发证日期:[日期] │
|
||||
│ 证书编号:[ID] │
|
||||
│ │
|
||||
│ [二维码] │
|
||||
│ 验证真伪 │
|
||||
└────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 6.5 API设计
|
||||
|
||||
```typescript
|
||||
// 证书
|
||||
GET /api/assessment/:id/certificate // 获取证书
|
||||
GET /api/assessment/:id/certificate/download // 下载PDF
|
||||
POST /api/assessment/certificate/verify // 验证证书
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 七、实施计划
|
||||
|
||||
### 7.1 任务分解
|
||||
|
||||
| Phase | Task | 内容 | 优先级 |
|
||||
|-------|------|------|--------|
|
||||
| 1 | A1 | 题库实体创建 | P0 |
|
||||
| 1 | A2 | 题目实体创建 | P0 |
|
||||
| 1 | A3 | 题库Service/Controller | P0 |
|
||||
| 1 | A4 | AI批量生成 | P0 |
|
||||
| 1 | A5 | 审核流程 | P0 |
|
||||
| 2 | B1 | 题目抽取算法 | P0 |
|
||||
| 2 | B2 | 评估流程接入题库 | P0 |
|
||||
| 2 | B3 | 追问预置+降级 | P0 |
|
||||
| 2 | B4 | 时间控制 | P1 |
|
||||
| 3 | C1 | 统计API | P1 |
|
||||
| 3 | C2 | 报表前端 | P1 |
|
||||
| 3 | C3 | 历史管理 | P1 |
|
||||
| 3 | C4 | 导出功能 | P1 |
|
||||
| 4 | D1 | 证书实体 | P1 |
|
||||
| 4 | D2 | 证书生成 | P1 |
|
||||
| 4 | D3 | 证书下载/验真 | P1 |
|
||||
| 5 | E1 | 复查功能 | P2 |
|
||||
|
||||
### 7.2 执行顺序
|
||||
|
||||
```
|
||||
Phase 1 (题库)
|
||||
↓
|
||||
Phase 2 (评估)
|
||||
↓
|
||||
Phase 3 (成绩)
|
||||
↓
|
||||
Phase 4 (证书)
|
||||
↓
|
||||
Phase 5 (复查)
|
||||
```
|
||||
|
||||
### 7.3 里程碑
|
||||
|
||||
| 里程碑 | 内容 | 时间 |
|
||||
|--------|------|------|
|
||||
| M1 | 题库管理基础功能 | 1周 |
|
||||
| M2 | 评估流程优化 | 1周 |
|
||||
| M3 | 成绩报表 | 1周 |
|
||||
| M4 | 证书功能 | 1周 |
|
||||
| M5 | 测试优化 | 1周 |
|
||||
|
||||
---
|
||||
|
||||
## 八、待评审问题
|
||||
|
||||
### 8.1 功能确认
|
||||
|
||||
| # | 问题 | 选项 |
|
||||
|---|------|------|
|
||||
| 1 | 题目抽取是否允许重复? | A.不可重复 B.可重复(推荐) C.限制次数 |
|
||||
| 2 | 追问方案选择? | A.预置 B.实时 C.预置+降级(推荐) |
|
||||
| 3 | 单题限时默认值? | 默认300秒 |
|
||||
| 4 | 总时长限制默认值? | 默认1800秒 |
|
||||
|
||||
### 8.2 待扩展功能
|
||||
|
||||
- 多维度得分(当前为二元制)
|
||||
- 组织架构动态调整
|
||||
- 定时评估任务
|
||||
|
||||
---
|
||||
|
||||
## 九、验收标准
|
||||
|
||||
- [ ] 题库支持创建/编辑/审核/发布
|
||||
- [ ] AI批量生成题目进入待审状态
|
||||
- [ ] 评估可从题库维度均衡抽取
|
||||
- [ ] 追问支持预置+降级
|
||||
- [ ] 成绩按组织架构权限隔离
|
||||
- [ ] 统计报表显示通过率/雷达图
|
||||
- [ ] 通过后自动生成证书
|
||||
- [ ] 证书可下载/验真
|
||||
- [ ] 历史记录保留最近3次
|
||||
|
||||
---
|
||||
|
||||
## 十、技术要点
|
||||
|
||||
### 10.1 维度均衡算法
|
||||
|
||||
```typescript
|
||||
function selectQuestions(bankId, count, dimensionRatio) {
|
||||
const dimensions = ['prompt', 'llm', 'ide', 'devPattern', 'workCapability'];
|
||||
const selected = [];
|
||||
const usedIds = new Set();
|
||||
|
||||
// 轮询从各维度选取
|
||||
let dimIdx = 0;
|
||||
while (selected.length < count) {
|
||||
const dim = dimensions[dimIdx % dimensions.length];
|
||||
const available = allItems.filter(
|
||||
(i) => i.dimension === dim && !usedIds.has(i.id) && i.status === 'PUBLISHED'
|
||||
);
|
||||
|
||||
if (available.length > 0) {
|
||||
// 高频题目优先
|
||||
const random = available.sort((a, b) => b.useCount - a.useCount)[0];
|
||||
selected.push(random);
|
||||
usedIds.add(random.id);
|
||||
}
|
||||
dimIdx++;
|
||||
}
|
||||
|
||||
return selected;
|
||||
}
|
||||
```
|
||||
|
||||
### 10.2 追问降级策略
|
||||
|
||||
```typescript
|
||||
function getFollowUp(question, userAnswer, followUpCount) {
|
||||
// 1. 检查预置追问
|
||||
if (question.predefinedFollowUps && followUpCount < question.predefinedFollowUps.length) {
|
||||
return question.predefinedFollowUps[followUpCount];
|
||||
}
|
||||
|
||||
// 2. 降级:跳过追问
|
||||
return null; // 进入下一题
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 附录
|
||||
|
||||
### A. 相关文档
|
||||
|
||||
- `docs/plans/2026-04-18-l1-talent-assessment-design.md` - L1设计
|
||||
- `docs/plans/2026-04-20-assessment-system-complete-plan.md` - 旧版计划
|
||||
|
||||
### B. 现有代码参考
|
||||
|
||||
- `server/src/assessment/` - 评估模块
|
||||
- `web/components/views/AssessmentView.tsx` - 前端评估界面
|
||||
|
||||
---
|
||||
|
||||
**文档状态**: 待评审后定稿
|
||||
**下一步**: 评审并确认各模块功能
|
||||
@@ -1,366 +0,0 @@
|
||||
# AuraK 人才评测系统代码整合性检查报告
|
||||
|
||||
> **文档状态**: ✅ 已完成修复
|
||||
> **创建日期**: 2026-05-14
|
||||
> **版本**: 1.1
|
||||
> **检查日期**: 2026-05-14
|
||||
> **修复日期**: 2026-05-14
|
||||
|
||||
---
|
||||
|
||||
## 一、检查背景
|
||||
|
||||
本次代码整合性检查从**各角色使用者**视角出发,对题目生成、测试、评估全流程进行系统性的审查。涵盖:
|
||||
|
||||
- 用户故事与功能矩阵
|
||||
- API 路由与参数传递
|
||||
- 业务流与关系链
|
||||
- 特殊场景与边界情况
|
||||
- 前后端集成度
|
||||
|
||||
---
|
||||
|
||||
## 二、角色与用户故事
|
||||
|
||||
### 2.1 角色定义
|
||||
|
||||
| 角色 | 说明 |
|
||||
|------|------|
|
||||
| 普通用户 (User) | 被评估者,参与测评答题 |
|
||||
| 管理员 (Admin) | 系统管理,全权限 |
|
||||
| 审核员 (Reviewer) | 题目/题库审核 |
|
||||
| 租户管理员 (Tenant Admin) | 租户内管理 |
|
||||
|
||||
### 2.2 用户故事矩阵
|
||||
|
||||
#### 普通用户
|
||||
|
||||
| 编号 | 用户故事 | API 端点 | 状态 |
|
||||
|------|---------|----------|------|
|
||||
| US-01 | 开始评估 | POST /assessment/start | ✅ |
|
||||
| US-02 | 回答问题 | POST /assessment/:id/answer | ✅ |
|
||||
| US-03 | 追问回答 | POST /assessment/:id/answer | ✅ |
|
||||
| US-04 | 查看历史(最新3条) | GET /assessment/history | ✅ |
|
||||
| US-05 | 查看最终报告 | GET /assessment/:id/state | ✅ |
|
||||
| US-06 | 下载证书 | GET /assessment/:id/certificate | ✅ |
|
||||
| US-07 | 导出Excel报告 | GET /assessment/:id/export/excel | ✅ |
|
||||
| US-08 | 导出PDF报告 | GET /assessment/:id/export/pdf | ✅ |
|
||||
| US-09 | 时间检查 | GET /assessment/:id/time-check | ✅ |
|
||||
| US-10 | 下一题计时 | POST /assessment/:id/next-question | ✅ |
|
||||
|
||||
#### 管理员
|
||||
|
||||
| 编号 | 用户故事 | API 端点 | 状态 |
|
||||
|------|---------|----------|------|
|
||||
| AM-01 | 创建模板 | POST /assessment/templates | ✅ |
|
||||
| AM-02 | 编辑模板 | PUT /assessment/templates/:id | ✅ |
|
||||
| AM-03 | 删除模板 | DELETE /assessment/templates/:id | ✅ |
|
||||
| AM-04 | 创建题库 | POST /question-banks | ✅ |
|
||||
| AM-05 | 编辑题库 | PUT /question-banks/:id | ✅ |
|
||||
| AM-06 | 删除题库 | DELETE /question-banks/:id | ✅ |
|
||||
| AM-07 | AI生成题目 | POST /question-banks/:bankId/generate | ✅ |
|
||||
| AM-08 | 添加题目 | POST /question-banks/:bankId/items | ✅ |
|
||||
| AM-09 | 编辑题目 | PUT /question-banks/:bankId/items/:id | ✅ |
|
||||
| AM-10 | 删除题目 | DELETE /question-banks/:bankId/items/:id | ✅ |
|
||||
| AM-11 | 提交审核 | PUT /question-banks/:id/submit | ✅ |
|
||||
| AM-12 | 审核题库 | PUT /question-banks/:id/review | ✅ |
|
||||
| AM-13 | 发布题库 | PUT /question-banks/:id/publish | ✅ |
|
||||
| AM-14 | 批量审核题目 | POST /question-banks/:bankId/items/batch-review | ✅ |
|
||||
| AM-15 | 审核评估 | PUT /assessment/:id/review | ✅ |
|
||||
| AM-16 | 删除评估 | DELETE /assessment/:id | ✅ |
|
||||
| AM-17 | 查看统计 | GET /assessment/stats | ✅ |
|
||||
| AM-18 | 查看雷达图 | GET /assessment/stats/radar | ✅ |
|
||||
| AM-19 | 查看趋势图 | GET /assessment/stats/trend | ✅ |
|
||||
| AM-20 | 验证证书 | GET /assessment/certificate/verify/:id | ✅ |
|
||||
| AM-21 | 公开证书信息 | GET /assessment/certificate/public/:id | ✅ |
|
||||
|
||||
#### 审核员
|
||||
|
||||
| 编号 | 用户故事 | API 端点 | 状态 |
|
||||
|------|---------|----------|------|
|
||||
| RV-01 | 审核题库 | PUT /question-banks/:id/review | ✅ |
|
||||
| RV-02 | 批量审核题目 | POST /question-banks/:bankId/items/batch-review | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 三、发现的问题清单
|
||||
|
||||
### 3.1 高优先级问题
|
||||
|
||||
| 编号 | 问题描述 | 位置 | 类型 | 修复建议 |
|
||||
|------|---------|------|------|---------|
|
||||
| P1-01 | 两个 `Get('history')` 路由冲突,会导致第一个被覆盖 | assessment.controller.ts:109, :159 | 路由冲突 | 删除或重命名其中一个 |
|
||||
| P1-02 | 前端缺少多个API调用方法 | web/services/assessmentService.ts | 前端集成 | 添加缺失的API方法 |
|
||||
| P1-03 | 前端缺少题库/模板服务 | web/services/ | 前端集成 | 新增 QuestionBankService, TemplateService |
|
||||
|
||||
### 3.2 中优先级问题
|
||||
|
||||
| 编号 | 问题描述 | 位置 | 类型 | 修复建议 |
|
||||
|------|---------|------|------|---------|
|
||||
| P2-01 | QuestionBank.status 字段无默认值 | question-bank.entity.ts:53 | Entity配置 | 添加 default: QuestionBankStatus.DRAFT |
|
||||
| P2-02 | 状态前置检查缺失 | question-bank.service.ts | 业务逻辑 | 审核/发布/提交前检查当前状态 |
|
||||
| P2-03 | 缺少强制结束评估功能 | assessment.controller.ts | 功能缺失 | 添加 POST /assessment/:id/force-end |
|
||||
|
||||
### 3.3 低优先级问题
|
||||
|
||||
| 编号 | 问题描述 | 位置 | 类型 | 修复建议 |
|
||||
|------|---------|------|------|---------|
|
||||
| P3-01 | `@Put` 未导入 | assessment.controller.ts:224 | 导入缺失 | 在 import 中添加 Put |
|
||||
| P3-02 | 缺少操作审计日志 | 整体设计 | 架构设计 | 可选:添加审计日志表 |
|
||||
| P3-03 | 批量操作功能缺失 | assessment.service.ts | 功能缺失 | 可选:批量删除/导出 |
|
||||
| P3-04 | 无事务/并发控制 | 整体设计 | 架构设计 | 可选:后续优化 |
|
||||
|
||||
---
|
||||
|
||||
## 四、特殊场景检查
|
||||
|
||||
### 4.1 考核过程特殊场景
|
||||
|
||||
| 场景 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 时间超时处理 | ✅ | checkTimeLimits, isTotalTimeout/isQuestionTimeout |
|
||||
| 中断恢复 | ✅ | interruptAfter + MemorySaver |
|
||||
| 追问场景 | ✅ | shouldFollowUp 逻辑 |
|
||||
| 网络中断恢复 | ⚠️ | 依赖LangGraph MemorySaver,无重连机制 |
|
||||
| 并发答题 | ⚠️ | 无分布式锁,可能冲突 |
|
||||
| 中途放弃 | ✅ | 用户可删除自己的session |
|
||||
|
||||
### 4.2 管理员特殊场景
|
||||
|
||||
| 场景 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 强制结束评估 | ❌ | 无此功能 |
|
||||
| 批量删除评估 | ❌ | 无批量删除API |
|
||||
| 批量导出数据 | ❌ | 无批量导出 |
|
||||
| 强制重置分数 | ⚠️ | 需通过 review 接口手动调整 |
|
||||
|
||||
### 4.3 业务规则验证
|
||||
|
||||
| 规则 | 状态 | 说明 |
|
||||
|------|------|------|
|
||||
| 只能审核已提交的题库 | ⚠️ | 无状态前置检查 |
|
||||
| 只能发布已审核通过的 | ⚠️ | 无状态前置检查 |
|
||||
| 只能在评估进行中回答 | ⚠️ | 无状态前置检查 |
|
||||
| 只能在评估完成后生成证书 | ✅ | 已检查 |
|
||||
| 只能删除自己的评估(非admin) | ✅ | 已实现 |
|
||||
|
||||
---
|
||||
|
||||
## 五、API 路由检查
|
||||
|
||||
### 5.1 后端路由清单
|
||||
|
||||
| 端点 | 方法 | 功能 | 状态 |
|
||||
|------|------|------|------|
|
||||
| /assessment/start | POST | 开始评估 | ✅ |
|
||||
| /assessment/:id/answer | POST | 回答问题 | ✅ |
|
||||
| /assessment/:id/state | GET | 获取状态 | ✅ |
|
||||
| /assessment/:id/certificate | GET | 获取证书 | ✅ |
|
||||
| /assessment/:id/review | PUT | 审核评估 | ✅ |
|
||||
| /assessment/:id/time-check | GET | 时间检查 | ✅ |
|
||||
| /assessment/:id/next-question | POST | 下一题计时 | ✅ |
|
||||
| /assessment/:id/export/excel | GET | 导出Excel | ✅ |
|
||||
| /assessment/:id/export/pdf | GET | 导出PDF | ✅ |
|
||||
| /assessment/stats | GET | 统计数据 | ✅ |
|
||||
| /assessment/stats/radar | GET | 雷达图数据 | ✅ |
|
||||
| /assessment/stats/trend | GET | 趋势图数据 | ✅ |
|
||||
| /assessment/history | GET | 用户历史 | ⚠️ 冲突 |
|
||||
| /assessment/certificate/verify/:id | GET | 验证证书 | ✅ |
|
||||
| /assessment/certificate/public/:id | GET | 公开证书 | ✅ |
|
||||
| /assessment/templates | POST/GET | 模板CRUD | ✅ |
|
||||
| /question-banks | POST/GET | 题库CRUD | ✅ |
|
||||
| /question-banks/:id/submit | PUT | 提交审核 | ✅ |
|
||||
| /question-banks/:id/review | PUT | 审核题库 | ✅ |
|
||||
| /question-banks/:id/publish | PUT | 发布题库 | ✅ |
|
||||
| /question-banks/:bankId/items | CRUD | 题目CRUD | ✅ |
|
||||
| /question-banks/:bankId/generate | POST | AI生成题目 | ✅ |
|
||||
| /question-banks/:bankId/items/batch-review | POST | 批量审核 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 六、数据关系链
|
||||
|
||||
### 6.1 Entity 关系图
|
||||
|
||||
```
|
||||
AssessmentTemplate (1) ←→ (N) AssessmentSession
|
||||
↑
|
||||
│
|
||||
↓
|
||||
QuestionBank (1) ←→ (N) QuestionBankItem
|
||||
│
|
||||
↓
|
||||
AssessmentSession (1) ←→ (N) AssessmentQuestion
|
||||
│
|
||||
↓
|
||||
AssessmentQuestion (1) ←→ (N) AssessmentAnswer
|
||||
│
|
||||
↓
|
||||
AssessmentSession (1) ←→ (1) AssessmentCertificate
|
||||
```
|
||||
|
||||
### 6.2 字段继承关系
|
||||
|
||||
| 源 | 目标 | 字段 |
|
||||
|------|------|------|
|
||||
| AssessmentTemplate | AssessmentSession | name, keywords, questionCount, difficultyConfig, weightConfig, passingScore, totalTimeLimit, perQuestionTimeLimit |
|
||||
| TemplateService | startSession | templateId → templateId |
|
||||
| question-bank.service | startSession | bankId → questions |
|
||||
|
||||
---
|
||||
|
||||
## 七、前后端集成检查
|
||||
|
||||
### 7.1 前端缺失的 API 方法
|
||||
|
||||
| 后端API | 前端需要的方法 |
|
||||
|---------|---------------|
|
||||
| GET /assessment/:id/certificate | getCertificate() |
|
||||
| PUT /assessment/:id/review | reviewAssessment() |
|
||||
| GET /assessment/stats | getStats() |
|
||||
| GET /assessment/stats/radar | getRadarStats() |
|
||||
| GET /assessment/stats/trend | getTrendStats() |
|
||||
| GET /assessment/:id/export/excel | exportExcel() |
|
||||
| GET /assessment/:id/export/pdf | exportPdf() |
|
||||
| GET /assessment/:id/time-check | checkTimeLimits() |
|
||||
| POST /assessment/:id/next-question | startNextQuestion() |
|
||||
| POST /question-banks/* | questionBankService (缺失) |
|
||||
| POST /assessment/templates/* | templateService (缺失) |
|
||||
|
||||
---
|
||||
|
||||
## 八、修复优先级与计划
|
||||
|
||||
### 立即修复 (P0)
|
||||
|
||||
1. **P1-01**: 修复路由冲突 - 删除或重命名第109行的 Get('history')
|
||||
2. **P1-02**: 补全前端 assessmentService.ts 缺失的 API 方法
|
||||
3. **P1-03**: 新增前端题库/模板服务
|
||||
|
||||
### 短期修复 (P1)
|
||||
|
||||
4. **P2-01**: QuestionBank.status 添加默认值
|
||||
5. **P2-02**: 添加状态前置检查逻辑
|
||||
6. **P2-03**: 添加强制结束评估功能
|
||||
|
||||
### 后续优化 (P2)
|
||||
|
||||
7. **P3-01**: 修复 Put 导入
|
||||
8. **P3-02**: 审计日志 (可选)
|
||||
9. **P3-03**: 批量操作 (可选)
|
||||
10. **P3-04**: 事务控制 (可选)
|
||||
|
||||
---
|
||||
|
||||
## 九、版本信息
|
||||
|
||||
| 版本 | 日期 | 说明 |
|
||||
|------|------|------|
|
||||
| 1.0 | 2026-05-14 | 初始版本,涵盖代码整合性检查 |
|
||||
| 1.1 | 2026-05-14 | 修复路由冲突、前后端集成等问题 |
|
||||
|
||||
---
|
||||
|
||||
## 十、修复记录
|
||||
|
||||
### 2026-05-14 修复内容
|
||||
|
||||
| ID | 问题 | 修复文件 | 修复内容 |
|
||||
|----|------|---------|---------|
|
||||
| R1 | 路由冲突 | assessment.controller.ts | 删除第109行 Get() 方法 |
|
||||
| R2 | Put 导入缺失 | assessment.controller.ts | 添加 Put 到 import 解构 |
|
||||
| R3 | Req 导入缺失 | assessment.controller.ts | 添加 Req 到 import 解构 |
|
||||
| R4 | ForbiddenException 缺失 | assessment.controller.ts | 添加 ForbiddenException 导入 |
|
||||
| S1 | publish 状态检查缺失 | question-bank.service.ts | 添加 PUBLISHED/REJECTED 状态检查 |
|
||||
| M1 | QuestionBank status 默认值 | question-bank.entity.ts | 添加 default: DRAFT |
|
||||
| M2 | 强制结束评估功能 | assessment.service.ts | 添加 forceEndAssessment 方法 |
|
||||
| M3 | 强制结束端点 | assessment.controller.ts | 添加 POST :id/force-end |
|
||||
| F1 | 前端 API 方法缺失 | web/services/assessmentService.ts | 添加所有缺失的 API 方法 |
|
||||
|
||||
---
|
||||
|
||||
## 十一、修复后验证
|
||||
|
||||
### 11.1 路由清单 (修复后)
|
||||
|
||||
```
|
||||
POST /assessment/start - 开始评估
|
||||
POST /assessment/:id/answer - 回答问题
|
||||
GET /assessment/:id/state - 获取状态
|
||||
DELETE /assessment/:id - 删除评估
|
||||
GET /assessment/:id/certificate - 获取证书
|
||||
GET /assessment/certificate/verify/:certificateId - 验证证书
|
||||
GET /assessment/certificate/public/:sessionId - 公开证书
|
||||
GET /assessment/history - 用户历史 (最新3条)
|
||||
GET /assessment/stats - 统计数据
|
||||
GET /assessment/stats/radar - 雷达图数据
|
||||
GET /assessment/stats/trend - 趋势图数据
|
||||
PUT /assessment/:id/review - 审核评估
|
||||
GET /assessment/:id/time-check - 时间检查
|
||||
POST /assessment/:id/next-question - 下一题计时
|
||||
POST /assessment/:id/force-end - 强制结束 (admin)
|
||||
GET /assessment/:id/export/excel - 导出Excel
|
||||
GET /assessment/:id/export/pdf - 导出PDF
|
||||
```
|
||||
|
||||
### 11.2 参数传递链验证
|
||||
|
||||
| 功能 | Controller 参数 | Service 参数 | 状态 |
|
||||
|------|----------------|-------------|------|
|
||||
| startSession | knowledgeBaseId?, language?, templateId? | userId, kbId, tenantId, language, templateId | ✅ |
|
||||
| submitAnswer | answer, language | sessionId, userId, answer, language | ✅ |
|
||||
| review | newScore, comment | sessionId, newScore, comment, reviewerId, tenantId | ✅ |
|
||||
| forceEnd | sessionId | sessionId | ✅ |
|
||||
|
||||
### 11.3 状态检查验证
|
||||
|
||||
| 操作 | 前置检查 | 状态 |
|
||||
|------|---------|------|
|
||||
| submitForReview | DRAFT | ✅ |
|
||||
| review | PENDING_REVIEW | ✅ |
|
||||
| publish | PUBLISHED/REJECTED | ✅ 已修复 |
|
||||
| reviewAssessment | COMPLETED | ✅ |
|
||||
| forceEnd | 无限制 | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 十二、前端画面验证修复
|
||||
|
||||
### 12.1 AssessmentView (主评估界面) 修复
|
||||
|
||||
| 功能 | 修复前 | 修复后 | 状态 |
|
||||
|------|--------|--------|------|
|
||||
| 查看证书 | 未实现 | 添加 getCertificate 调用 | ✅ |
|
||||
| 导出PDF报告 | 按钮无功能 | 添加 exportPdf 调用 | ✅ |
|
||||
| 导出Excel报告 | 未实现 | 添加 exportExcel 调用 | ✅ |
|
||||
| 时间显示 | 未实现 | 添加 checkTimeLimits 定期调用 | ✅ |
|
||||
|
||||
### 12.2 画面功能验证
|
||||
|
||||
| 画面 | 用户故事覆盖 | API完整性 | 参数传递 | 闭环 |
|
||||
|------|-------------|-----------|---------|------|
|
||||
| AssessmentView | 10/10 | ✅ | ✅ | ✅ |
|
||||
| AssessmentStatsView | 7/7 | ✅ | ✅ | ✅ |
|
||||
| AssessmentTemplateManager | 5/5 | ✅ | ✅ | ✅ |
|
||||
| QuestionBankView | 6/6 | ✅ | ✅ | ✅ |
|
||||
| QuestionBankDetailView | 7/7 | ✅ | ✅ | ✅ |
|
||||
|
||||
---
|
||||
|
||||
## 十三、附录
|
||||
|
||||
### A. 相关文件路径
|
||||
|
||||
- Controller: `server/src/assessment/assessment.controller.ts`
|
||||
- Service: `server/src/assessment/assessment.service.ts`
|
||||
- Entities: `server/src/assessment/entities/`
|
||||
- Graph: `server/src/assessment/graph/`
|
||||
- 前端服务: `web/services/assessmentService.ts`
|
||||
- 前端题库服务: `web/services/questionBankService.ts`
|
||||
- 前端模板服务: `web/services/templateService.ts`
|
||||
|
||||
### B. 参考文档
|
||||
|
||||
- `docs/plans/2026-04-23-assessment-system-full-plan-v2.md`
|
||||
- `docs/debugging-checklist.md`
|
||||
- `docs/admin-credentials.md`
|
||||
@@ -1,955 +0,0 @@
|
||||
# Feishu Bot Integration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Enable Feishu (飞书) bot integration as a standalone **Plugin** that users can manage via the system's "Plugins" menu. This allows users to bind bots, interact with RAG, and access assessment features in a modular, plug-and-play fashion.
|
||||
|
||||
**Architecture:** Implement the integration as a pluggable `FeishuModule`. It acts as a bridge between Feishu Open Platform and AuraK's core services. The plugin is managed through the new `/plugins` workspace, isolating its UI and backend logic from the core system.
|
||||
|
||||
**Tech Stack:**
|
||||
- NestJS (existing)
|
||||
- Feishu Open Platform APIs (im/v1/messages, auth/v3/tenant_access_token)
|
||||
- Event subscription (Webhooks)
|
||||
- Existing: ChatService, AssessmentService, JWT Auth
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
┌─────────────┐ ┌──────────────┐ ┌─────────────┐
|
||||
│ Feishu User│─────▶│ Feishu Server│─────▶│ AuraK API │
|
||||
│ (User A) │◀─────│ Webhook │◀─────│ /feishu/* │
|
||||
└─────────────┘ └──────────────┘ └─────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────┐
|
||||
│ FeishuModule│
|
||||
│ - Bot Entity│
|
||||
│ - Message │
|
||||
│ - Events │
|
||||
└─────────────┘
|
||||
│
|
||||
┌───────────────────────┼───────────────────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
|
||||
│ ChatService │ │AssessmentSvc│ │ PluginsSvc │
|
||||
│ (RAG Q&A) │ │ (评测对话) │ │ (插件状态管理)│
|
||||
└─────────────┘ └─────────────┘ └─────────────┘
|
||||
```
|
||||
|
||||
**Plugin Isolation Strategy:**
|
||||
- **Backend**: `FeishuModule` is a standalone NestJS module. It handles its own database entities and webhook logic.
|
||||
- **Frontend**: Integrated as a sub-view within `/plugins`. When the Feishu plugin is "enabled", its configuration UI is rendered.
|
||||
- **Decoupling**: Communicates with core services via standard service calls or an event-subscriber pattern.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
|
||||
### New Files to Create
|
||||
|
||||
```
|
||||
server/src/
|
||||
├── feishu/
|
||||
│ ├── feishu.module.ts # Module definition
|
||||
│ ├── feishu.controller.ts # Webhook endpoints
|
||||
│ ├── feishu.service.ts # Business logic
|
||||
│ ├── feishu.gateway.ts # Event handling (optional)
|
||||
│ ├── entities/
|
||||
│ │ └── feishu-bot.entity.ts # Bot configuration entity
|
||||
│ ├── dto/
|
||||
│ │ ├── create-bot.dto.ts # Create bot DTO
|
||||
│ │ ├── bind-bot.dto.ts # Bind bot DTO
|
||||
│ │ └── feishu-webhook.dto.ts # Webhook event DTO
|
||||
│ └── interfaces/
|
||||
│ └── feishu.interface.ts # TypeScript interfaces
|
||||
```
|
||||
|
||||
### Existing Files to Modify
|
||||
|
||||
```
|
||||
server/src/
|
||||
├── app.module.ts # Import FeishuModule
|
||||
├── user/
|
||||
│ └── user.entity.ts # Add one-to-many relation to FeishuBot
|
||||
├── user/user.service.ts # Add methods to get/set Feishu binding
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: Entity and DTO Definitions
|
||||
|
||||
### Task 1.1: Create FeishuBot Entity
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/feishu/entities/feishu-bot.entity.ts`
|
||||
|
||||
- [ ] **Step 1: Create the FeishuBot entity file**
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Entity,
|
||||
PrimaryGeneratedColumn,
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
UpdateDateColumn,
|
||||
ManyToOne,
|
||||
JoinColumn,
|
||||
} from 'typeorm';
|
||||
import { User } from '../../user/user.entity';
|
||||
|
||||
@Entity('feishu_bots')
|
||||
export class FeishuBot {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ name: 'user_id' })
|
||||
userId: string; // Plugin manages its own relationship to the core User
|
||||
|
||||
@ManyToOne(() => User)
|
||||
@JoinColumn({ name: 'user_id' })
|
||||
user: User;
|
||||
|
||||
@Column({ name: 'app_id', length: 64 })
|
||||
appId: string;
|
||||
// ... (rest of the fields as defined previously)
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Decoupled Relation**
|
||||
Instead of modifying the core `User` entity directly, the `FeishuBot` entity maintains its own reference to `User`. This keeps the core system clean and allows the plugin to be purely optional.
|
||||
|
||||
|
||||
- [ ] **Step 3: Create DTOs**
|
||||
|
||||
Create: `server/src/feishu/dto/create-bot.dto.ts`
|
||||
```typescript
|
||||
import { IsString, IsNotEmpty, IsOptional, IsBoolean } from 'class-validator';
|
||||
|
||||
export class CreateFeishuBotDto {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
appId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
appSecret: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
verificationToken?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
encryptKey?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
botName?: string;
|
||||
|
||||
@IsBoolean()
|
||||
@IsOptional()
|
||||
enabled?: boolean;
|
||||
}
|
||||
```
|
||||
|
||||
Create: `server/src/feishu/dto/bind-bot.dto.ts`
|
||||
```typescript
|
||||
import { IsString, IsNotEmpty, IsUUID } from 'class-validator';
|
||||
|
||||
export class BindFeishuBotDto {
|
||||
@IsUUID()
|
||||
@IsNotEmpty()
|
||||
botId: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
verificationCode?: string; // Optional: 用于验证绑定关系
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: Core Service Implementation
|
||||
|
||||
### Task 2.1: Create Feishu Service
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/feishu/feishu.service.ts`
|
||||
|
||||
- [ ] **Step 1: Implement FeishuService with token management and message sending**
|
||||
|
||||
```typescript
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { Repository } from 'typeorm';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { HttpService } from '@nestjs/axios';
|
||||
import { FeishuBot } from './entities/feishu-bot.entity';
|
||||
import { CreateFeishuBotDto } from './dto/create-bot.dto';
|
||||
|
||||
@Injectable()
|
||||
export class FeishuService {
|
||||
private readonly logger = new Logger(FeishuService.name);
|
||||
private readonly feishuApiBase = 'https://open.feishu.cn/open-apis';
|
||||
|
||||
constructor(
|
||||
@InjectRepository(FeishuBot)
|
||||
private botRepository: Repository<FeishuBot>,
|
||||
private httpService: HttpService,
|
||||
private configService: ConfigService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Create or update a Feishu bot for a user
|
||||
*/
|
||||
async createBot(userId: string, dto: CreateFeishuBotDto): Promise<FeishuBot> {
|
||||
// Check if bot already exists for this user with same appId
|
||||
const existing = await this.botRepository.findOne({
|
||||
where: { userId, appId: dto.appId },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
// Update existing bot
|
||||
Object.assign(existing, dto);
|
||||
return this.botRepository.save(existing);
|
||||
}
|
||||
|
||||
// Create new bot
|
||||
const bot = this.botRepository.create({
|
||||
userId,
|
||||
...dto,
|
||||
});
|
||||
return this.botRepository.save(bot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all bots for a user
|
||||
*/
|
||||
async getUserBots(userId: string): Promise<FeishuBot[]> {
|
||||
return this.botRepository.find({ where: { userId } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bot by ID
|
||||
*/
|
||||
async getBotById(botId: string): Promise<FeishuBot | null> {
|
||||
return this.botRepository.findOne({ where: { id: botId } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get bot by appId
|
||||
*/
|
||||
async getBotByAppId(appId: string): Promise<FeishuBot | null> {
|
||||
return this.botRepository.findOne({ where: { appId } });
|
||||
}
|
||||
|
||||
/**
|
||||
* Get or refresh tenant_access_token
|
||||
*/
|
||||
async getValidToken(bot: FeishuBot): Promise<string> {
|
||||
// Check if token is still valid (expire in 2 hours, refresh at 1.5 hours)
|
||||
if (
|
||||
bot.tokenExpiresAt &&
|
||||
bot.tenantAccessToken &&
|
||||
new Date(bot.tokenExpiresAt) > new Date(Date.now() + 30 * 60 * 1000)
|
||||
) {
|
||||
return bot.tenantAccessToken;
|
||||
}
|
||||
|
||||
// Refresh token
|
||||
const response = await this.httpService
|
||||
.post(`${this.feishuApiBase}/auth/v3/tenant_access_token/internal`, {
|
||||
app_id: bot.appId,
|
||||
app_secret: bot.appSecret,
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
if (response.data.code !== 0) {
|
||||
throw new Error(`Failed to get token: ${response.data.msg}`);
|
||||
}
|
||||
|
||||
const { tenant_access_token, expire } = response.data;
|
||||
|
||||
// Update bot with new token
|
||||
bot.tenantAccessToken = tenant_access_token;
|
||||
bot.tokenExpiresAt = new Date(Date.now() + expire * 1000);
|
||||
await this.botRepository.save(bot);
|
||||
|
||||
return tenant_access_token;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send message to Feishu user
|
||||
*/
|
||||
async sendMessage(
|
||||
bot: FeishuBot,
|
||||
receiveIdType: 'open_id' | 'user_id' | 'union_id' | 'chat_id',
|
||||
receiveId: string,
|
||||
messageType: 'text' | 'interactive' | 'post',
|
||||
content: any,
|
||||
): Promise<string> {
|
||||
const token = await this.getValidToken(bot);
|
||||
|
||||
const response = await this.httpService
|
||||
.post(
|
||||
`${this.feishuApiBase}/im/v1/messages?receive_id_type=${receiveIdType}`,
|
||||
{
|
||||
receive_id: receiveId,
|
||||
msg_type: messageType,
|
||||
content: JSON.stringify(content),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
if (response.data.code !== 0) {
|
||||
throw new Error(`Failed to send message: ${response.data.msg}`);
|
||||
}
|
||||
|
||||
return response.data.data.message_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send text message (convenience method)
|
||||
*/
|
||||
async sendTextMessage(
|
||||
bot: FeishuBot,
|
||||
receiveIdType: 'open_id' | 'user_id' | 'chat_id',
|
||||
receiveId: string,
|
||||
text: string,
|
||||
): Promise<string> {
|
||||
return this.sendMessage(bot, receiveIdType, receiveId, 'text', { text });
|
||||
}
|
||||
|
||||
/**
|
||||
* Reply to a message
|
||||
*/
|
||||
async replyMessage(
|
||||
bot: FeishuBot,
|
||||
messageId: string,
|
||||
messageType: 'text' | 'interactive' | 'post',
|
||||
content: any,
|
||||
): Promise<string> {
|
||||
const token = await this.getValidToken(bot);
|
||||
|
||||
const response = await this.httpService
|
||||
.post(
|
||||
`${this.feishuApiBase}/im/v1/messages/${messageId}/reply`,
|
||||
{
|
||||
msg_type: messageType,
|
||||
content: JSON.stringify(content),
|
||||
},
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
},
|
||||
)
|
||||
.toPromise();
|
||||
|
||||
if (response.data.code !== 0) {
|
||||
throw new Error(`Failed to reply message: ${response.data.msg}`);
|
||||
}
|
||||
|
||||
return response.data.data.message_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload image for sending
|
||||
*/
|
||||
async uploadImage(
|
||||
bot: FeishuBot,
|
||||
imageType: 'message' | 'avatar',
|
||||
image: Buffer,
|
||||
imageName: string,
|
||||
): Promise<string> {
|
||||
const token = await this.getValidToken(bot);
|
||||
|
||||
const FormData = require('form-data');
|
||||
const form = new FormData();
|
||||
form.append('image_type', imageType);
|
||||
form.append('image', image, { filename: imageName });
|
||||
|
||||
const response = await this.httpService
|
||||
.post(`${this.feishuApiBase}/im/v1/images`, form, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
...form.getHeaders(),
|
||||
},
|
||||
})
|
||||
.toPromise();
|
||||
|
||||
if (response.data.code !== 0) {
|
||||
throw new Error(`Failed to upload image: ${response.data.msg}`);
|
||||
}
|
||||
|
||||
return response.data.data.image_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete bot
|
||||
*/
|
||||
async deleteBot(botId: string): Promise<void> {
|
||||
await this.botRepository.delete(botId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Enable/disable bot
|
||||
*/
|
||||
async setBotEnabled(botId: string, enabled: boolean): Promise<FeishuBot> {
|
||||
const bot = await this.botRepository.findOne({ where: { id: botId } });
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
bot.enabled = enabled;
|
||||
return this.botRepository.save(bot);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: Controller and Webhook Endpoints
|
||||
|
||||
### Task 3.1: Create Feishu Controller
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/feishu/feishu.controller.ts`
|
||||
|
||||
- [ ] **Step 1: Implement webhook endpoints**
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Get,
|
||||
Delete,
|
||||
Body,
|
||||
Param,
|
||||
Query,
|
||||
Headers,
|
||||
UseGuards,
|
||||
Request,
|
||||
RawBodyRequest,
|
||||
Req,
|
||||
} from '@nestjs/common';
|
||||
import { Request as ExpressRequest } from 'express';
|
||||
import { FeishuService } from './feishu.service';
|
||||
import { CreateFeishuBotDto } from './dto/create-bot.dto';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CreateSignatureDto, VerifyWebhookDto } from './dto/webhook.dto';
|
||||
|
||||
@Controller('feishu')
|
||||
export class FeishuController {
|
||||
constructor(private readonly feishuService: FeishuService) {}
|
||||
|
||||
/**
|
||||
* GET /feishu/bots - List user's bots
|
||||
* Requires JWT auth
|
||||
*/
|
||||
@Get('bots')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async listBots(@Request() req) {
|
||||
const bots = await this.feishuService.getUserBots(req.user.id);
|
||||
// Mask sensitive data
|
||||
return bots.map((bot) => ({
|
||||
id: bot.id,
|
||||
appId: bot.appId,
|
||||
botName: bot.botName,
|
||||
enabled: bot.enabled,
|
||||
isDefault: bot.isDefault,
|
||||
createdAt: bot.createdAt,
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /feishu/bots - Create a new bot
|
||||
* Requires JWT auth
|
||||
*/
|
||||
@Post('bots')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async createBot(@Request() req, @Body() dto: CreateFeishuBotDto) {
|
||||
const bot = await this.feishuService.createBot(req.user.id, dto);
|
||||
return {
|
||||
id: bot.id,
|
||||
appId: bot.appId,
|
||||
botName: bot.botName,
|
||||
enabled: bot.enabled,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* DELETE /feishu/bots/:id - Delete a bot
|
||||
* Requires JWT auth
|
||||
*/
|
||||
@Delete('bots/:id')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
async deleteBot(@Request() req, @Param('id') botId: string) {
|
||||
await this.feishuService.deleteBot(botId);
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /feishu/webhook - Feishu webhook endpoint
|
||||
* Public endpoint - no auth required (uses verification token)
|
||||
*/
|
||||
@Post('webhook')
|
||||
async handleWebhook(
|
||||
@Body() body: any,
|
||||
@Headers('x-xsign') xSign?: string,
|
||||
@Headers('x-timESTAMP') xTimestamp?: string,
|
||||
) {
|
||||
this.logger.log(`Received webhook: ${JSON.stringify(body)}`);
|
||||
|
||||
const { type, schema, event } = body;
|
||||
|
||||
// Handle URL verification (飞书首次配置时验证)
|
||||
if (type === 'url_verification') {
|
||||
return {
|
||||
challenge: body.challenge,
|
||||
};
|
||||
}
|
||||
|
||||
// Handle event callback
|
||||
if (type === 'event_callback') {
|
||||
const { event_type, token } = body;
|
||||
|
||||
// Verify token
|
||||
if (token !== body?.verify_token) {
|
||||
this.logger.warn('Webhook token verification failed');
|
||||
return { success: false };
|
||||
}
|
||||
|
||||
// Handle different event types
|
||||
switch (event_type) {
|
||||
case 'im.message.p2p_msg_received':
|
||||
// Handle private message
|
||||
await this.handleP2PMsg(event);
|
||||
break;
|
||||
|
||||
case 'im.message.group_at_msg_received':
|
||||
// Handle group @message
|
||||
await this.handleGroupMsg(event);
|
||||
break;
|
||||
|
||||
default:
|
||||
this.logger.log(`Unhandled event type: ${event_type}`);
|
||||
}
|
||||
}
|
||||
|
||||
return { success: true };
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle private message
|
||||
*/
|
||||
private async handleP2PMsg(event: any) {
|
||||
const { message } = event;
|
||||
const { message_id, chat_id, sender, message_type, body } = message;
|
||||
|
||||
// Get message content
|
||||
const content = JSON.parse(message.content || '{}');
|
||||
const text = content.text || '';
|
||||
|
||||
// Find bot by app_id (from chat or event)
|
||||
const openId = sender?.id?.open_id;
|
||||
if (!openId) {
|
||||
this.logger.warn('No sender open_id found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Find user's bot
|
||||
const bot = await this.feishuService.getBotByAppId(sender?.sender_id?.app_id);
|
||||
if (!bot || !bot.enabled) {
|
||||
this.logger.warn('Bot not found or disabled');
|
||||
return;
|
||||
}
|
||||
|
||||
// Process message through RAG/Chat service
|
||||
await this.processMessage(bot, openId, message_id, text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle group message (@bot)
|
||||
*/
|
||||
private async handleGroupMsg(event: any) {
|
||||
const { message } = event;
|
||||
const { message_id, chat_id, sender, message_type, content } = message;
|
||||
|
||||
// Check if bot was mentioned
|
||||
const msgContent = JSON.parse(content || '{}');
|
||||
// Group messages often require specific handling
|
||||
|
||||
// Similar to P2P but with chat_id context
|
||||
await this.handleP2PMsg(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process message through ChatService
|
||||
*/
|
||||
private async processMessage(
|
||||
bot: any,
|
||||
openId: string,
|
||||
messageId: string,
|
||||
text: string,
|
||||
) {
|
||||
// TODO: Integrate with ChatService
|
||||
// This will be implemented in Chunk 5
|
||||
|
||||
// For now, echo back (placeholder)
|
||||
try {
|
||||
await this.feishuService.sendTextMessage(
|
||||
bot,
|
||||
'open_id',
|
||||
openId,
|
||||
`Received: ${text}`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to send response', error);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Create webhook DTOs**
|
||||
|
||||
Create: `server/src/feishu/dto/webhook.dto.ts`
|
||||
```typescript
|
||||
import { IsString, IsOptional } from 'class-validator';
|
||||
|
||||
export class CreateSignatureDto {
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
timestamp?: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
nonce?: string;
|
||||
}
|
||||
|
||||
export class VerifyWebhookDto {
|
||||
@IsString()
|
||||
token: string;
|
||||
|
||||
@IsString()
|
||||
@IsOptional()
|
||||
challenge?: string;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 4: Plugin Registration
|
||||
|
||||
### Task 4.1: Register Feishu Plugin
|
||||
**Note:** This module acts as an optional extension to the AuraK ecosystem.
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/feishu/feishu.module.ts`
|
||||
- Modify: `server/src/app.module.ts`
|
||||
|
||||
- [ ] **Step 1: Create FeishuModule with isolated exports**
|
||||
|
||||
|
||||
```typescript
|
||||
import { Module, forwardRef } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { HttpModule } from '@nestjs/axios';
|
||||
import { FeishuController } from './feishu.controller';
|
||||
import { FeishuService } from './feishu.service';
|
||||
import { FeishuBot } from './entities/feishu-bot.entity';
|
||||
import { ChatModule } from '../chat/chat.module';
|
||||
import { UserModule } from '../user/user.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeOrmModule.forFeature([FeishuBot]),
|
||||
HttpModule,
|
||||
forwardRef(() => ChatModule),
|
||||
forwardRef(() => UserModule),
|
||||
],
|
||||
controllers: [FeishuController],
|
||||
providers: [FeishuService],
|
||||
exports: [FeishuService, TypeOrmModule],
|
||||
})
|
||||
export class FeishuModule {}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Add FeishuModule to AppModule**
|
||||
|
||||
Modify: `server/src/app.module.ts`
|
||||
|
||||
Add import:
|
||||
```typescript
|
||||
import { FeishuModule } from './feishu/feishu.module';
|
||||
```
|
||||
|
||||
Add to imports array:
|
||||
```typescript
|
||||
FeishuModule,
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 5: Integration with ChatService
|
||||
|
||||
### Task 5.1: Connect Feishu messages to ChatService
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/feishu/feishu.controller.ts`
|
||||
- Modify: `server/src/feishu/feishu.service.ts`
|
||||
|
||||
- [ ] **Step 1: Extend FeishuService to handle chat integration**
|
||||
|
||||
Add to FeishuService:
|
||||
```typescript
|
||||
// Import these
|
||||
import { ChatService } from '../chat/chat.service';
|
||||
import { ModelConfigService } from '../model-config/model-config.service';
|
||||
import { TenantService } from '../tenant/tenant.service';
|
||||
import { UserService } from '../user/user.service';
|
||||
import { ModelType } from '../types';
|
||||
|
||||
// Add to constructor
|
||||
constructor(
|
||||
// ... existing
|
||||
private chatService: ChatService,
|
||||
private modelConfigService: ModelConfigService,
|
||||
private tenantService: TenantService,
|
||||
private userService: UserService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Process chat message through RAG
|
||||
*/
|
||||
async processChatMessage(
|
||||
bot: FeishuBot,
|
||||
openId: string,
|
||||
messageId: string,
|
||||
userMessage: string,
|
||||
): Promise<void> {
|
||||
// Get user by Feishu open_id mapping (future: map table)
|
||||
// For now, use userId from bot
|
||||
const userId = bot.userId;
|
||||
|
||||
// Get user's tenant
|
||||
const user = await this.userService.findById(userId);
|
||||
const tenantId = user?.tenantId || 'default';
|
||||
|
||||
// Get user's LLM config
|
||||
const llmModel = await this.modelConfigService.findDefaultByType(
|
||||
tenantId,
|
||||
ModelType.LLM,
|
||||
);
|
||||
|
||||
if (!llmModel) {
|
||||
await this.sendTextMessage(bot, 'open_id', openId, '请先配置 LLM 模型');
|
||||
return;
|
||||
}
|
||||
|
||||
// Send "thinking" message
|
||||
await this.sendTextMessage(bot, 'open_id', openId, '正在思考...');
|
||||
|
||||
// Stream chat response
|
||||
const stream = this.chatService.streamChat(
|
||||
userMessage,
|
||||
[], // No history for now (future: persist per openId)
|
||||
userId,
|
||||
llmModel as any,
|
||||
user?.userSetting?.language || 'zh',
|
||||
undefined, // selectedEmbeddingId
|
||||
undefined, // selectedGroups
|
||||
undefined, // selectedFiles
|
||||
undefined, // historyId
|
||||
false, // enableRerank
|
||||
undefined, // selectedRerankId
|
||||
undefined, // temperature
|
||||
undefined, // maxTokens
|
||||
10, // topK
|
||||
0.7, // similarityThreshold
|
||||
undefined, // rerankSimilarityThreshold
|
||||
undefined, // enableQueryExpansion
|
||||
undefined, // enableHyDE
|
||||
tenantId,
|
||||
);
|
||||
|
||||
let fullResponse = '';
|
||||
for await (const chunk of stream) {
|
||||
if (chunk.type === 'content') {
|
||||
fullResponse += chunk.data;
|
||||
// Could send incrementally, but Feishu prefers complete messages
|
||||
}
|
||||
}
|
||||
|
||||
// Send final response
|
||||
// Truncate if too long (Feishu has limits)
|
||||
const maxLength = 5000;
|
||||
const finalMessage = fullResponse.length > maxLength
|
||||
? fullResponse.substring(0, maxLength) + '...(内容过长)'
|
||||
: fullResponse;
|
||||
|
||||
await this.sendTextMessage(bot, 'open_id', openId, finalMessage);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Update controller to use chat integration**
|
||||
|
||||
Modify the `processMessage` method in FeishuController to call `feishuService.processChatMessage()`:
|
||||
|
||||
```typescript
|
||||
private async processMessage(
|
||||
bot: any,
|
||||
openId: string,
|
||||
messageId: string,
|
||||
text: string,
|
||||
) {
|
||||
try {
|
||||
await this.feishuService.processChatMessage(bot, openId, messageId, text);
|
||||
} catch (error) {
|
||||
this.logger.error('Failed to process message', error);
|
||||
try {
|
||||
await this.feishuService.sendTextMessage(
|
||||
bot,
|
||||
'open_id',
|
||||
openId,
|
||||
'抱歉,处理消息时发生错误,请稍后重试。',
|
||||
);
|
||||
} catch (sendError) {
|
||||
this.logger.error('Failed to send error message', sendError);
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 6: Frontend Integration (Optional)
|
||||
|
||||
### Task 6.1: Add Feishu sub-view to Plugins
|
||||
**Files:**
|
||||
- Create: `web/src/pages/Plugins/FeishuPlugin.tsx`
|
||||
- Modify: `web/src/components/views/PluginsView.tsx`
|
||||
- Modify: `web/src/services/api.ts`
|
||||
|
||||
- [ ] **Step 1: Create the Plugin Configuration UI**
|
||||
This view should match the design of other plugin cards in the /plugins page but provide a detailed setup guide and form for:
|
||||
- App ID / App Secret
|
||||
- Webhook URL (Read-only generated URL)
|
||||
- Verification Token & Encrypt Key
|
||||
|
||||
- [ ] **Step 2: Register the Plugin in PluginsView**
|
||||
Modify the main plugins listing to include "Feishu Bot" as an available (or installed) plugin.
|
||||
|
||||
Modify: `web/src/services/api.ts`
|
||||
```typescript
|
||||
// Add Feishu API calls
|
||||
export const feishuApi = {
|
||||
listBots: () => api.get('/feishu/bots'),
|
||||
createBot: (data: CreateBotDto) => api.post('/feishu/bots', data),
|
||||
deleteBot: (botId: string) => api.delete(`/feishu/bots/${botId}`),
|
||||
};
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Implementation of PluginsView.tsx**
|
||||
If `web/src/components/views/PluginsView.tsx` does not exist, create a generic plugin management layout that can host the Feishu configuration.
|
||||
|
||||
**Layout Requirements:**
|
||||
- Grid of available plugins.
|
||||
- Status toggle (Enabled/Disabled).
|
||||
- Detail/Configuration view for active plugins.
|
||||
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
|
||||
- `feishu.service.spec.ts` - Test token refresh, message sending
|
||||
- `feishu.controller.spec.ts` - Test webhook endpoints
|
||||
|
||||
### Integration Tests
|
||||
|
||||
- Test full flow: Feishu message → Webhook → ChatService → Response
|
||||
|
||||
### Manual Testing
|
||||
|
||||
1. Create Feishu app in 开放平台
|
||||
2. Configure webhook URL (use ngrok for local)
|
||||
3. Subscribe to message events
|
||||
4. Send message to bot
|
||||
5. Verify RAG response
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Security Considerations
|
||||
|
||||
1. **Token Storage**: Encrypt `app_secret` and `encrypt_key` before storing in DB.
|
||||
2. **Webhook Verification**:
|
||||
- Verify `verify_token` for simplicity.
|
||||
- **Recommended**: Validate `X-Lark-Signature` using `encrypt_key` to ensure authenticity.
|
||||
|
||||
3. **Rate Limiting**: Implement a message queue (e.g., BullMQ) for outbound messages to respect Feishu's global and per-bot rate limits.
|
||||
4. **User Privacy**: Implement an opt-in flow for group chats to ensure the bot only processes messages when explicitly allowed or mentioned.
|
||||
|
||||
---
|
||||
|
||||
## Advanced Optimizations (Recommended)
|
||||
|
||||
### 1. Webhook Performance & Reliability
|
||||
- **Immediate Response**: Feishu requires a response within 3 seconds. The RAG process can take 10s+.
|
||||
- **Optimization**: The `handleWebhook` should only validate the request and push the event to an internal queue, returning `200 OK` immediately. A background worker then processes the RAG logic and sends the response.
|
||||
- **Deduplication**: Use the `event_id` in the webhook payload to ignore duplicate retries from Feishu.
|
||||
|
||||
### 2. UX: Managed "Thinking" State
|
||||
- **Simulated Streaming**: Since Feishu doesn't support SSE, send an initial "Thinking..." message and use the `PATCH /im/v1/messages/:message_id` API to update the message content every few seconds as chunks arrive.
|
||||
- **Interactive Cards**: Use [Message Cards](https://open.feishu.cn/document/common-capabilities/message-card/message-card-overview) instead of plain text for:
|
||||
- Showing search citations with clickable links.
|
||||
- Providing "Regenerate" or "Clear Context" buttons.
|
||||
- Displaying assessment results with formatting (tables/charts).
|
||||
|
||||
### 3. Context & History Management
|
||||
- **OpenID Mapping**: Maintain a mapping between Feishu `open_id` and AuraK `userId` to persist chat history across different devices/platforms.
|
||||
- **Thread Support**: Use Feishu's `root_id` and `parent_id` to allow users to ask follow-up questions within a thread, keeping the UI clean.
|
||||
|
||||
### 4. Multi-modal Support
|
||||
- **File Ingestion**: Support `im.message.file_received` events. If a user sends a PDF/Docx to the bot, automatically import it into a "Feishu Uploads" group for immediate RAG context.
|
||||
- **Image Analysis**: Use the `VisionService` to handle images sent via Feishu.
|
||||
|
||||
|
||||
## Future Enhancements
|
||||
|
||||
1. **Assessment Integration**: Bind assessment sessions to Feishu conversations using interactive card forms.
|
||||
2. **Rich Responses**: Use Feishu interactive cards for better visual presentation.
|
||||
3. **Multi-bot Support**: Users can have multiple bots for different specialized tasks.
|
||||
4. **Group Chats**: Support bot in group chats with specific @mention logic and moderation.
|
||||
5. **Voice Messages**: Handle voice message transcription via Feishu's audio-to-text API for accessibility.
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `@nestjs/axios` - For HTTP requests to Feishu API
|
||||
- `form-data` - For file/image uploads
|
||||
- Optional: `crypto` (built-in) - For signature verification
|
||||
|
||||
---
|
||||
|
||||
## Reference Links
|
||||
|
||||
- [飞书开放平台 - 机器人文档](https://open.feishu.cn/document/faq/bot)
|
||||
- [飞书事件订阅](https://open.feishu.cn/document/server-docs/event-subscription-guide/overview)
|
||||
- [消息发送 API](https://open.feishu.cn/document/server-docs/im-v1/message-content-description)
|
||||
- [获取 tenant_access_token](https://open.feishu.cn/document/server-docs/authentication-management/access-token)
|
||||
|
||||
---
|
||||
|
||||
> **Plan created:** 2026-03-16
|
||||
> **Based on:** Feishu integration analysis
|
||||
@@ -1,727 +0,0 @@
|
||||
# Feishu WebSocket Integration Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Add WebSocket long-connection support for Feishu bot integration, enabling internal network deployment without requiring public domain.
|
||||
|
||||
**Architecture:** Each bot maintains its own WebSocket connection to Feishu cloud via official SDK. Connection management handled by dedicated FeishuWsManager service. Existing webhook mode preserved for backward compatibility.
|
||||
|
||||
**Tech Stack:** NestJS, @larksuiteoapi/node-sdk, TypeScript
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
```
|
||||
server/src/feishu/
|
||||
├── feishu.module.ts # Register new manager
|
||||
├── feishu.service.ts # Add WS control methods
|
||||
├── feishu.controller.ts # Add WS API endpoints
|
||||
├── feishu-ws.manager.ts # NEW: WebSocket connection manager
|
||||
├── dto/
|
||||
│ └── ws-status.dto.ts # NEW: WebSocket status DTOs
|
||||
└── entities/
|
||||
└── feishu-bot.entity.ts # Add WS fields
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 1: Dependencies & Entity Changes
|
||||
|
||||
### Task 1: Install Feishu SDK
|
||||
|
||||
- [ ] **Step 1: Install @larksuiteoapi/node-sdk**
|
||||
|
||||
```bash
|
||||
cd D:\aura\AuraK\server
|
||||
yarn add @larksuiteoapi/node-sdk
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Verify installation**
|
||||
|
||||
```bash
|
||||
yarn list @larksuiteoapi/node-sdk
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 2: Update FeishuBot Entity
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/feishu/entities/feishu-bot.entity.ts`
|
||||
|
||||
- [ ] **Step 1: Read current entity**
|
||||
|
||||
File: `D:\aura\AuraK\server\src\feishu\entities\feishu-bot.entity.ts`
|
||||
|
||||
- [ ] **Step 2: Add WebSocket fields**
|
||||
|
||||
Add after existing columns:
|
||||
|
||||
```typescript
|
||||
@Column({ default: false })
|
||||
useWebSocket: boolean;
|
||||
|
||||
@Column({ nullable: true })
|
||||
wsConnectionState: string;
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run build to verify**
|
||||
|
||||
```bash
|
||||
cd D:\aura\AuraK\server
|
||||
yarn build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Chunk 2: WebSocket Manager
|
||||
|
||||
### Task 3: Create WebSocket Status DTOs
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/feishu/dto/ws-status.dto.ts`
|
||||
|
||||
- [ ] **Step 1: Create DTO file**
|
||||
|
||||
```typescript
|
||||
export enum ConnectionState {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTING = 'connecting',
|
||||
CONNECTED = 'connected',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export interface ConnectionStatus {
|
||||
botId: string;
|
||||
state: ConnectionState;
|
||||
connectedAt?: Date;
|
||||
lastHeartbeat?: Date;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class ConnectWsDto {
|
||||
// No params needed - uses bot ID from route
|
||||
}
|
||||
|
||||
export class WsStatusResponseDto {
|
||||
botId: string;
|
||||
state: ConnectionState;
|
||||
connectedAt?: string;
|
||||
lastHeartbeat?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class WsConnectResponseDto {
|
||||
success: boolean;
|
||||
botId: string;
|
||||
status: ConnectionState;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class WsDisconnectResponseDto {
|
||||
success: boolean;
|
||||
botId: string;
|
||||
status: ConnectionState;
|
||||
}
|
||||
|
||||
export class AllWsStatusResponseDto {
|
||||
connections: WsStatusResponseDto[];
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 4: Create FeishuWsManager
|
||||
|
||||
**Files:**
|
||||
- Create: `server/src/feishu/feishu-ws.manager.ts`
|
||||
|
||||
- [ ] **Step 1: Create the WebSocket manager**
|
||||
|
||||
```typescript
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { EventDispatcher, Conf } from '@larksuiteoapi/node-sdk';
|
||||
import { FeishuBot } from './entities/feishu-bot.entity';
|
||||
import { ConnectionState, ConnectionStatus } from './dto/ws-status.dto';
|
||||
import { FeishuService } from './feishu.service';
|
||||
|
||||
@Injectable()
|
||||
export class FeishuWsManager {
|
||||
private readonly logger = new Logger(FeishuWsManager.name);
|
||||
private connections: Map<string, { client: EventDispatcher; status: ConnectionStatus }> = new Map();
|
||||
private reconnectAttempts: Map<string, number> = new Map();
|
||||
private readonly MAX_RECONNECT_ATTEMPTS = 5;
|
||||
private readonly RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000]; // Exponential backoff
|
||||
|
||||
constructor(private readonly feishuService: FeishuService) {}
|
||||
|
||||
/**
|
||||
* Start WebSocket connection for a bot
|
||||
*/
|
||||
async connect(bot: FeishuBot): Promise<void> {
|
||||
const botId = bot.id;
|
||||
|
||||
// Check if already connected
|
||||
const existing = this.connections.get(botId);
|
||||
if (existing && existing.status.state === ConnectionState.CONNECTED) {
|
||||
this.logger.warn(`Bot ${botId} already connected`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Set connecting state
|
||||
this.updateStatus(botId, {
|
||||
botId,
|
||||
state: ConnectionState.CONNECTING,
|
||||
});
|
||||
|
||||
try {
|
||||
// Create event dispatcher (WebSocket client)
|
||||
const client = new EventDispatcher(
|
||||
{
|
||||
appId: bot.appId,
|
||||
appSecret: bot.appSecret,
|
||||
verificationToken: bot.verificationToken,
|
||||
} as any,
|
||||
{
|
||||
logger: {
|
||||
debug: (msg: any) => this.logger.debug(msg),
|
||||
info: (msg: any) => this.logger.log(msg),
|
||||
warn: (msg: any) => this.logger.warn(msg),
|
||||
error: (msg: any) => this.logger.error(msg),
|
||||
},
|
||||
} as any,
|
||||
);
|
||||
|
||||
// Register event handlers
|
||||
client.on('im.message.receive_v1', async (data: any) => {
|
||||
await this.handleMessage(bot, data);
|
||||
});
|
||||
|
||||
// Store connection
|
||||
this.connections.set(botId, {
|
||||
client: client as any,
|
||||
status: {
|
||||
botId,
|
||||
state: ConnectionState.CONNECTED,
|
||||
connectedAt: new Date(),
|
||||
},
|
||||
});
|
||||
|
||||
this.reconnectAttempts.set(botId, 0);
|
||||
|
||||
this.logger.log(`WebSocket connected for bot ${botId}`);
|
||||
|
||||
// Update bot state in DB
|
||||
await this.feishuService.getBotById(botId);
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to connect WebSocket for bot ${botId}`, error);
|
||||
this.updateStatus(botId, {
|
||||
botId,
|
||||
state: ConnectionState.ERROR,
|
||||
error: error.message || 'Connection failed',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect WebSocket for a bot
|
||||
*/
|
||||
async disconnect(botId: string): Promise<void> {
|
||||
const connection = this.connections.get(botId);
|
||||
if (!connection) {
|
||||
this.logger.warn(`No connection found for bot ${botId}`);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// SDK doesn't have explicit disconnect, just remove references
|
||||
this.connections.delete(botId);
|
||||
this.reconnectAttempts.delete(botId);
|
||||
|
||||
this.logger.log(`WebSocket disconnected for bot ${botId}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error disconnecting bot ${botId}`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection status for a bot
|
||||
*/
|
||||
getStatus(botId: string): ConnectionStatus | null {
|
||||
const connection = this.connections.get(botId);
|
||||
return connection?.status || null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all connection statuses
|
||||
*/
|
||||
getAllStatuses(): ConnectionStatus[] {
|
||||
const statuses: ConnectionStatus[] = [];
|
||||
for (const [botId, connection] of this.connections.entries()) {
|
||||
statuses.push(connection.status);
|
||||
}
|
||||
return statuses;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a bot is connected
|
||||
*/
|
||||
isConnected(botId: string): boolean {
|
||||
const connection = this.connections.get(botId);
|
||||
return connection?.status.state === ConnectionState.CONNECTED;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle incoming message from Feishu
|
||||
*/
|
||||
private async handleMessage(bot: FeishuBot, data: any): Promise<void> {
|
||||
this.logger.log(`Received message for bot ${bot.id}: ${JSON.stringify(data)}`);
|
||||
|
||||
try {
|
||||
const event = data.event || data;
|
||||
const message = event?.message;
|
||||
|
||||
if (!message) {
|
||||
this.logger.warn('No message in event data');
|
||||
return;
|
||||
}
|
||||
|
||||
const messageId = message.message_id;
|
||||
const openId = event?.sender?.sender_id?.open_id;
|
||||
|
||||
if (!openId) {
|
||||
this.logger.warn('No sender open_id found');
|
||||
return;
|
||||
}
|
||||
|
||||
// Parse text content
|
||||
let userText = '';
|
||||
try {
|
||||
const content = JSON.parse(message.content || '{}');
|
||||
userText = content.text || '';
|
||||
} catch {
|
||||
this.logger.warn('Failed to parse message content');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!userText.trim()) return;
|
||||
|
||||
// Process via FeishuService
|
||||
await this.feishuService.processChatMessage(bot, openId, messageId, userText);
|
||||
} catch (error) {
|
||||
this.logger.error('Error handling message', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update connection status
|
||||
*/
|
||||
private updateStatus(botId: string, status: Partial<ConnectionStatus>): void {
|
||||
const connection = this.connections.get(botId);
|
||||
if (connection) {
|
||||
connection.status = { ...connection.status, ...status };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to reconnect a bot
|
||||
*/
|
||||
async attemptReconnect(bot: FeishuBot): Promise<void> {
|
||||
const botId = bot.id;
|
||||
const attempts = this.reconnectAttempts.get(botId) || 0;
|
||||
|
||||
if (attempts >= this.MAX_RECONNECT_ATTEMPTS) {
|
||||
this.logger.error(`Max reconnect attempts reached for bot ${botId}`);
|
||||
this.updateStatus(botId, {
|
||||
botId,
|
||||
state: ConnectionState.ERROR,
|
||||
error: 'Max reconnect attempts reached',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const delay = this.RECONNECT_DELAYS[attempts] || this.RECONNECT_DELAYS[this.RECONNECT_DELAYS.length - 1];
|
||||
this.logger.log(`Reconnecting bot ${botId} in ${delay}ms (attempt ${attempts + 1})`);
|
||||
|
||||
this.reconnectAttempts.set(botId, attempts + 1);
|
||||
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
await this.connect(bot);
|
||||
} catch (error) {
|
||||
this.logger.error(`Reconnect failed for bot ${botId}`, error);
|
||||
}
|
||||
}, delay);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run build to verify**
|
||||
|
||||
```bash
|
||||
cd D:\aura\AuraK\server
|
||||
yarn build
|
||||
```
|
||||
|
||||
Expected: No errors
|
||||
|
||||
---
|
||||
|
||||
## Chunk 3: Service Integration
|
||||
|
||||
### Task 5: Update FeishuService
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/feishu/feishu.service.ts`
|
||||
|
||||
- [ ] **Step 1: Read current service**
|
||||
|
||||
File: `D:\aura\AuraK\server\src\feishu\feishu.service.ts`
|
||||
|
||||
- [ ] **Step 2: Add WS management methods**
|
||||
|
||||
Add at the end of the class (before the closing brace):
|
||||
|
||||
```typescript
|
||||
// ─── WebSocket Connection Management ─────────────────────────────────────────
|
||||
|
||||
@Inject(forwardRef(() => FeishuWsManager))
|
||||
private wsManager: FeishuWsManager;
|
||||
|
||||
/**
|
||||
* Start WebSocket connection for a bot
|
||||
*/
|
||||
async startWsConnection(botId: string): Promise<void> {
|
||||
const bot = await this.getBotById(botId);
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
if (!bot.enabled) {
|
||||
throw new Error('Bot is disabled');
|
||||
}
|
||||
|
||||
bot.useWebSocket = true;
|
||||
await this.botRepository.save(bot);
|
||||
|
||||
await this.wsManager.connect(bot);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop WebSocket connection for a bot
|
||||
*/
|
||||
async stopWsConnection(botId: string): Promise<void> {
|
||||
const bot = await this.getBotById(botId);
|
||||
if (!bot) {
|
||||
throw new Error('Bot not found');
|
||||
}
|
||||
|
||||
bot.useWebSocket = false;
|
||||
await this.botRepository.save(bot);
|
||||
|
||||
await this.wsManager.disconnect(botId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get WebSocket connection status
|
||||
*/
|
||||
async getWsStatus(botId: string): Promise<ConnectionStatus | null> {
|
||||
return this.wsManager.getStatus(botId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all WebSocket connection statuses
|
||||
*/
|
||||
async getAllWsStatuses(): Promise<ConnectionStatus[]> {
|
||||
return this.wsManager.getAllStatuses();
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Add import for ConnectionStatus**
|
||||
|
||||
Add at top of file:
|
||||
|
||||
```typescript
|
||||
import { ConnectionStatus } from './dto/ws-status.dto';
|
||||
import { FeishuWsManager } from './feishu-ws.manager';
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run build to verify**
|
||||
|
||||
```bash
|
||||
cd D:\aura\AuraK\server
|
||||
yarn build
|
||||
```
|
||||
|
||||
Expected: No errors
|
||||
|
||||
---
|
||||
|
||||
## Chunk 4: Controller Endpoints
|
||||
|
||||
### Task 6: Update FeishuController
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/feishu/feishu.controller.ts`
|
||||
|
||||
- [ ] **Step 1: Read current controller**
|
||||
|
||||
File: `D:\aura\AuraK\server\src\feishu\feishu.controller.ts`
|
||||
|
||||
- [ ] **Step 2: Add WebSocket endpoints**
|
||||
|
||||
Add after the existing bot management endpoints (after line ~79):
|
||||
|
||||
```typescript
|
||||
// ─── WebSocket Management Endpoints ────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* POST /feishu/bots/:id/ws/connect - Start WebSocket connection
|
||||
*/
|
||||
@Post('bots/:id/ws/connect')
|
||||
@UseGuards(CombinedAuthGuard)
|
||||
async connectWs(@Request() req, @Param('id') botId: string) {
|
||||
// Verify bot belongs to user
|
||||
const bot = await this.feishuService.getBotById(botId);
|
||||
if (!bot || bot.userId !== req.user.id) {
|
||||
return { success: false, error: 'Bot not found' };
|
||||
}
|
||||
|
||||
try {
|
||||
await this.feishuService.startWsConnection(botId);
|
||||
return {
|
||||
success: true,
|
||||
botId,
|
||||
status: 'connecting',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
botId,
|
||||
error: error.message || 'Failed to connect',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* POST /feishu/bots/:id/ws/disconnect - Stop WebSocket connection
|
||||
*/
|
||||
@Post('bots/:id/ws/disconnect')
|
||||
@UseGuards(CombinedAuthGuard)
|
||||
async disconnectWs(@Request() req, @Param('id') botId: string) {
|
||||
// Verify bot belongs to user
|
||||
const bot = await this.feishuService.getBotById(botId);
|
||||
if (!bot || bot.userId !== req.user.id) {
|
||||
return { success: false, error: 'Bot not found' };
|
||||
}
|
||||
|
||||
try {
|
||||
await this.feishuService.stopWsConnection(botId);
|
||||
return {
|
||||
success: true,
|
||||
botId,
|
||||
status: 'disconnected',
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
botId,
|
||||
error: error.message || 'Failed to disconnect',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /feishu/bots/:id/ws/status - Get connection status
|
||||
*/
|
||||
@Get('bots/:id/ws/status')
|
||||
@UseGuards(CombinedAuthGuard)
|
||||
async getWsStatus(@Request() req, @Param('id') botId: string) {
|
||||
// Verify bot belongs to user
|
||||
const bot = await this.feishuService.getBotById(botId);
|
||||
if (!bot || bot.userId !== req.user.id) {
|
||||
return { success: false, error: 'Bot not found' };
|
||||
}
|
||||
|
||||
const status = await this.feishuService.getWsStatus(botId);
|
||||
|
||||
if (!status) {
|
||||
return {
|
||||
botId,
|
||||
state: 'disconnected',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
botId: status.botId,
|
||||
state: status.state,
|
||||
connectedAt: status.connectedAt?.toISOString(),
|
||||
lastHeartbeat: status.lastHeartbeat?.toISOString(),
|
||||
error: status.error,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* GET /feishu/ws/status - Get all connection statuses
|
||||
*/
|
||||
@Get('ws/status')
|
||||
@UseGuards(CombinedAuthGuard)
|
||||
async getAllWsStatus(@Request() req) {
|
||||
const statuses = await this.feishuService.getAllWsStatuses();
|
||||
|
||||
return {
|
||||
connections: statuses.map(s => ({
|
||||
botId: s.botId,
|
||||
state: s.state,
|
||||
connectedAt: s.connectedAt?.toISOString(),
|
||||
lastHeartbeat: s.lastHeartbeat?.toISOString(),
|
||||
error: s.error,
|
||||
})),
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Run build to verify**
|
||||
|
||||
```bash
|
||||
cd D:\aura\AuraK\server
|
||||
yarn build
|
||||
```
|
||||
|
||||
Expected: No errors
|
||||
|
||||
---
|
||||
|
||||
## Chunk 5: Module Registration
|
||||
|
||||
### Task 7: Update FeishuModule
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/feishu/feishu.module.ts`
|
||||
|
||||
- [ ] **Step 1: Read current module**
|
||||
|
||||
File: `D:\aura\AuraK\server\src\feishu\feishu.module.ts`
|
||||
|
||||
- [ ] **Step 2: Register FeishuWsManager**
|
||||
|
||||
Add FeishuWsManager to providers and add FeishuService as constructor dependency:
|
||||
|
||||
```typescript
|
||||
import { FeishuWsManager } from './feishu-ws.manager';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([FeishuBot])],
|
||||
controllers: [FeishuController],
|
||||
providers: [FeishuService, FeishuWsManager],
|
||||
exports: [FeishuService],
|
||||
})
|
||||
export class FeishuModule {}
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Update FeishuService constructor**
|
||||
|
||||
In `feishu.service.ts`, add FeishuWsManager to constructor:
|
||||
|
||||
```typescript
|
||||
constructor(
|
||||
@InjectRepository(FeishuBot)
|
||||
private botRepository: Repository<FeishuBot>,
|
||||
@Inject(forwardRef(() => ChatService))
|
||||
private chatService: ChatService,
|
||||
@Inject(forwardRef(() => ModelConfigService))
|
||||
private modelConfigService: ModelConfigService,
|
||||
@Inject(forwardRef(() => UserService))
|
||||
private userService: UserService,
|
||||
@Inject(forwardRef(() => FeishuWsManager))
|
||||
private wsManager: FeishuWsManager,
|
||||
) {}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run build to verify**
|
||||
|
||||
```bash
|
||||
cd D:\aura\AuraK\server
|
||||
yarn build
|
||||
```
|
||||
|
||||
Expected: No errors
|
||||
|
||||
---
|
||||
|
||||
## Chunk 6: Testing & Verification
|
||||
|
||||
### Task 8: Test WebSocket Integration
|
||||
|
||||
- [ ] **Step 1: Start the server**
|
||||
|
||||
```bash
|
||||
cd D:\aura\AuraK\server
|
||||
yarn start:dev
|
||||
```
|
||||
|
||||
Expected: Server starts without errors
|
||||
|
||||
- [ ] **Step 2: Verify endpoints exist**
|
||||
|
||||
```bash
|
||||
curl http://localhost:13000/api/feishu/ws/status
|
||||
```
|
||||
|
||||
Expected: Returns JSON with connections array
|
||||
|
||||
- [ ] **Step 3: Manual test with Feishu bot**
|
||||
|
||||
1. Create a Feishu bot in the UI
|
||||
2. Configure in Feishu developer console:
|
||||
- Enable "Use long connection to receive events"
|
||||
- Add event: im.message.receive_v1
|
||||
3. Call connect API:
|
||||
```bash
|
||||
curl -X POST http://localhost:13000/api/feishu/bots/{botId}/ws/connect
|
||||
```
|
||||
4. Send a message in Feishu to the bot
|
||||
5. Verify response is received
|
||||
|
||||
- [ ] **Step 4: Test disconnect**
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:13000/api/feishu/bots/{botId}/ws/disconnect
|
||||
```
|
||||
|
||||
- [ ] **Step 5: Verify webhook still works**
|
||||
|
||||
Test existing webhook endpoint still works as before.
|
||||
|
||||
---
|
||||
|
||||
## Chunk 7: Documentation Update
|
||||
|
||||
### Task 9: Update User Documentation
|
||||
|
||||
- [ ] **Step 1: Add WebSocket configuration guide**
|
||||
|
||||
Create or update documentation in `docs/` explaining:
|
||||
- How to configure WebSocket mode
|
||||
- Differences from webhook mode
|
||||
- Troubleshooting steps
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
| Task | Description | Estimated Time |
|
||||
|------|-------------|----------------|
|
||||
| 1 | Install Feishu SDK | 2 min |
|
||||
| 2 | Update FeishuBot entity | 5 min |
|
||||
| 3 | Create WS Status DTOs | 5 min |
|
||||
| 4 | Create FeishuWsManager | 15 min |
|
||||
| 5 | Update FeishuService | 10 min |
|
||||
| 6 | Update FeishuController | 10 min |
|
||||
| 7 | Update FeishuModule | 5 min |
|
||||
| 8 | Testing & Verification | 20 min |
|
||||
| 9 | Documentation | 10 min |
|
||||
|
||||
**Total estimated time:** ~80 minutes
|
||||
@@ -1,345 +0,0 @@
|
||||
# i18n Default Language Configuration Fix Design
|
||||
|
||||
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development to implement this plan.
|
||||
|
||||
**Goal:** Add DEFAULT_LANGUAGE configuration in .env file, with English as the hardcoded fallback when not configured.
|
||||
|
||||
**Architecture:** Read DEFAULT_LANGUAGE from environment variables in constants.ts, default to 'en' if not set.
|
||||
|
||||
**Tech Stack:** NestJS (backend), React (frontend), TypeScript, dotenv
|
||||
|
||||
---
|
||||
|
||||
## Problem Statement
|
||||
|
||||
### Current Issues
|
||||
|
||||
1. **No .env Configuration** - Default language is hardcoded in constants.ts
|
||||
- Cannot be changed without code modification
|
||||
- Different environments cannot have different defaults
|
||||
|
||||
2. **Fallback Language Issue** (`server/src/i18n/i18n.service.ts:180`)
|
||||
- When an unsupported language is passed, system defaults to Japanese
|
||||
- This causes unexpected Japanese output for users with unsupported locale
|
||||
|
||||
3. **Japanese Comments** (`server/src/common/constants.ts:21`)
|
||||
- Comment `// デフォルト言語` should be English per project standards
|
||||
|
||||
4. **Hardcoded System Prompts** (`server/src/i18n/i18n.service.ts`)
|
||||
- `getPrompt()` method: Japanese fallback prompts hardcoded in else branch
|
||||
- `getDocumentTitlePrompt()` method: Japanese fallback hardcoded
|
||||
- `getChatTitlePrompt()` method: Japanese fallback hardcoded
|
||||
|
||||
### Current State
|
||||
|
||||
| Component | Default Language | Source |
|
||||
|-----------|-----------------|--------|
|
||||
| Backend constants | `'zh'` | Hardcoded |
|
||||
| Backend i18n service | `'zh'` | From constants |
|
||||
| Frontend context | `'en'` | Hardcoded |
|
||||
|
||||
---
|
||||
|
||||
## Fix Design
|
||||
|
||||
### Decision: Env-Based Configuration with English Default
|
||||
|
||||
**Configuration Flow:**
|
||||
```
|
||||
.env → DEFAULT_LANGUAGE → constants.ts → i18n.service.ts
|
||||
↓ (if not set)
|
||||
'en' (hardcoded fallback)
|
||||
```
|
||||
|
||||
**Rationale:**
|
||||
- English is the most universally understood international language
|
||||
- Safer default for international users
|
||||
- Matches frontend default
|
||||
- Environment-based config allows per-deployment customization
|
||||
|
||||
---
|
||||
|
||||
## Files to Modify
|
||||
|
||||
### 1. `server/.env.sample`
|
||||
|
||||
**Add:** Default language configuration option
|
||||
|
||||
```bash
|
||||
# Default language for the system (zh, en, ja)
|
||||
# If not set, defaults to 'en'
|
||||
# DEFAULT_LANGUAGE=en
|
||||
```
|
||||
|
||||
### 2. `server/src/common/constants.ts`
|
||||
|
||||
**Change:** Read from environment with English fallback
|
||||
|
||||
```typescript
|
||||
// Supported languages
|
||||
const SUPPORTED_LANGUAGES = ['zh', 'en', 'ja'] as const;
|
||||
|
||||
// Read DEFAULT_LANGUAGE from environment
|
||||
function getDefaultLanguage(): typeof SUPPORTED_LANGUAGES[number] {
|
||||
const envValue = process.env.DEFAULT_LANGUAGE?.toLowerCase();
|
||||
|
||||
// Validate: must be one of supported languages
|
||||
if (envValue && SUPPORTED_LANGUAGES.includes(envValue as typeof SUPPORTED_LANGUAGES[number])) {
|
||||
return envValue as typeof SUPPORTED_LANGUAGES[number];
|
||||
}
|
||||
|
||||
// Fallback to English if not set or invalid
|
||||
return 'en';
|
||||
}
|
||||
|
||||
// Default language - read from env, fallback to English
|
||||
export const DEFAULT_LANGUAGE = getDefaultLanguage();
|
||||
export const DEFAULT_LANGUAGE_FALLBACK = 'en';
|
||||
```
|
||||
|
||||
### 3. `server/src/i18n/i18n.service.ts`
|
||||
|
||||
#### 3.1 Fix getPrompt() fallback (line ~180)
|
||||
|
||||
**Current:**
|
||||
```typescript
|
||||
} else { // 默认为日语,符合プロジェクト要求
|
||||
return type === 'withContext' ? `
|
||||
以下ナレッジベース...
|
||||
```
|
||||
|
||||
**Proposed:**
|
||||
```typescript
|
||||
} else { // Fallback to English for unsupported languages
|
||||
return type === 'withContext' ? `
|
||||
Answer the user's question based on the following knowledge base content.
|
||||
${hasKnowledgeGroup ? `
|
||||
**IMPORTANT**: The user has selected a specific knowledge group. Please answer strictly based on the knowledge base content below. If the relevant information is not found in the knowledge base, explicitly tell the user: "${noMatchMsg}", before providing an answer.
|
||||
` : ''}
|
||||
Knowledge Base CONTENT:
|
||||
{context}
|
||||
|
||||
Conversation history:
|
||||
{history}
|
||||
|
||||
User question: {question}
|
||||
|
||||
Please answer in English and strictly follow these Markdown formatting guidelines:
|
||||
|
||||
1. **Paragraphs & Structure**:
|
||||
- Use clear paragraph breaks with blank lines between key points
|
||||
- Use headings (## or ###) to organize longer answers
|
||||
|
||||
2. **Text Formatting**:
|
||||
- Use **bold** to emphasize important concepts and keywords
|
||||
- Use lists (- or 1.) to organize multiple points
|
||||
- Use \`code\` to mark technical terms, commands, file names
|
||||
|
||||
3. **Code Display**:
|
||||
- Use code blocks with language specification:
|
||||
\`\`\`python
|
||||
def example():
|
||||
return "example"
|
||||
\`\`\`
|
||||
- Supported languages: python, javascript, typescript, java, bash, sql, etc.
|
||||
|
||||
4. **Diagrams & Charts**:
|
||||
- Use Mermaid syntax for flowcharts, sequence diagrams, etc.:
|
||||
\`\`\`mermaid
|
||||
graph LR
|
||||
A[Start] --> B[Process]
|
||||
B --> C[End]
|
||||
\`\`\`
|
||||
- Use cases: process flows, architecture diagrams, state diagrams, sequence diagrams
|
||||
|
||||
5. **Other Requirements**:
|
||||
- Keep answers concise and clear
|
||||
- Use numbered lists for multi-step processes
|
||||
- Use tables for comparison information (if applicable)
|
||||
` : `
|
||||
As an intelligent assistant, please answer the user's question.
|
||||
|
||||
Conversation history:
|
||||
{history}
|
||||
|
||||
User question: {question}
|
||||
|
||||
Please answer in English.
|
||||
`;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.2 Fix getDocumentTitlePrompt() fallback (line ~253)
|
||||
|
||||
**Current:**
|
||||
```typescript
|
||||
} else {
|
||||
return `あなたはドキュメントアナライザー...
|
||||
```
|
||||
|
||||
**Proposed:**
|
||||
```typescript
|
||||
} else {
|
||||
return `You are a document analyzer. Read the following text (start of a document) and generate a concise, professional title (max 50 chars).
|
||||
Return ONLY the title text. No preamble like "The title is...".
|
||||
Language: English
|
||||
Text:
|
||||
${contentSample}`;
|
||||
}
|
||||
```
|
||||
|
||||
#### 3.3 Fix getChatTitlePrompt() fallback (line ~278)
|
||||
|
||||
**Current:**
|
||||
```typescript
|
||||
} else {
|
||||
return `以下の会話スニペットに基づい...
|
||||
```
|
||||
|
||||
**Proposed:**
|
||||
```typescript
|
||||
} else {
|
||||
return `Based on the following conversation snippet, generate a short, descriptive title (max 50 chars) that summarizes the topic.
|
||||
Return ONLY the title. No preamble.
|
||||
Language: English
|
||||
Snippet:
|
||||
User: ${userMessage}
|
||||
Assistant: ${aiResponse}`;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Task Breakdown
|
||||
|
||||
### Task 1: Update .env.sample
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/.env.sample`
|
||||
|
||||
- [ ] **Step 1: Add DEFAULT_LANGUAGE configuration**
|
||||
|
||||
```bash
|
||||
# Default language for the system (zh, en, ja)
|
||||
# If not set, defaults to 'en' (English)
|
||||
DEFAULT_LANGUAGE=en
|
||||
```
|
||||
|
||||
### Task 2: Update constants.ts with env-based config
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/common/constants.ts`
|
||||
|
||||
- [ ] **Step 1: Replace hardcoded DEFAULT_LANGUAGE with env-based logic**
|
||||
|
||||
```typescript
|
||||
// Supported languages
|
||||
const SUPPORTED_LANGUAGES = ['zh', 'en', 'ja'] as const;
|
||||
|
||||
// Read DEFAULT_LANGUAGE from environment
|
||||
function getDefaultLanguage(): typeof SUPPORTED_LANGUAGES[number] {
|
||||
const envValue = process.env.DEFAULT_LANGUAGE?.toLowerCase();
|
||||
|
||||
// Validate: must be one of supported languages
|
||||
if (envValue && SUPPORTED_LANGUAGES.includes(envValue as typeof SUPPORTED_LANGUAGES[number])) {
|
||||
return envValue as typeof SUPPORTED_LANGUAGES[number];
|
||||
}
|
||||
|
||||
// Fallback to English if not set or invalid
|
||||
return 'en';
|
||||
}
|
||||
|
||||
// Default language - read from env, fallback to English
|
||||
export const DEFAULT_LANGUAGE = getDefaultLanguage();
|
||||
export const DEFAULT_LANGUAGE_FALLBACK = 'en';
|
||||
```
|
||||
|
||||
**Note:** No new dependencies required - uses simple array validation.
|
||||
|
||||
### Task 3: Fix i18n.service.ts getPrompt() Fallback
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/i18n/i18n.service.ts:180-235`
|
||||
|
||||
- [ ] **Step 1: Replace Japanese fallback with English**
|
||||
|
||||
Replace the entire `else` block (lines ~180-235) with English fallback prompts.
|
||||
|
||||
### Task 4: Fix i18n.service.ts getDocumentTitlePrompt() Fallback
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/i18n/i18n.service.ts:253-259`
|
||||
|
||||
- [ ] **Step 1: Replace Japanese fallback with English**
|
||||
|
||||
Replace the `else` block with English prompt.
|
||||
|
||||
### Task 5: Fix i18n.service.ts getChatTitlePrompt() Fallback
|
||||
|
||||
**Files:**
|
||||
- Modify: `server/src/i18n/i18n.service.ts:278-285`
|
||||
|
||||
- [ ] **Step 1: Replace Japanese fallback with English**
|
||||
|
||||
Replace the `else` block with English prompt.
|
||||
|
||||
### Task 6: Verification
|
||||
|
||||
- [ ] **Step 1: Run TypeScript check**
|
||||
|
||||
```bash
|
||||
cd server && yarn build
|
||||
```
|
||||
|
||||
Expected: Build succeeds without errors
|
||||
|
||||
- [ ] **Step 2: Test env configuration**
|
||||
|
||||
```bash
|
||||
# Test with DEFAULT_LANGUAGE=zh
|
||||
DEFAULT_LANGUAGE=zh yarn start:dev
|
||||
|
||||
# Test with DEFAULT_LANGUAGE=en
|
||||
DEFAULT_LANGUAGE=en yarn start:dev
|
||||
|
||||
# Test with no DEFAULT_LANGUAGE (should default to 'en')
|
||||
yarn start:dev
|
||||
```
|
||||
|
||||
- [ ] **Step 3: Verify fallback behavior**
|
||||
|
||||
Test that unsupported language code returns English instead of Japanese.
|
||||
|
||||
---
|
||||
|
||||
## Expected Result
|
||||
|
||||
| Configuration | Before | After |
|
||||
|---------------|--------|-------|
|
||||
| DEFAULT_LANGUAGE source | Hardcoded `'zh'` | Read from `.env`, fallback to `'en'` |
|
||||
| Fallback language | `'ja'` | `'en'` |
|
||||
| constants.ts comment | 日本語 | English |
|
||||
| getPrompt fallback | 日本語 | English |
|
||||
| getDocumentTitlePrompt fallback | 日本語 | English |
|
||||
| getChatTitlePrompt fallback | 日本語 | English |
|
||||
|
||||
### Configuration Examples
|
||||
|
||||
| .env Setting | Result |
|
||||
|--------------|--------|
|
||||
| `DEFAULT_LANGUAGE=zh` | 中文 |
|
||||
| `DEFAULT_LANGUAGE=en` | English |
|
||||
| `DEFAULT_LANGUAGE=ja` | 日本語 |
|
||||
| (not set) | English (default) |
|
||||
| `DEFAULT_LANGUAGE=invalid` | English (fallback) |
|
||||
|
||||
---
|
||||
|
||||
## Risk & Mitigation
|
||||
|
||||
**Low Risk:** Changes are configuration-based with safe defaults.
|
||||
|
||||
**Mitigation:**
|
||||
- Hardcoded `'en'` fallback ensures safe behavior even if env is misconfigured
|
||||
- Keep 'zh', 'en', 'ja' as supported languages
|
||||
- Japanese ('ja') still fully supported when explicitly configured
|
||||
@@ -1,505 +0,0 @@
|
||||
# Talent Assessment Management System - Design Document
|
||||
|
||||
**Date**: 2026-03-16
|
||||
**Author**: Sisyphus AI Agent
|
||||
**Status**: Draft for Review
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Enhance the existing talent assessment system with comprehensive management functionality allowing administrators to configure assessment generation through keywords, question counts, and style requirements. The system will extract relevant content from knowledge bases and generate customized assessments based on these configurations.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Overview
|
||||
|
||||
### Current System Flow
|
||||
```
|
||||
User selects knowledge base → System generates 3-5 questions → User answers → AI grades → Report generated
|
||||
```
|
||||
|
||||
### Enhanced System Flow
|
||||
```
|
||||
User selects template/inline config → System filters content by keywords → Generates N questions with specified style → User answers → AI grades → Report generated
|
||||
```
|
||||
|
||||
### Key Components
|
||||
|
||||
#### Backend (NestJS)
|
||||
- **AssessmentConfig Entity**: Store reusable assessment templates
|
||||
- **Enhanced Question Generator**: Accept config parameters for customization
|
||||
- **Content Filter Service**: Filter knowledge base content by keywords
|
||||
- **Config Management API**: CRUD operations for assessment templates
|
||||
|
||||
#### Frontend (React)
|
||||
- **Template Management Page**: Create/edit assessment templates
|
||||
- **Enhanced Assessment Start**: Support template selection + inline config
|
||||
- **Configuration UI**: Keywords input, question count slider, style presets
|
||||
|
||||
---
|
||||
|
||||
## 2. Database Schema Design
|
||||
|
||||
### New Entity: AssessmentConfig
|
||||
|
||||
```typescript
|
||||
@Entity('assessment_configs')
|
||||
export class AssessmentConfig {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column()
|
||||
name: string;
|
||||
|
||||
@Column({ nullable: true })
|
||||
description: string;
|
||||
|
||||
@Column({ name: 'tenant_id', nullable: true })
|
||||
tenantId: string;
|
||||
|
||||
@Column({ type: 'simple-json' })
|
||||
configuration: AssessmentConfigSettings;
|
||||
|
||||
@Column({ default: true })
|
||||
isActive: boolean;
|
||||
|
||||
@CreateDateColumn({ name: 'created_at' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ name: 'updated_at' })
|
||||
updatedAt: Date;
|
||||
}
|
||||
```
|
||||
|
||||
### Configuration Settings Interface
|
||||
|
||||
```typescript
|
||||
interface AssessmentConfigSettings {
|
||||
// Keywords for content filtering and AI focus
|
||||
keywords: string[];
|
||||
|
||||
// Question generation parameters
|
||||
questionCount: number; // 1-20
|
||||
difficultyDistribution: {
|
||||
standard: number; // 0-100%
|
||||
advanced: number; // 0-100%
|
||||
specialist: number; // 0-100%
|
||||
};
|
||||
|
||||
// Style requirements
|
||||
style: {
|
||||
tone: 'formal' | 'conversational' | 'technical' | 'business';
|
||||
questionTypes: ('multiple-choice' | 'open-ended' | 'scenario-based' | 'technical')[];
|
||||
assessmentGoals: ('knowledge-check' | 'problem-solving' | 'critical-thinking' | 'application')[];
|
||||
};
|
||||
|
||||
// Content filtering
|
||||
contentFilter: {
|
||||
minContentLength: number; // Minimum characters for content to be considered
|
||||
maxContextChunks: number; // Max chunks to include in context
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Modified Session Entity
|
||||
|
||||
Add reference to assessment config:
|
||||
|
||||
```typescript
|
||||
@Entity('assessment_sessions')
|
||||
export class AssessmentSession {
|
||||
// ... existing fields ...
|
||||
|
||||
@Column({ name: 'config_id', nullable: true })
|
||||
configId: string | null;
|
||||
|
||||
@ManyToOne(() => AssessmentConfig, { nullable: true })
|
||||
@JoinColumn({ name: 'config_id' })
|
||||
config: AssessmentConfig;
|
||||
|
||||
// Inline configuration (when no template used)
|
||||
@Column({ type: 'simple-json', nullable: true, name: 'inline_config' })
|
||||
inlineConfig: AssessmentConfigSettings | null;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Backend Implementation
|
||||
|
||||
### 3.1 New Service: AssessmentConfigService
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class AssessmentConfigService {
|
||||
async createConfig(tenantId: string, settings: AssessmentConfigSettings): Promise<AssessmentConfig>
|
||||
async getConfigs(tenantId: string): Promise<AssessmentConfig[]>
|
||||
async getConfig(id: string, tenantId: string): Promise<AssessmentConfig>
|
||||
async updateConfig(id: string, tenantId: string, settings: Partial<AssessmentConfigSettings>): Promise<AssessmentConfig>
|
||||
async deleteConfig(id: string, tenantId: string): Promise<void>
|
||||
async getActiveConfigs(tenantId: string): Promise<AssessmentConfig[]>
|
||||
}
|
||||
```
|
||||
|
||||
### 3.2 Enhanced Content Filter Service
|
||||
|
||||
```typescript
|
||||
@Injectable()
|
||||
export class ContentFilterService {
|
||||
async filterByKeywords(
|
||||
content: string,
|
||||
keywords: string[],
|
||||
method: 'exact' | 'semantic' | 'hybrid' = 'hybrid'
|
||||
): Promise<string> {
|
||||
// Extract relevant sections based on keywords
|
||||
// Use semantic similarity for broader matching
|
||||
// Return filtered content for question generation
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 Enhanced Question Generator Node
|
||||
|
||||
Modify `generator.node.ts` to accept configuration:
|
||||
|
||||
```typescript
|
||||
interface GeneratorConfig {
|
||||
keywords?: string[];
|
||||
questionCount?: number;
|
||||
difficultyDistribution?: AssessmentConfigSettings['difficultyDistribution'];
|
||||
style?: AssessmentConfigSettings['style'];
|
||||
}
|
||||
|
||||
export const questionGeneratorNode = async (
|
||||
state: EvaluationState,
|
||||
config?: RunnableConfig & { generatorConfig?: GeneratorConfig }
|
||||
): Promise<Partial<EvaluationState>> {
|
||||
// 1. Filter knowledge base content by keywords if provided
|
||||
// 2. Generate questions based on configuration
|
||||
// 3. Enforce question count and difficulty distribution
|
||||
// 4. Apply style requirements to question generation
|
||||
}
|
||||
```
|
||||
|
||||
### 3.4 API Endpoints
|
||||
|
||||
#### Config Management
|
||||
```
|
||||
POST /api/v1/assessment/configs - Create assessment config
|
||||
GET /api/v1/assessment/configs - List configs
|
||||
GET /api/v1/assessment/configs/:id - Get config details
|
||||
PUT /api/v1/assessment/configs/:id - Update config
|
||||
DELETE /api/v1/assessment/configs/:id - Delete config
|
||||
```
|
||||
|
||||
#### Enhanced Session Start
|
||||
```typescript
|
||||
// Existing: POST /assessment/start
|
||||
// New parameters supported:
|
||||
interface StartSessionRequest {
|
||||
knowledgeBaseId: string;
|
||||
language?: string;
|
||||
configId?: string; // Use template
|
||||
inlineConfig?: AssessmentConfigSettings; // Or use inline config
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Frontend Implementation
|
||||
|
||||
### 4.1 New Page: AssessmentTemplatePage
|
||||
|
||||
**Location**: `web/src/pages/workspace/AssessmentTemplatePage.tsx`
|
||||
|
||||
**Features**:
|
||||
- List existing templates with quick actions (edit, clone, delete)
|
||||
- Create new template wizard
|
||||
- Template preview with configuration summary
|
||||
- Status toggle (active/inactive)
|
||||
|
||||
### 4.2 Enhanced Assessment Start Component
|
||||
|
||||
**Location**: `web/components/views/AssessmentView.tsx`
|
||||
|
||||
**New UI Elements**:
|
||||
- Template selection dropdown
|
||||
- "Use custom settings" toggle for inline config
|
||||
- Configuration panel with:
|
||||
- Keywords input (tags)
|
||||
- Question count slider (1-20)
|
||||
- Difficulty distribution pie chart/editor
|
||||
- Style preset buttons
|
||||
- Advanced settings collapsible section
|
||||
|
||||
### 4.3 Configuration UI Components
|
||||
|
||||
```typescript
|
||||
// KeywordsInput.tsx
|
||||
- Tag-based input for keywords
|
||||
- Suggestions from knowledge base content
|
||||
- Visual feedback for keyword matching
|
||||
|
||||
// QuestionCountSlider.tsx
|
||||
- Slider from 1-20 questions
|
||||
- Smart defaults based on content size
|
||||
|
||||
// DifficultyDistribution.tsx
|
||||
- Interactive pie chart
|
||||
- Drag to adjust percentages
|
||||
- Real-time validation (must sum to 100%)
|
||||
|
||||
// StylePresets.tsx
|
||||
- Pre-defined style combinations
|
||||
- Custom style builder
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Data Flow & Processing
|
||||
|
||||
### 5.1 Content Filtering Pipeline
|
||||
|
||||
```
|
||||
1. Load knowledge base content
|
||||
2. Apply keyword filtering (semantic similarity)
|
||||
3. Chunk content into manageable segments
|
||||
4. Rank segments by relevance to keywords
|
||||
5. Select top N segments for context
|
||||
6. Pass filtered content to question generator
|
||||
```
|
||||
|
||||
### 5.2 Question Generation Pipeline
|
||||
|
||||
```
|
||||
1. Receive configuration + filtered content
|
||||
2. Analyze content for key concepts
|
||||
3. Generate questions matching:
|
||||
- Specified count
|
||||
- Difficulty distribution
|
||||
- Question types
|
||||
- Style requirements
|
||||
4. Validate question quality
|
||||
5. Return structured question set
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Security & Validation
|
||||
|
||||
### 6.1 Input Validation
|
||||
|
||||
```typescript
|
||||
// Keywords validation
|
||||
@ValidateNested()
|
||||
@ArrayMaxSize(50)
|
||||
keywords: string[];
|
||||
|
||||
// Question count validation
|
||||
@IsInt()
|
||||
@Min(1)
|
||||
@Max(20)
|
||||
questionCount: number;
|
||||
|
||||
// Difficulty distribution validation
|
||||
@ValidateNested()
|
||||
@IsNotEmpty()
|
||||
difficultyDistribution: {
|
||||
@IsInt()
|
||||
@Min(0)
|
||||
@Max(100)
|
||||
standard: number;
|
||||
|
||||
// ... similar for advanced, specialist
|
||||
};
|
||||
|
||||
// Validate sum equals 100
|
||||
@ValidatorConstraint({ name: 'difficultySum', async: false })
|
||||
export class DifficultySumConstraint implements ValidatorConstraintInterface {
|
||||
validate(value: any) {
|
||||
return (value.standard + value.advanced + value.specialist) === 100;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 Access Control
|
||||
|
||||
- **Tenant Isolation**: Configs scoped to tenant
|
||||
- **Role-Based Access**: Only admins can manage templates
|
||||
- **Ownership Tracking**: Track who created/modified each config
|
||||
|
||||
---
|
||||
|
||||
## 7. Error Handling
|
||||
|
||||
### 7.1 Common Error Scenarios
|
||||
|
||||
| Error | Cause | Resolution |
|
||||
|-------|-------|------------|
|
||||
| `INSUFFICIENT_CONTENT` | Filtered content too short | Relax keyword constraints or select broader keywords |
|
||||
| `INVALID_CONFIG` | Config validation failed | Return validation errors with specific field issues |
|
||||
| `CONFIG_NOT_FOUND` | Template ID doesn't exist | Return 404 with config ID |
|
||||
| `KEYWORD_TOO_SPECIFIC` | No content matches keywords | Suggest broader keywords or use all content |
|
||||
|
||||
### 7.2 Error Response Format
|
||||
|
||||
```json
|
||||
{
|
||||
"error": {
|
||||
"code": "INSUFFICIENT_CONTENT",
|
||||
"message": "Filtered content is too short for question generation",
|
||||
"details": {
|
||||
"minLength": 1000,
|
||||
"actualLength": 250,
|
||||
"suggestedKeywords": ["technology", "software"]
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Testing Strategy
|
||||
|
||||
### 8.1 Unit Tests
|
||||
|
||||
**Backend**:
|
||||
- `AssessmentConfigService` - CRUD operations
|
||||
- `ContentFilterService` - Keyword filtering logic
|
||||
- `questionGeneratorNode` - Question generation with config
|
||||
|
||||
**Frontend**:
|
||||
- Configuration UI components
|
||||
- Template management page
|
||||
- Integration with existing assessment flow
|
||||
|
||||
### 8.2 Integration Tests
|
||||
|
||||
- End-to-end assessment creation with templates
|
||||
- Content filtering accuracy validation
|
||||
- Question generation quality assessment
|
||||
|
||||
### 8.3 Performance Tests
|
||||
|
||||
- Large knowledge base filtering
|
||||
- Multiple concurrent template usage
|
||||
- API response times under load
|
||||
|
||||
---
|
||||
|
||||
## 9. Migration Plan
|
||||
|
||||
### Phase 1: Database Schema (1 day)
|
||||
1. Create `assessment_configs` table
|
||||
2. Add `config_id` and `inline_config` to `assessment_sessions`
|
||||
3. Create indexes for performance
|
||||
|
||||
### Phase 2: Backend API (2 days)
|
||||
1. Implement `AssessmentConfigService`
|
||||
2. Create API endpoints
|
||||
3. Enhance question generator node
|
||||
4. Add content filtering service
|
||||
|
||||
### Phase 3: Frontend UI (2 days)
|
||||
1. Create template management page
|
||||
2. Enhance assessment start component
|
||||
3. Add configuration UI components
|
||||
4. Integrate with existing flows
|
||||
|
||||
### Phase 4: Testing & Polish (1 day)
|
||||
1. Comprehensive testing
|
||||
2. Bug fixes
|
||||
3. Performance optimization
|
||||
4. Documentation
|
||||
|
||||
---
|
||||
|
||||
## 10. Success Metrics
|
||||
|
||||
### 10.1 Functional Metrics
|
||||
- ✅ Templates can be created/edited/deleted
|
||||
- ✅ Question count configurable (1-20)
|
||||
- ✅ Keywords filter content effectively
|
||||
- ✅ Style requirements applied to questions
|
||||
- ✅ Backward compatibility maintained
|
||||
|
||||
### 10.2 Quality Metrics
|
||||
- Question relevance score > 80% (user feedback)
|
||||
- Content filtering accuracy > 90%
|
||||
- API response time < 500ms
|
||||
- Frontend load time < 2s
|
||||
|
||||
### 10.3 User Experience
|
||||
- Template creation time < 2 minutes
|
||||
- Assessment start with config < 10 seconds
|
||||
- Configuration UI intuitive (measured by user testing)
|
||||
|
||||
---
|
||||
|
||||
## 11. Risks & Mitigations
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
|------|--------|------------|
|
||||
| Keyword filtering too restrictive | Low quality questions | Implement semantic matching + fallback |
|
||||
| Config complexity overwhelms users | Poor adoption | Progressive disclosure + presets |
|
||||
| Performance degradation with large KB | Slow generation | Content chunking + caching |
|
||||
| Breaking existing assessments | System disruption | Feature flag + gradual rollout |
|
||||
|
||||
---
|
||||
|
||||
## 12. Future Enhancements
|
||||
|
||||
1. **AI-powered keyword suggestions** based on content analysis
|
||||
2. **Template sharing** across tenants (with permissions)
|
||||
3. **Assessment analytics** - track which configs produce best results
|
||||
4. **Multi-language templates** - store configs in multiple languages
|
||||
5. **Integration with HR systems** - export assessment results
|
||||
|
||||
---
|
||||
|
||||
## Appendix A: Configuration Examples
|
||||
|
||||
### Example 1: Technical Interview Template
|
||||
```json
|
||||
{
|
||||
"name": "Senior Developer Interview",
|
||||
"keywords": ["system design", "algorithms", "database", "API", "security"],
|
||||
"questionCount": 8,
|
||||
"difficultyDistribution": {
|
||||
"standard": 25,
|
||||
"advanced": 50,
|
||||
"specialist": 25
|
||||
},
|
||||
"style": {
|
||||
"tone": "technical",
|
||||
"questionTypes": ["technical", "scenario-based"],
|
||||
"assessmentGoals": ["problem-solving", "critical-thinking"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Example 2: Knowledge Check Template
|
||||
```json
|
||||
{
|
||||
"name": "Weekly Training Quiz",
|
||||
"keywords": [],
|
||||
"questionCount": 5,
|
||||
"difficultyDistribution": {
|
||||
"standard": 70,
|
||||
"advanced": 30,
|
||||
"specialist": 0
|
||||
},
|
||||
"style": {
|
||||
"tone": "formal",
|
||||
"questionTypes": ["multiple-choice", "open-ended"],
|
||||
"assessmentGoals": ["knowledge-check"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Document Status**: Ready for review
|
||||
**Next Steps**:
|
||||
1. Review this design document
|
||||
2. Approve or request changes
|
||||
3. Create implementation plan using writing-plans skill
|
||||
@@ -1,456 +0,0 @@
|
||||
# Feishu WebSocket Integration - Design Document
|
||||
|
||||
**Date**: 2026-03-17
|
||||
**Author**: Sisyphus AI Agent
|
||||
**Status**: Draft for Review
|
||||
|
||||
## Executive Summary
|
||||
|
||||
Add WebSocket long-connection support for Feishu bot integration, enabling internal network deployment without requiring public domain or internet-facing endpoints. The system will support both existing webhook mode and new WebSocket mode, allowing users to choose their preferred connection method.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Overview
|
||||
|
||||
### Current Architecture (Webhook Mode)
|
||||
|
||||
```
|
||||
Feishu Cloud → Public Domain → NAT/Firewall → Internal Server
|
||||
└→ POST /api/feishu/webhook/:appId
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
- Requires public domain with SSL certificate
|
||||
- Requires NAT/firewall port forwarding or reverse proxy
|
||||
- Not suitable for pure internal network deployment
|
||||
|
||||
### New Architecture (WebSocket Mode)
|
||||
|
||||
```
|
||||
Feishu Cloud ←──────── WebSocket (wss://open.feishu.cn) ──────── Internal Server
|
||||
↑
|
||||
Feishu Cloud → Webhook (optional backup) → Internal Server
|
||||
```
|
||||
|
||||
**Advantages:**
|
||||
- No public domain required
|
||||
- No NAT/firewall configuration needed
|
||||
- Direct connection from internal network to Feishu cloud
|
||||
- Real-time message delivery (milliseconds vs minutes)
|
||||
- Connection复用,资源效率更高
|
||||
|
||||
### Architecture Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Feishu Open Platform │
|
||||
│ (WebSocket Event Subscription) │
|
||||
└───────────────────────┬─────────────────────────────────────┘
|
||||
│
|
||||
┌─────────────┴─────────────┐
|
||||
│ │
|
||||
┌─────▼──────┐ ┌──────▼─────┐
|
||||
│ Bot A │ │ Bot B │
|
||||
│ ws://.../A │ │ ws://.../B │
|
||||
└─────┬──────┘ └──────┬─────┘
|
||||
│ │
|
||||
┌─────▼──────────────────────────▼──────┐
|
||||
│ AuraK Server │
|
||||
│ ┌──────────────────────────────────┐ │
|
||||
│ │ FeishuModule │ │
|
||||
│ │ ┌────────────────────────────┐ │ │
|
||||
│ │ │ FeishuWsManager │ │ │
|
||||
│ │ │ - per-bot connections │ │ │
|
||||
│ │ │ - auto-reconnect │ │ │
|
||||
│ │ │ - message routing │ │ │
|
||||
│ │ └────────────────────────────┘ │ │
|
||||
│ │ ┌────────────────────────────┐ │ │
|
||||
│ │ │ FeishuService │ │ │
|
||||
│ │ │ - existing logic │ │ │
|
||||
│ │ │ - new ws connect/disconnect│ │ │
|
||||
│ │ └────────────────────────────┘ │ │
|
||||
│ │ ┌────────────────────────────┐ │ │
|
||||
│ │ │ FeishuController │ │ │
|
||||
│ │ │ - webhook endpoints │ │ │
|
||||
│ │ │ - ws management APIs │ │ │
|
||||
│ │ └────────────────────────────┘ │ │
|
||||
│ └──────────────────────────────────┘ │
|
||||
└──────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Implementation Plan
|
||||
|
||||
### 2.1 New Components
|
||||
|
||||
#### 2.1.1 FeishuWsManager
|
||||
|
||||
**Purpose**: Manage WebSocket connections for each bot
|
||||
|
||||
**Responsibilities:**
|
||||
- Establish and maintain WebSocket connections
|
||||
- Handle connection lifecycle (connect, disconnect, reconnect)
|
||||
- Route incoming messages to appropriate bot handlers
|
||||
- Manage connection state per bot
|
||||
|
||||
**Location**: `server/src/feishu/feishu-ws.manager.ts`
|
||||
|
||||
**Key Methods:**
|
||||
```typescript
|
||||
class FeishuWsManager {
|
||||
// Start WebSocket connection for a bot
|
||||
async connect(bot: FeishuBot): Promise<void>
|
||||
|
||||
// Stop WebSocket connection for a bot
|
||||
async disconnect(botId: string): Promise<void>
|
||||
|
||||
// Get connection status
|
||||
getStatus(botId: string): ConnectionStatus
|
||||
|
||||
// Get all active connections
|
||||
getAllConnections(): Map<string, ConnectionStatus>
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.1.2 ConnectionStatus Type
|
||||
|
||||
```typescript
|
||||
enum ConnectionState {
|
||||
DISCONNECTED = 'disconnected',
|
||||
CONNECTING = 'connecting',
|
||||
CONNECTED = 'connected',
|
||||
ERROR = 'error'
|
||||
}
|
||||
|
||||
interface ConnectionStatus {
|
||||
botId: string
|
||||
state: ConnectionState
|
||||
connectedAt?: Date
|
||||
lastHeartbeat?: Date
|
||||
error?: string
|
||||
}
|
||||
```
|
||||
|
||||
### 2.2 Modified Components
|
||||
|
||||
#### 2.2.1 FeishuService
|
||||
|
||||
**New Methods:**
|
||||
```typescript
|
||||
class FeishuService {
|
||||
// Start WebSocket connection for a bot
|
||||
async startWsConnection(botId: string): Promise<void>
|
||||
|
||||
// Stop WebSocket connection for a bot
|
||||
async stopWsConnection(botId: string): Promise<void>
|
||||
|
||||
// Get connection status
|
||||
async getWsStatus(botId: string): Promise<ConnectionStatus>
|
||||
|
||||
// List all connection statuses
|
||||
async getAllWsStatuses(): Promise<ConnectionStatus[]>
|
||||
}
|
||||
```
|
||||
|
||||
#### 2.2.2 FeishuController
|
||||
|
||||
**New Endpoints:**
|
||||
```typescript
|
||||
// POST /feishu/bots/:id/ws/connect - Start WebSocket connection
|
||||
// POST /feishu/bots/:id/ws/disconnect - Stop WebSocket connection
|
||||
// GET /feishu/bots/:id/ws/status - Get connection status
|
||||
// GET /feishu/ws/status - Get all connection statuses
|
||||
```
|
||||
|
||||
**Modified Endpoints:**
|
||||
- Keep existing webhook endpoints unchanged
|
||||
- Add WebSocket status indicator in bot list response
|
||||
|
||||
#### 2.2.3 FeishuBot Entity
|
||||
|
||||
**New Fields:**
|
||||
```typescript
|
||||
@Entity('feishu_bots')
|
||||
export class FeishuBot {
|
||||
// ... existing fields ...
|
||||
|
||||
@Column({ default: false })
|
||||
useWebSocket: boolean
|
||||
|
||||
@Column({ nullable: true })
|
||||
wsConnectionState: string
|
||||
}
|
||||
```
|
||||
|
||||
### 2.3 Feishu SDK Integration
|
||||
|
||||
**Package**: `@larksuiteoapi/node-sdk`
|
||||
|
||||
**Installation:**
|
||||
```bash
|
||||
cd server && yarn add @larksuiteoapi/node-sdk
|
||||
```
|
||||
|
||||
**Configuration:**
|
||||
```typescript
|
||||
import { EventDispatcher, Conf } from '@larksuiteoapi/node-sdk'
|
||||
|
||||
const client = new EventDispatcher({
|
||||
appId: bot.appId,
|
||||
appSecret: bot.appSecret,
|
||||
verificationToken: bot.verificationToken,
|
||||
}, {
|
||||
logger: console
|
||||
})
|
||||
```
|
||||
|
||||
### 2.4 Event Handling
|
||||
|
||||
**Flow for WebSocket Mode:**
|
||||
```
|
||||
Feishu Cloud ──WebSocket──> FeishuWsManager.on('message')
|
||||
│
|
||||
▼
|
||||
Parse event type
|
||||
│
|
||||
┌───────────────┼───────────────┐
|
||||
│ │ │
|
||||
im.message. im.message. other
|
||||
receive_v1 p2p_msg_received
|
||||
│ │
|
||||
▼ ▼
|
||||
FeishuService.processChatMessage()
|
||||
│
|
||||
▼
|
||||
Send response via
|
||||
FeishuService.sendTextMessage()
|
||||
```
|
||||
|
||||
### 2.5 Configuration Changes
|
||||
|
||||
**Feishu Open Platform:**
|
||||
|
||||
Users need to configure in Feishu developer console:
|
||||
1. Go to "Event Subscription" (事件与回调)
|
||||
2. Select "Use long connection to receive events" (使用长连接接收事件)
|
||||
3. Add event: `im.message.receive_v1`
|
||||
4. **Important**: Must start local WebSocket client first before saving
|
||||
|
||||
---
|
||||
|
||||
## 3. Data Flow
|
||||
|
||||
### WebSocket Message Flow
|
||||
|
||||
```
|
||||
1. User triggers connect API
|
||||
│
|
||||
▼
|
||||
2. FeishuController.connect(botId)
|
||||
│
|
||||
▼
|
||||
3. FeishuService.startWsConnection(botId)
|
||||
│
|
||||
▼
|
||||
4. FeishuWsManager.connect(bot)
|
||||
│
|
||||
▼
|
||||
5. SDK establishes WebSocket to open.feishu.cn
|
||||
│
|
||||
▼
|
||||
6. Connection established, events flow:
|
||||
│
|
||||
├─> on('message') ──> _processEvent() ──> _handleMessage()
|
||||
│ │
|
||||
│ ▼
|
||||
│ FeishuService.processChatMessage()
|
||||
│ │
|
||||
│ ▼
|
||||
│ FeishuService.sendTextMessage() (via SDK)
|
||||
│
|
||||
├─> on('error') ──> log error ──> trigger reconnect
|
||||
│
|
||||
└─> on('close') ──> trigger auto-reconnect
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Error Handling
|
||||
|
||||
### Connection Errors
|
||||
|
||||
| Error Type | Handling |
|
||||
|------------|----------|
|
||||
| Network timeout | Retry with exponential backoff (max 5 attempts) |
|
||||
| Invalid credentials | Mark bot as error state, notify user |
|
||||
| Token expired | Refresh token, reconnect |
|
||||
| Feishu server error | Wait 30s, retry |
|
||||
|
||||
### Auto-Reconnect Strategy
|
||||
|
||||
```
|
||||
Initial delay: 1 second
|
||||
Max delay: 60 seconds
|
||||
Backoff multiplier: 2x
|
||||
Max attempts: 5
|
||||
Reset on successful connection
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. API Design
|
||||
|
||||
### 5.1 Connect WebSocket
|
||||
|
||||
```
|
||||
POST /api/feishu/bots/:id/ws/connect
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"success": true,
|
||||
"botId": "bot_xxx",
|
||||
"status": "connecting"
|
||||
}
|
||||
|
||||
Response 400:
|
||||
{
|
||||
"success": false,
|
||||
"error": "Bot not found or disabled"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.2 Disconnect WebSocket
|
||||
|
||||
```
|
||||
POST /api/feishu/bots/:id/ws/disconnect
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"success": true,
|
||||
"botId": "bot_xxx",
|
||||
"status": "disconnected"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 Get Connection Status
|
||||
|
||||
```
|
||||
GET /api/feishu/bots/:id/ws/status
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"botId": "bot_xxx",
|
||||
"state": "connected",
|
||||
"connectedAt": "2026-03-17T10:00:00Z",
|
||||
"lastHeartbeat": "2026-03-17T10:05:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.4 Get All Statuses
|
||||
|
||||
```
|
||||
GET /api/feishu/ws/status
|
||||
|
||||
Response 200:
|
||||
{
|
||||
"connections": [
|
||||
{
|
||||
"botId": "bot_xxx",
|
||||
"state": "connected",
|
||||
"connectedAt": "2026-03-17T10:00:00Z"
|
||||
},
|
||||
{
|
||||
"botId": "bot_yyy",
|
||||
"state": "disconnected"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Security Considerations
|
||||
|
||||
1. **Credential Storage**: App ID and App Secret stored encrypted in database
|
||||
2. **Connection Validation**: Verify bot belongs to authenticated user before connect
|
||||
3. **Rate Limiting**: Implement per-bot message rate limiting
|
||||
4. **Connection Limits**: Max 10 concurrent WebSocket connections per server instance
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing Strategy
|
||||
|
||||
### Unit Tests
|
||||
- FeishuWsManager connection lifecycle
|
||||
- Message routing logic
|
||||
- Error handling and reconnection
|
||||
|
||||
### Integration Tests
|
||||
- Full WebSocket connection flow
|
||||
- Message send/receive cycle
|
||||
- Reconnection after network failure
|
||||
|
||||
### Manual Testing
|
||||
- Local development without ngrok
|
||||
- Verify webhook still works alongside WebSocket
|
||||
|
||||
---
|
||||
|
||||
## 8. Backward Compatibility
|
||||
|
||||
- **Existing webhook endpoints**: Unchanged, continue to work
|
||||
- **Bot configuration**: Existing bots keep webhook mode by default
|
||||
- **Migration path**: Users can switch to WebSocket anytime via API
|
||||
- **Dual mode**: Both modes can run simultaneously for different bots
|
||||
|
||||
---
|
||||
|
||||
## 9. Migration Guide
|
||||
|
||||
### For Existing Users
|
||||
|
||||
1. Update AuraK to new version (with WebSocket support)
|
||||
2. Install Feishu SDK: `yarn add @larksuiteoapi/node-sdk`
|
||||
3. In Feishu Developer Console:
|
||||
- Start local WebSocket server
|
||||
- Change event subscription to "Use long connection"
|
||||
4. Call `POST /api/feishu/bots/:id/ws/connect` to activate
|
||||
|
||||
---
|
||||
|
||||
## 10. Limitations
|
||||
|
||||
1. **Outbound Network Required**: Server must be able to reach `open.feishu.cn` via WebSocket
|
||||
2. **Single Connection Per Bot**: Each bot needs its own WebSocket connection
|
||||
3. **Feishu SDK Required**: Must install official SDK, cannot use raw WebSocket
|
||||
4. **Private Feishu**: Does not support Feishu private deployment (自建飞书)
|
||||
|
||||
---
|
||||
|
||||
## 11. File Changes Summary
|
||||
|
||||
### New Files
|
||||
- `server/src/feishu/feishu-ws.manager.ts` - WebSocket connection manager
|
||||
- `server/src/feishu/dto/ws-status.dto.ts` - WebSocket status DTOs
|
||||
|
||||
### Modified Files
|
||||
- `server/src/feishu/feishu.service.ts` - Add WS methods
|
||||
- `server/src/feishu/feishu.controller.ts` - Add WS endpoints
|
||||
- `server/src/feishu/entities/feishu-bot.entity.ts` - Add WS fields
|
||||
- `server/src/feishu/feishu.module.ts` - Register new manager
|
||||
|
||||
### Dependencies
|
||||
- Add: `@larksuiteoapi/node-sdk`
|
||||
|
||||
---
|
||||
|
||||
## 12. Success Criteria
|
||||
|
||||
- [ ] Server can establish WebSocket connection to Feishu
|
||||
- [ ] Messages received via WebSocket are processed correctly
|
||||
- [ ] Responses sent back to Feishu via SDK
|
||||
- [ ] Auto-reconnect works after network interruption
|
||||
- [ ] Webhook mode continues to work unchanged
|
||||
- [ ] Both modes can coexist for different bots
|
||||
- [ ] Internal network deployment works without public domain
|
||||
Reference in New Issue
Block a user