mirror of
https://github.com/cjo4m06/mcp-shrimp-task-manager.git
synced 2025-07-27 08:32:27 +08:00
新增工作日誌功能,記錄MCP與LLM之間的對話歷史,並提供查詢及清除日誌的工具。更新任務工具以在任務規劃、分析、反思及執行過程中自動記錄日誌,確保系統的可追溯性和完整性。
This commit is contained in:
parent
3d0f002dd8
commit
74f700e3cd
118
README.md
118
README.md
@ -9,6 +9,7 @@
|
|||||||
3. **依賴管理**:處理任務間的依賴關係,確保正確的執行順序
|
3. **依賴管理**:處理任務間的依賴關係,確保正確的執行順序
|
||||||
4. **執行追蹤**:監控任務執行進度和狀態
|
4. **執行追蹤**:監控任務執行進度和狀態
|
||||||
5. **任務驗證**:確保任務符合預期要求
|
5. **任務驗證**:確保任務符合預期要求
|
||||||
|
6. **工作日誌**:記錄和查詢對話歷史,提供任務執行過程的完整紀錄
|
||||||
|
|
||||||
## 任務管理工作流程
|
## 任務管理工作流程
|
||||||
|
|
||||||
@ -23,6 +24,123 @@
|
|||||||
7. **檢驗任務 (verify_task)**:檢查任務完成情況
|
7. **檢驗任務 (verify_task)**:檢查任務完成情況
|
||||||
8. **完成任務 (complete_task)**:標記任務完成並提供報告
|
8. **完成任務 (complete_task)**:標記任務完成並提供報告
|
||||||
|
|
||||||
|
## 工作日誌功能
|
||||||
|
|
||||||
|
### 功能概述
|
||||||
|
|
||||||
|
工作日誌系統記錄 MCP 與 LLM 之間的關鍵對話內容,主要目的是:
|
||||||
|
|
||||||
|
1. **保存執行脈絡**:記錄任務執行過程中的關鍵決策和事件
|
||||||
|
2. **提供可追溯性**:方便回溯查看歷史操作和決策理由
|
||||||
|
3. **知識累積**:積累經驗知識,避免重複解決相同問題
|
||||||
|
4. **效能分析**:提供數據支持,幫助分析系統效能和改進方向
|
||||||
|
|
||||||
|
系統會自動在以下關鍵時刻記錄日誌:
|
||||||
|
|
||||||
|
- 任務執行開始時
|
||||||
|
- 關鍵決策點
|
||||||
|
- 任務驗證過程中
|
||||||
|
- 任務完成時
|
||||||
|
|
||||||
|
### 如何使用日誌查詢工具
|
||||||
|
|
||||||
|
系統提供兩個主要的日誌管理工具:
|
||||||
|
|
||||||
|
#### 1. 查詢日誌 (list_conversation_log)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const logResult = await mcp.mcp_shrimp_task_manager.list_conversation_log({
|
||||||
|
taskId: "任務ID", // 選填,按特定任務過濾
|
||||||
|
startDate: "2025-01-01T00:00:00Z", // 選填,開始日期(ISO格式)
|
||||||
|
endDate: "2025-12-31T23:59:59Z", // 選填,結束日期(ISO格式)
|
||||||
|
limit: 20, // 選填,每頁顯示數量,預設20,最大100
|
||||||
|
offset: 0, // 選填,分頁起始位置,預設0
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
參數說明:
|
||||||
|
|
||||||
|
- `taskId`:按任務 ID 過濾日誌
|
||||||
|
- `startDate`:查詢開始日期,ISO 格式字串
|
||||||
|
- `endDate`:查詢結束日期,ISO 格式字串
|
||||||
|
- `limit`:每頁顯示的記錄數量,預設 20,最大 100
|
||||||
|
- `offset`:分頁偏移量,用於實現分頁查詢
|
||||||
|
|
||||||
|
#### 2. 清除日誌 (clear_conversation_log)
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const clearResult = await mcp.mcp_shrimp_task_manager.clear_conversation_log({
|
||||||
|
confirm: true, // 必填,確認刪除操作
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
注意:清除操作不可逆,請謹慎使用。
|
||||||
|
|
||||||
|
### 日誌數據結構
|
||||||
|
|
||||||
|
工作日誌的核心數據結構為 `ConversationEntry`:
|
||||||
|
|
||||||
|
```typescript
|
||||||
|
interface ConversationEntry {
|
||||||
|
id: string; // 唯一識別符
|
||||||
|
timestamp: Date; // 記錄時間
|
||||||
|
participant: ConversationParticipant; // 對話參與者(MCP或LLM)
|
||||||
|
summary: string; // 消息摘要,僅記錄關鍵信息
|
||||||
|
relatedTaskId?: string; // 關聯的任務ID(選填)
|
||||||
|
context?: string; // 額外上下文信息(選填)
|
||||||
|
}
|
||||||
|
|
||||||
|
enum ConversationParticipant {
|
||||||
|
MCP = "MCP", // 系統方
|
||||||
|
LLM = "LLM", // 模型方
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
日誌以 JSON 格式存儲在 `data/conversation_log.json` 文件中,當記錄數量超過閾值時,系統會自動將舊日誌歸檔並創建新的日誌文件。
|
||||||
|
|
||||||
|
### 開發者指南:擴展或修改日誌功能
|
||||||
|
|
||||||
|
#### 關鍵文件
|
||||||
|
|
||||||
|
1. **類型定義**:`src/types/index.ts`
|
||||||
|
|
||||||
|
- 包含 `ConversationEntry` 和 `ConversationParticipant` 等核心類型定義
|
||||||
|
|
||||||
|
2. **模型層**:`src/models/conversationLogModel.ts`
|
||||||
|
|
||||||
|
- 包含所有日誌相關的數據操作函數
|
||||||
|
- 日誌文件的讀寫、查詢、歸檔等功能
|
||||||
|
|
||||||
|
3. **工具層**:`src/tools/logTools.ts`
|
||||||
|
|
||||||
|
- 提供給外部調用的日誌工具函數
|
||||||
|
- 實現格式化輸出和參數處理
|
||||||
|
|
||||||
|
4. **摘要提取**:`src/utils/summaryExtractor.ts`
|
||||||
|
- 從完整對話中提取關鍵信息的工具
|
||||||
|
- 使用關鍵詞匹配和重要性評分算法
|
||||||
|
|
||||||
|
#### 如何擴展
|
||||||
|
|
||||||
|
1. **添加新的日誌查詢方式**
|
||||||
|
|
||||||
|
- 在 `conversationLogModel.ts` 中添加新的查詢函數
|
||||||
|
- 在 `logTools.ts` 中創建相應的工具函數
|
||||||
|
- 在 `index.ts` 中註冊新工具
|
||||||
|
|
||||||
|
2. **修改日誌存儲方式**
|
||||||
|
|
||||||
|
- 日誌默認以 JSON 文件形式存儲,可修改 `conversationLogModel.ts` 改用數據庫存儲
|
||||||
|
- 同時更新相關的讀寫函數
|
||||||
|
|
||||||
|
3. **優化摘要提取算法**
|
||||||
|
|
||||||
|
- 可在 `summaryExtractor.ts` 中增強或替換摘要提取算法
|
||||||
|
- 考慮添加基於機器學習的摘要方法
|
||||||
|
|
||||||
|
4. **添加新的日誌觸發點**
|
||||||
|
- 在關鍵流程中調用 `addConversationEntry` 函數添加新的日誌記錄點
|
||||||
|
|
||||||
## 任務依賴關係
|
## 任務依賴關係
|
||||||
|
|
||||||
系統支持兩種方式指定任務依賴:
|
系統支持兩種方式指定任務依賴:
|
||||||
|
56
src/index.ts
56
src/index.ts
@ -23,6 +23,14 @@ import {
|
|||||||
deleteTaskSchema,
|
deleteTaskSchema,
|
||||||
} from "./tools/taskTools.js";
|
} from "./tools/taskTools.js";
|
||||||
|
|
||||||
|
// 導入日誌工具函數
|
||||||
|
import {
|
||||||
|
listConversationLog,
|
||||||
|
listConversationLogSchema,
|
||||||
|
clearConversationLog,
|
||||||
|
clearConversationLogSchema,
|
||||||
|
} from "./tools/logTools.js";
|
||||||
|
|
||||||
// 導入提示模板
|
// 導入提示模板
|
||||||
import {
|
import {
|
||||||
planTaskPrompt,
|
planTaskPrompt,
|
||||||
@ -199,6 +207,54 @@ async function main() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 註冊日誌查詢工具
|
||||||
|
server.tool(
|
||||||
|
"list_conversation_log",
|
||||||
|
"查詢系統對話日誌,支持按任務 ID 或時間範圍過濾,提供分頁功能處理大量記錄",
|
||||||
|
{
|
||||||
|
taskId: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("按任務 ID 過濾對話記錄(選填)"),
|
||||||
|
startDate: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("起始日期過濾,格式為 ISO 日期字串(選填)"),
|
||||||
|
endDate: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("結束日期過濾,格式為 ISO 日期字串(選填)"),
|
||||||
|
limit: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.max(100)
|
||||||
|
.default(20)
|
||||||
|
.describe("返回結果數量限制,最大 100(預設:20)"),
|
||||||
|
offset: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.nonnegative()
|
||||||
|
.default(0)
|
||||||
|
.describe("分頁偏移量(預設:0)"),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
return await listConversationLog(args);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 註冊日誌清除工具
|
||||||
|
server.tool(
|
||||||
|
"clear_conversation_log",
|
||||||
|
"清除所有對話日誌記錄,需要明確確認以避免意外操作",
|
||||||
|
{
|
||||||
|
confirm: z.boolean().describe("確認刪除所有日誌記錄(此操作不可逆)"),
|
||||||
|
},
|
||||||
|
async (args) => {
|
||||||
|
return await clearConversationLog(args);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
// 註冊提示
|
// 註冊提示
|
||||||
server.prompt(
|
server.prompt(
|
||||||
"plan_task_prompt",
|
"plan_task_prompt",
|
||||||
|
462
src/models/conversationLogModel.ts
Normal file
462
src/models/conversationLogModel.ts
Normal file
@ -0,0 +1,462 @@
|
|||||||
|
import { ConversationEntry, ConversationParticipant } from "../types/index.js";
|
||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { v4 as uuidv4 } from "uuid";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { extractSummary } from "../utils/summaryExtractor.js";
|
||||||
|
|
||||||
|
// 確保獲取專案資料夾路徑
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
const PROJECT_ROOT = path.resolve(__dirname, "../..");
|
||||||
|
|
||||||
|
// 數據文件路徑
|
||||||
|
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, "data");
|
||||||
|
const CONVERSATION_LOG_FILE = path.join(DATA_DIR, "conversation_log.json");
|
||||||
|
|
||||||
|
// 配置參數
|
||||||
|
const MAX_LOG_ENTRIES = 10000; // 單個日誌文件最大條目數
|
||||||
|
const MAX_ARCHIVED_LOGS = 5; // 最大歸檔日誌文件數
|
||||||
|
const LOG_ENTRY_TRIM_THRESHOLD = 8000; // 當日誌超過該條目數時進行精簡
|
||||||
|
|
||||||
|
// 確保數據目錄存在
|
||||||
|
async function ensureDataDir() {
|
||||||
|
try {
|
||||||
|
await fs.access(DATA_DIR);
|
||||||
|
} catch (error) {
|
||||||
|
await fs.mkdir(DATA_DIR, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(CONVERSATION_LOG_FILE);
|
||||||
|
} catch (error) {
|
||||||
|
await fs.writeFile(CONVERSATION_LOG_FILE, JSON.stringify({ entries: [] }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 讀取所有對話日誌
|
||||||
|
async function readConversationLog(): Promise<ConversationEntry[]> {
|
||||||
|
await ensureDataDir();
|
||||||
|
const data = await fs.readFile(CONVERSATION_LOG_FILE, "utf-8");
|
||||||
|
const entries = JSON.parse(data).entries;
|
||||||
|
|
||||||
|
// 將日期字串轉換回 Date 物件
|
||||||
|
return entries.map((entry: any) => ({
|
||||||
|
...entry,
|
||||||
|
timestamp: entry.timestamp ? new Date(entry.timestamp) : new Date(),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 寫入所有對話日誌
|
||||||
|
async function writeConversationLog(
|
||||||
|
entries: ConversationEntry[]
|
||||||
|
): Promise<void> {
|
||||||
|
await ensureDataDir();
|
||||||
|
await fs.writeFile(
|
||||||
|
CONVERSATION_LOG_FILE,
|
||||||
|
JSON.stringify({ entries }, null, 2)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取所有對話日誌
|
||||||
|
export async function getAllConversationEntries(): Promise<
|
||||||
|
ConversationEntry[]
|
||||||
|
> {
|
||||||
|
return await readConversationLog();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根據 ID 獲取對話日誌條目
|
||||||
|
export async function getConversationEntryById(
|
||||||
|
entryId: string
|
||||||
|
): Promise<ConversationEntry | null> {
|
||||||
|
const entries = await readConversationLog();
|
||||||
|
return entries.find((entry) => entry.id === entryId) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加新的對話日誌條目
|
||||||
|
export async function addConversationEntry(
|
||||||
|
participant: ConversationParticipant,
|
||||||
|
summary: string,
|
||||||
|
relatedTaskId?: string,
|
||||||
|
context?: string
|
||||||
|
): Promise<ConversationEntry> {
|
||||||
|
const entries = await readConversationLog();
|
||||||
|
|
||||||
|
// 如果日誌條目超過閾值,進行日誌輪換
|
||||||
|
if (entries.length >= MAX_LOG_ENTRIES) {
|
||||||
|
await rotateLogFile();
|
||||||
|
return addConversationEntry(participant, summary, relatedTaskId, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果日誌條目超過精簡閾值,進行精簡處理
|
||||||
|
if (entries.length >= LOG_ENTRY_TRIM_THRESHOLD) {
|
||||||
|
await trimLogEntries();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 摘要太長時自動縮減
|
||||||
|
const processedSummary =
|
||||||
|
summary.length > 500 ? extractSummary(summary, 300) : summary;
|
||||||
|
|
||||||
|
const newEntry: ConversationEntry = {
|
||||||
|
id: uuidv4(),
|
||||||
|
timestamp: new Date(),
|
||||||
|
participant,
|
||||||
|
summary: processedSummary,
|
||||||
|
relatedTaskId,
|
||||||
|
context,
|
||||||
|
};
|
||||||
|
|
||||||
|
entries.push(newEntry);
|
||||||
|
await writeConversationLog(entries);
|
||||||
|
|
||||||
|
return newEntry;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 更新對話日誌條目
|
||||||
|
export async function updateConversationEntry(
|
||||||
|
entryId: string,
|
||||||
|
updates: Partial<ConversationEntry>
|
||||||
|
): Promise<ConversationEntry | null> {
|
||||||
|
const entries = await readConversationLog();
|
||||||
|
const entryIndex = entries.findIndex((entry) => entry.id === entryId);
|
||||||
|
|
||||||
|
if (entryIndex === -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
entries[entryIndex] = {
|
||||||
|
...entries[entryIndex],
|
||||||
|
...updates,
|
||||||
|
};
|
||||||
|
|
||||||
|
await writeConversationLog(entries);
|
||||||
|
|
||||||
|
return entries[entryIndex];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取特定任務的對話日誌條目
|
||||||
|
export async function getConversationEntriesByTaskId(
|
||||||
|
taskId: string
|
||||||
|
): Promise<ConversationEntry[]> {
|
||||||
|
const entries = await readConversationLog();
|
||||||
|
return entries.filter((entry) => entry.relatedTaskId === taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 刪除對話日誌條目
|
||||||
|
export async function deleteConversationEntry(
|
||||||
|
entryId: string
|
||||||
|
): Promise<{ success: boolean; message: string }> {
|
||||||
|
const entries = await readConversationLog();
|
||||||
|
const initialLength = entries.length;
|
||||||
|
|
||||||
|
const filteredEntries = entries.filter((entry) => entry.id !== entryId);
|
||||||
|
|
||||||
|
if (filteredEntries.length === initialLength) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
message: `找不到 ID 為 ${entryId} 的對話日誌條目`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
await writeConversationLog(filteredEntries);
|
||||||
|
return { success: true, message: "對話日誌條目已成功刪除" };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根據時間範圍獲取對話日誌條目
|
||||||
|
export async function getConversationEntriesByDateRange(
|
||||||
|
startDate: Date,
|
||||||
|
endDate: Date
|
||||||
|
): Promise<ConversationEntry[]> {
|
||||||
|
const entries = await readConversationLog();
|
||||||
|
|
||||||
|
return entries.filter((entry) => {
|
||||||
|
const entryTime = entry.timestamp.getTime();
|
||||||
|
return entryTime >= startDate.getTime() && entryTime <= endDate.getTime();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有對話日誌
|
||||||
|
export async function clearAllConversationEntries(): Promise<void> {
|
||||||
|
await writeConversationLog([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取分頁的對話日誌條目
|
||||||
|
export async function getPaginatedConversationEntries(
|
||||||
|
limit: number = 10,
|
||||||
|
offset: number = 0,
|
||||||
|
taskId?: string,
|
||||||
|
startDate?: Date,
|
||||||
|
endDate?: Date
|
||||||
|
): Promise<{ entries: ConversationEntry[]; total: number }> {
|
||||||
|
let entries = await readConversationLog();
|
||||||
|
|
||||||
|
// 根據任務 ID 過濾
|
||||||
|
if (taskId) {
|
||||||
|
entries = entries.filter((entry) => entry.relatedTaskId === taskId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根據日期範圍過濾
|
||||||
|
if (startDate && endDate) {
|
||||||
|
entries = entries.filter((entry) => {
|
||||||
|
const entryTime = entry.timestamp.getTime();
|
||||||
|
return entryTime >= startDate.getTime() && entryTime <= endDate.getTime();
|
||||||
|
});
|
||||||
|
} else if (startDate) {
|
||||||
|
entries = entries.filter(
|
||||||
|
(entry) => entry.timestamp.getTime() >= startDate.getTime()
|
||||||
|
);
|
||||||
|
} else if (endDate) {
|
||||||
|
entries = entries.filter(
|
||||||
|
(entry) => entry.timestamp.getTime() <= endDate.getTime()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按時間排序(降序,最新的在前)
|
||||||
|
entries.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
||||||
|
|
||||||
|
const total = entries.length;
|
||||||
|
const paginatedEntries = entries.slice(offset, offset + limit);
|
||||||
|
|
||||||
|
return { entries: paginatedEntries, total };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 日誌文件輪換功能 - 將當前日誌歸檔並創建新的空日誌
|
||||||
|
*/
|
||||||
|
async function rotateLogFile(): Promise<void> {
|
||||||
|
console.log("執行日誌輪換操作...");
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 確保目錄存在
|
||||||
|
await ensureDataDir();
|
||||||
|
|
||||||
|
// 檢查當前日誌文件是否存在
|
||||||
|
try {
|
||||||
|
await fs.access(CONVERSATION_LOG_FILE);
|
||||||
|
} catch (error) {
|
||||||
|
// 如果不存在,創建空文件並返回
|
||||||
|
await fs.writeFile(
|
||||||
|
CONVERSATION_LOG_FILE,
|
||||||
|
JSON.stringify({ entries: [] })
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成歸檔文件名 (conversation_log_時間戳.json)
|
||||||
|
const timestamp = new Date()
|
||||||
|
.toISOString()
|
||||||
|
.replace(/:/g, "-")
|
||||||
|
.replace(/\..+/, "");
|
||||||
|
const archiveFileName = `conversation_log_${timestamp}.json`;
|
||||||
|
const archiveFilePath = path.join(DATA_DIR, archiveFileName);
|
||||||
|
|
||||||
|
// 將當前日誌複製到歸檔文件
|
||||||
|
await fs.copyFile(CONVERSATION_LOG_FILE, archiveFilePath);
|
||||||
|
|
||||||
|
// 創建新的空日誌文件
|
||||||
|
await fs.writeFile(CONVERSATION_LOG_FILE, JSON.stringify({ entries: [] }));
|
||||||
|
|
||||||
|
// 清理過多的歸檔文件
|
||||||
|
await cleanupArchivedLogs();
|
||||||
|
|
||||||
|
console.log(`日誌輪換完成,歸檔文件: ${archiveFileName}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("日誌輪換過程中發生錯誤:", error);
|
||||||
|
// 即使輪換失敗,也要確保主日誌文件存在
|
||||||
|
try {
|
||||||
|
await fs.writeFile(
|
||||||
|
CONVERSATION_LOG_FILE,
|
||||||
|
JSON.stringify({ entries: [] })
|
||||||
|
);
|
||||||
|
} catch (innerError) {
|
||||||
|
console.error("創建新日誌文件失敗:", innerError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清理過舊的歸檔日誌文件
|
||||||
|
*/
|
||||||
|
async function cleanupArchivedLogs(): Promise<void> {
|
||||||
|
try {
|
||||||
|
// 讀取數據目錄中的所有文件
|
||||||
|
const files = await fs.readdir(DATA_DIR);
|
||||||
|
|
||||||
|
// 過濾出歸檔日誌文件
|
||||||
|
const archivedLogs = files
|
||||||
|
.filter(
|
||||||
|
(file) => file.startsWith("conversation_log_") && file.endsWith(".json")
|
||||||
|
)
|
||||||
|
.map((file) => ({
|
||||||
|
name: file,
|
||||||
|
path: path.join(DATA_DIR, file),
|
||||||
|
// 從文件名中提取時間戳
|
||||||
|
timestamp: file.replace("conversation_log_", "").replace(".json", ""),
|
||||||
|
}))
|
||||||
|
// 按時間戳降序排序(最新的在前)
|
||||||
|
.sort((a, b) => b.timestamp.localeCompare(a.timestamp));
|
||||||
|
|
||||||
|
// 如果歸檔日誌文件數量超過最大保留數量,刪除最舊的
|
||||||
|
if (archivedLogs.length > MAX_ARCHIVED_LOGS) {
|
||||||
|
const logsToDelete = archivedLogs.slice(MAX_ARCHIVED_LOGS);
|
||||||
|
for (const log of logsToDelete) {
|
||||||
|
try {
|
||||||
|
await fs.unlink(log.path);
|
||||||
|
console.log(`已刪除過舊的日誌歸檔: ${log.name}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`刪除日誌歸檔 ${log.name} 失敗:`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("清理歸檔日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 對日誌條目進行精簡,移除不重要的舊條目
|
||||||
|
*/
|
||||||
|
async function trimLogEntries(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const entries = await readConversationLog();
|
||||||
|
|
||||||
|
// 如果條目數量未達到精簡閾值,不進行處理
|
||||||
|
if (entries.length < LOG_ENTRY_TRIM_THRESHOLD) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`日誌條目數量(${entries.length})超過精簡閾值(${LOG_ENTRY_TRIM_THRESHOLD}),進行精簡處理...`
|
||||||
|
);
|
||||||
|
|
||||||
|
// 策略:保留最新的75%條目,優先移除一般日誌,保留錯誤和重要操作
|
||||||
|
const entriesToKeep = Math.floor(entries.length * 0.75);
|
||||||
|
|
||||||
|
// 先按重要性對條目進行分類
|
||||||
|
const errorEntries = entries.filter(
|
||||||
|
(entry) =>
|
||||||
|
entry.context?.includes("錯誤") ||
|
||||||
|
entry.context?.includes("失敗") ||
|
||||||
|
entry.summary.includes("錯誤") ||
|
||||||
|
entry.summary.includes("失敗")
|
||||||
|
);
|
||||||
|
|
||||||
|
const taskEntries = entries.filter(
|
||||||
|
(entry) =>
|
||||||
|
entry.relatedTaskId && !errorEntries.some((e) => e.id === entry.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
const generalEntries = entries.filter(
|
||||||
|
(entry) =>
|
||||||
|
!errorEntries.some((e) => e.id === entry.id) &&
|
||||||
|
!taskEntries.some((e) => e.id === entry.id)
|
||||||
|
);
|
||||||
|
|
||||||
|
// 確定每類保留多少條目
|
||||||
|
const totalToRemove = entries.length - entriesToKeep;
|
||||||
|
|
||||||
|
// 優先從一般日誌中移除
|
||||||
|
let trimmedGeneralEntries = generalEntries;
|
||||||
|
if (generalEntries.length > totalToRemove) {
|
||||||
|
// 按時間排序,移除最舊的
|
||||||
|
trimmedGeneralEntries = generalEntries
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||||
|
)
|
||||||
|
.slice(0, generalEntries.length - totalToRemove);
|
||||||
|
} else {
|
||||||
|
// 如果一般日誌不夠,需要從任務日誌中移除
|
||||||
|
trimmedGeneralEntries = [];
|
||||||
|
const remainingToRemove = totalToRemove - generalEntries.length;
|
||||||
|
|
||||||
|
if (remainingToRemove > 0 && taskEntries.length > remainingToRemove) {
|
||||||
|
const trimmedTaskEntries = taskEntries
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.timestamp).getTime() - new Date(a.timestamp).getTime()
|
||||||
|
)
|
||||||
|
.slice(0, taskEntries.length - remainingToRemove);
|
||||||
|
|
||||||
|
// 合併保留的條目
|
||||||
|
const newEntries = [
|
||||||
|
...errorEntries,
|
||||||
|
...trimmedTaskEntries,
|
||||||
|
...trimmedGeneralEntries,
|
||||||
|
];
|
||||||
|
await writeConversationLog(newEntries);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`日誌精簡完成: 從 ${entries.length} 條減少到 ${newEntries.length} 條`
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 合併保留的條目
|
||||||
|
const newEntries = [
|
||||||
|
...errorEntries,
|
||||||
|
...taskEntries,
|
||||||
|
...trimmedGeneralEntries,
|
||||||
|
];
|
||||||
|
await writeConversationLog(newEntries);
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`日誌精簡完成: 從 ${entries.length} 條減少到 ${newEntries.length} 條`
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("精簡日誌條目時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 獲取所有歸檔日誌文件列表
|
||||||
|
*/
|
||||||
|
export async function getArchivedLogFiles(): Promise<string[]> {
|
||||||
|
try {
|
||||||
|
// 確保目錄存在
|
||||||
|
await ensureDataDir();
|
||||||
|
|
||||||
|
const files = await fs.readdir(DATA_DIR);
|
||||||
|
|
||||||
|
// 過濾出歸檔日誌文件
|
||||||
|
return files
|
||||||
|
.filter(
|
||||||
|
(file) => file.startsWith("conversation_log_") && file.endsWith(".json")
|
||||||
|
)
|
||||||
|
.sort()
|
||||||
|
.reverse(); // 最新的在前
|
||||||
|
} catch (error) {
|
||||||
|
console.error("獲取歸檔日誌文件列表時發生錯誤:", error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 讀取特定歸檔日誌文件
|
||||||
|
*/
|
||||||
|
export async function readArchivedLog(
|
||||||
|
archiveFileName: string
|
||||||
|
): Promise<ConversationEntry[]> {
|
||||||
|
// 安全性檢查:確保文件名格式正確
|
||||||
|
if (!archiveFileName.match(/^conversation_log_[\d-]+T[\d-]+\.json$/)) {
|
||||||
|
throw new Error("無效的歸檔日誌文件名");
|
||||||
|
}
|
||||||
|
|
||||||
|
const archiveFilePath = path.join(DATA_DIR, archiveFileName);
|
||||||
|
|
||||||
|
try {
|
||||||
|
await fs.access(archiveFilePath);
|
||||||
|
const data = await fs.readFile(archiveFilePath, "utf-8");
|
||||||
|
const entries = JSON.parse(data).entries;
|
||||||
|
|
||||||
|
// 將日期字串轉換回 Date 物件
|
||||||
|
return entries.map((entry: any) => ({
|
||||||
|
...entry,
|
||||||
|
timestamp: entry.timestamp ? new Date(entry.timestamp) : new Date(),
|
||||||
|
}));
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`讀取歸檔日誌 ${archiveFileName} 時發生錯誤:`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
434
src/tools/logTools.ts
Normal file
434
src/tools/logTools.ts
Normal file
@ -0,0 +1,434 @@
|
|||||||
|
import { z } from "zod";
|
||||||
|
import {
|
||||||
|
getAllConversationEntries,
|
||||||
|
getConversationEntriesByTaskId,
|
||||||
|
getPaginatedConversationEntries,
|
||||||
|
clearAllConversationEntries,
|
||||||
|
getArchivedLogFiles,
|
||||||
|
readArchivedLog,
|
||||||
|
} from "../models/conversationLogModel.js";
|
||||||
|
import { getTaskById } from "../models/taskModel.js";
|
||||||
|
import { ListConversationLogArgs } from "../types/index.js";
|
||||||
|
|
||||||
|
// 列出對話日誌工具
|
||||||
|
export const listConversationLogSchema = z.object({
|
||||||
|
taskId: z.string().optional().describe("按任務 ID 過濾對話記錄(選填)"),
|
||||||
|
startDate: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("起始日期過濾,格式為 ISO 日期字串(選填)"),
|
||||||
|
endDate: z
|
||||||
|
.string()
|
||||||
|
.optional()
|
||||||
|
.describe("結束日期過濾,格式為 ISO 日期字串(選填)"),
|
||||||
|
limit: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.max(100)
|
||||||
|
.default(20)
|
||||||
|
.describe("返回結果數量限制,最大 100(預設:20)"),
|
||||||
|
offset: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.nonnegative()
|
||||||
|
.default(0)
|
||||||
|
.describe("分頁偏移量(預設:0)"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function listConversationLog({
|
||||||
|
taskId,
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
}: z.infer<typeof listConversationLogSchema>) {
|
||||||
|
// 將日期字串轉換為 Date 物件
|
||||||
|
const startDateObj = startDate ? new Date(startDate) : undefined;
|
||||||
|
const endDateObj = endDate ? new Date(endDate) : undefined;
|
||||||
|
|
||||||
|
// 驗證日期格式
|
||||||
|
if (startDate && isNaN(startDateObj?.getTime() ?? NaN)) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `## 參數錯誤\n\n起始日期格式無效。請使用 ISO 日期字串格式(例如:2023-01-01T00:00:00Z)。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endDate && isNaN(endDateObj?.getTime() ?? NaN)) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `## 參數錯誤\n\n結束日期格式無效。請使用 ISO 日期字串格式(例如:2023-01-01T00:00:00Z)。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果指定了任務 ID,檢查任務是否存在
|
||||||
|
let taskInfo = "";
|
||||||
|
if (taskId) {
|
||||||
|
const task = await getTaskById(taskId);
|
||||||
|
if (!task) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `## 參數錯誤\n\n找不到 ID 為 \`${taskId}\` 的任務。請使用「list_tasks」工具確認有效的任務 ID 後再試。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
taskInfo = `任務:${task.name} (ID: \`${task.id}\`)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 獲取分頁的對話日誌
|
||||||
|
const { entries, total } = await getPaginatedConversationEntries(
|
||||||
|
limit,
|
||||||
|
offset,
|
||||||
|
taskId,
|
||||||
|
startDateObj,
|
||||||
|
endDateObj
|
||||||
|
);
|
||||||
|
|
||||||
|
if (entries.length === 0) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `## 查詢結果\n\n未找到符合條件的對話日誌記錄。${
|
||||||
|
taskId ? `\n\n${taskInfo}` : ""
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 構建過濾條件描述
|
||||||
|
const filterDescs = [];
|
||||||
|
if (taskInfo) filterDescs.push(taskInfo);
|
||||||
|
if (startDateObj) filterDescs.push(`開始日期:${startDateObj.toISOString()}`);
|
||||||
|
if (endDateObj) filterDescs.push(`結束日期:${endDateObj.toISOString()}`);
|
||||||
|
const filterDesc =
|
||||||
|
filterDescs.length > 0 ? `\n\n過濾條件:${filterDescs.join(",")}` : "";
|
||||||
|
|
||||||
|
// 構建分頁信息
|
||||||
|
const pageInfo = `\n\n當前顯示:第 ${offset + 1} 到 ${Math.min(
|
||||||
|
offset + limit,
|
||||||
|
total
|
||||||
|
)} 條,共 ${total} 條記錄`;
|
||||||
|
|
||||||
|
// 構建分頁導航提示
|
||||||
|
let navTips = "";
|
||||||
|
if (total > limit) {
|
||||||
|
const prevPageAvailable = offset > 0;
|
||||||
|
const nextPageAvailable = offset + limit < total;
|
||||||
|
|
||||||
|
navTips = "\n\n分頁導航:";
|
||||||
|
if (prevPageAvailable) {
|
||||||
|
const prevOffset = Math.max(0, offset - limit);
|
||||||
|
navTips += `\n- 上一頁:使用 offset=${prevOffset}`;
|
||||||
|
}
|
||||||
|
if (nextPageAvailable) {
|
||||||
|
const nextOffset = offset + limit;
|
||||||
|
navTips += `\n- 下一頁:使用 offset=${nextOffset}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 格式化對話日誌列表
|
||||||
|
const formattedEntries = entries
|
||||||
|
.map((entry, index) => {
|
||||||
|
const entryNumber = offset + index + 1;
|
||||||
|
return `### ${entryNumber}. ${entry.participant} (${new Date(
|
||||||
|
entry.timestamp
|
||||||
|
).toISOString()})\n${
|
||||||
|
entry.relatedTaskId ? `關聯任務:\`${entry.relatedTaskId}\`\n` : ""
|
||||||
|
}${entry.context ? `上下文:${entry.context}\n` : ""}摘要:${
|
||||||
|
entry.summary
|
||||||
|
}`;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
const result = `## 對話日誌查詢結果${filterDesc}${pageInfo}\n\n${formattedEntries}${navTips}`;
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: result,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清除所有對話日誌工具
|
||||||
|
export const clearConversationLogSchema = z.object({
|
||||||
|
confirm: z.boolean().describe("確認刪除所有日誌記錄(此操作不可逆)"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function clearConversationLog({
|
||||||
|
confirm,
|
||||||
|
}: z.infer<typeof clearConversationLogSchema>) {
|
||||||
|
if (!confirm) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `## 操作取消\n\n未確認清除操作。如要清除所有對話日誌,請將 confirm 參數設為 true。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 執行清除操作
|
||||||
|
await clearAllConversationEntries();
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `## 操作成功\n\n所有對話日誌已成功清除。`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 列出歸檔日誌工具
|
||||||
|
export const listArchivedLogsSchema = z.object({
|
||||||
|
includeDetails: z
|
||||||
|
.boolean()
|
||||||
|
.default(false)
|
||||||
|
.describe("是否包含歸檔文件的詳細信息(預設:否)"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function listArchivedLogs({
|
||||||
|
includeDetails = false,
|
||||||
|
}: z.infer<typeof listArchivedLogsSchema>) {
|
||||||
|
const archiveFiles = await getArchivedLogFiles();
|
||||||
|
|
||||||
|
let content = "";
|
||||||
|
|
||||||
|
if (archiveFiles.length === 0) {
|
||||||
|
content = "## 日誌歸檔\n\n目前沒有任何日誌歸檔文件。";
|
||||||
|
} else {
|
||||||
|
content = "## 日誌歸檔文件\n\n";
|
||||||
|
|
||||||
|
if (includeDetails) {
|
||||||
|
// 為每個歸檔文件獲取更多詳細信息
|
||||||
|
const detailedFiles = await Promise.all(
|
||||||
|
archiveFiles.map(async (file) => {
|
||||||
|
try {
|
||||||
|
const entries = await readArchivedLog(file);
|
||||||
|
const timestamp = file
|
||||||
|
.replace("conversation_log_", "")
|
||||||
|
.replace(".json", "");
|
||||||
|
const formattedDate = new Date(
|
||||||
|
timestamp.replace(/-/g, ":").replace("T", " ")
|
||||||
|
).toLocaleString();
|
||||||
|
|
||||||
|
return {
|
||||||
|
filename: file,
|
||||||
|
date: formattedDate,
|
||||||
|
entriesCount: entries.length,
|
||||||
|
firstEntry: entries.length > 0 ? entries[0] : null,
|
||||||
|
lastEntry:
|
||||||
|
entries.length > 0 ? entries[entries.length - 1] : null,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
filename: file,
|
||||||
|
date: "未知",
|
||||||
|
entriesCount: 0,
|
||||||
|
error: (error as Error).message,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
// 格式化輸出
|
||||||
|
content += detailedFiles
|
||||||
|
.map((file, index) => {
|
||||||
|
let fileInfo = `${index + 1}. **${file.filename}**\n`;
|
||||||
|
fileInfo += ` - 創建日期: ${file.date}\n`;
|
||||||
|
fileInfo += ` - 條目數量: ${file.entriesCount}\n`;
|
||||||
|
|
||||||
|
if (file.firstEntry) {
|
||||||
|
const firstDate = new Date(
|
||||||
|
file.firstEntry.timestamp
|
||||||
|
).toLocaleString();
|
||||||
|
fileInfo += ` - 最早條目: ${firstDate}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.lastEntry) {
|
||||||
|
const lastDate = new Date(
|
||||||
|
file.lastEntry.timestamp
|
||||||
|
).toLocaleString();
|
||||||
|
fileInfo += ` - 最晚條目: ${lastDate}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.error) {
|
||||||
|
fileInfo += ` - 錯誤: ${file.error}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileInfo;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
} else {
|
||||||
|
// 簡單列出歸檔文件名
|
||||||
|
content += archiveFiles
|
||||||
|
.map((file, index) => {
|
||||||
|
// 從文件名提取日期
|
||||||
|
const timestamp = file
|
||||||
|
.replace("conversation_log_", "")
|
||||||
|
.replace(".json", "");
|
||||||
|
const formattedDate = new Date(
|
||||||
|
timestamp.replace(/-/g, ":").replace("T", " ")
|
||||||
|
).toLocaleString();
|
||||||
|
return `${index + 1}. **${file}** (${formattedDate})`;
|
||||||
|
})
|
||||||
|
.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
content += "\n\n使用讀取歸檔日誌工具可查看特定歸檔文件的內容。";
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 讀取特定歸檔日誌工具
|
||||||
|
export const readArchivedLogSchema = z.object({
|
||||||
|
filename: z
|
||||||
|
.string()
|
||||||
|
.describe("歸檔日誌文件名,格式為 'conversation_log_[timestamp].json'"),
|
||||||
|
limit: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.positive()
|
||||||
|
.max(100)
|
||||||
|
.default(50)
|
||||||
|
.describe("返回結果數量限制,最大 100(預設:50)"),
|
||||||
|
offset: z
|
||||||
|
.number()
|
||||||
|
.int()
|
||||||
|
.nonnegative()
|
||||||
|
.default(0)
|
||||||
|
.describe("分頁起始位置(預設:0)"),
|
||||||
|
});
|
||||||
|
|
||||||
|
export async function readArchivedLogTool({
|
||||||
|
filename,
|
||||||
|
limit = 50,
|
||||||
|
offset = 0,
|
||||||
|
}: z.infer<typeof readArchivedLogSchema>) {
|
||||||
|
try {
|
||||||
|
// 安全性檢查:確保文件名格式正確
|
||||||
|
if (!filename.match(/^conversation_log_[\d-]+T[\d-]+\.json$/)) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: "## 錯誤\n\n無效的歸檔日誌文件名。正確格式為 'conversation_log_[timestamp].json'。",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = await readArchivedLog(filename);
|
||||||
|
|
||||||
|
// 分頁處理
|
||||||
|
const paginatedEntries = entries.slice(offset, offset + limit);
|
||||||
|
const total = entries.length;
|
||||||
|
|
||||||
|
let content = `## 歸檔日誌: ${filename}\n\n`;
|
||||||
|
|
||||||
|
if (paginatedEntries.length === 0) {
|
||||||
|
content += "此歸檔文件沒有任何日誌條目。";
|
||||||
|
} else {
|
||||||
|
// 添加分頁信息
|
||||||
|
content += `顯示 ${Math.min(total, offset + 1)}-${Math.min(
|
||||||
|
total,
|
||||||
|
offset + limit
|
||||||
|
)} 條,共 ${total} 條\n\n`;
|
||||||
|
|
||||||
|
// 格式化日誌條目
|
||||||
|
content += paginatedEntries
|
||||||
|
.map((entry, index) => {
|
||||||
|
const date = new Date(entry.timestamp).toLocaleString();
|
||||||
|
let entryContent = `### ${offset + index + 1}. ${date} (${
|
||||||
|
entry.participant
|
||||||
|
})\n`;
|
||||||
|
|
||||||
|
if (entry.context) {
|
||||||
|
entryContent += `**上下文:** ${entry.context}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.relatedTaskId) {
|
||||||
|
entryContent += `**相關任務:** ${entry.relatedTaskId}\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
entryContent += `\n${entry.summary}\n`;
|
||||||
|
|
||||||
|
return entryContent;
|
||||||
|
})
|
||||||
|
.join("\n\n");
|
||||||
|
|
||||||
|
// 添加導航鏈接
|
||||||
|
if (offset > 0 || offset + limit < total) {
|
||||||
|
content += "\n\n### 分頁導航\n";
|
||||||
|
|
||||||
|
if (offset > 0) {
|
||||||
|
const prevOffset = Math.max(0, offset - limit);
|
||||||
|
content += `- 上一頁 (${prevOffset + 1}-${Math.min(
|
||||||
|
total,
|
||||||
|
prevOffset + limit
|
||||||
|
)})\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset + limit < total) {
|
||||||
|
const nextOffset = offset + limit;
|
||||||
|
content += `- 下一頁 (${nextOffset + 1}-${Math.min(
|
||||||
|
total,
|
||||||
|
nextOffset + limit
|
||||||
|
)})\n`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: content,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
content: [
|
||||||
|
{
|
||||||
|
type: "text" as const,
|
||||||
|
text: `## 錯誤\n\n讀取歸檔日誌時發生錯誤: ${
|
||||||
|
(error as Error).message
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
isError: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
@ -7,7 +7,9 @@ import {
|
|||||||
batchCreateOrUpdateTasks,
|
batchCreateOrUpdateTasks,
|
||||||
deleteTask as modelDeleteTask,
|
deleteTask as modelDeleteTask,
|
||||||
} from "../models/taskModel.js";
|
} from "../models/taskModel.js";
|
||||||
import { TaskStatus } from "../types/index.js";
|
import { TaskStatus, ConversationParticipant } from "../types/index.js";
|
||||||
|
import { addConversationEntry } from "../models/conversationLogModel.js";
|
||||||
|
import { extractSummary } from "../utils/summaryExtractor.js";
|
||||||
|
|
||||||
// 開始規劃工具
|
// 開始規劃工具
|
||||||
export const planTaskSchema = z.object({
|
export const planTaskSchema = z.object({
|
||||||
@ -24,6 +26,20 @@ export async function planTask({
|
|||||||
description,
|
description,
|
||||||
requirements,
|
requirements,
|
||||||
}: z.infer<typeof planTaskSchema>) {
|
}: z.infer<typeof planTaskSchema>) {
|
||||||
|
// 記錄任務規劃開始
|
||||||
|
try {
|
||||||
|
// 使用摘要提取工具處理較長的描述
|
||||||
|
const descriptionSummary = extractSummary(description, 100);
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`開始新任務規劃,描述:${descriptionSummary}`,
|
||||||
|
undefined,
|
||||||
|
"任務規劃"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
let prompt = `## 任務分析請求\n\n請仔細分析以下任務問題,理解其核心要求、範圍和約束條件:\n\n\`\`\`\n${description}\n\`\`\`\n\n`;
|
let prompt = `## 任務分析請求\n\n請仔細分析以下任務問題,理解其核心要求、範圍和約束條件:\n\n\`\`\`\n${description}\n\`\`\`\n\n`;
|
||||||
|
|
||||||
if (requirements) {
|
if (requirements) {
|
||||||
@ -68,6 +84,23 @@ export async function analyzeTask({
|
|||||||
initialConcept,
|
initialConcept,
|
||||||
previousAnalysis,
|
previousAnalysis,
|
||||||
}: z.infer<typeof analyzeTaskSchema>) {
|
}: z.infer<typeof analyzeTaskSchema>) {
|
||||||
|
// 記錄任務分析
|
||||||
|
try {
|
||||||
|
// 使用摘要提取工具處理較長的概念描述
|
||||||
|
const conceptSummary = extractSummary(initialConcept, 100);
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`開始分析任務:${extractSummary(
|
||||||
|
summary,
|
||||||
|
100
|
||||||
|
)},初步概念:${conceptSummary}`,
|
||||||
|
undefined,
|
||||||
|
"任務分析"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
let prompt = `## 代碼庫分析任務\n\n### 任務摘要\n\`\`\`\n${summary}\n\`\`\`\n\n已收到您的初步解答構想:\n\n\`\`\`\n${initialConcept}\n\`\`\`\n\n`;
|
let prompt = `## 代碼庫分析任務\n\n### 任務摘要\n\`\`\`\n${summary}\n\`\`\`\n\n已收到您的初步解答構想:\n\n\`\`\`\n${initialConcept}\n\`\`\`\n\n`;
|
||||||
|
|
||||||
prompt += `## 技術審核指引\n\n請執行以下分析步驟:\n\n1. 檢查現有程式碼庫中的相似實現或可重用組件
|
prompt += `## 技術審核指引\n\n請執行以下分析步驟:\n\n1. 檢查現有程式碼庫中的相似實現或可重用組件
|
||||||
@ -107,6 +140,23 @@ export async function reflectTask({
|
|||||||
summary,
|
summary,
|
||||||
analysis,
|
analysis,
|
||||||
}: z.infer<typeof reflectTaskSchema>) {
|
}: z.infer<typeof reflectTaskSchema>) {
|
||||||
|
// 記錄任務反思
|
||||||
|
try {
|
||||||
|
// 使用摘要提取工具處理較長的分析
|
||||||
|
const analysisSummary = extractSummary(analysis, 100);
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`開始反思任務解決方案:${extractSummary(
|
||||||
|
summary,
|
||||||
|
50
|
||||||
|
)},分析:${analysisSummary}`,
|
||||||
|
undefined,
|
||||||
|
"任務反思"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const prompt = `## 解決方案反思與評估\n\n### 任務摘要\n\`\`\`\n${summary}\n\`\`\`\n\n### 詳細分析結果\n\`\`\`\n${analysis}\n\`\`\`\n\n## 批判性評估指引\n\n請從以下多個維度對您的解決方案進行全面且批判性的審查:\n\n### 1. 技術完整性評估\n- 方案是否存在技術缺陷或邏輯漏洞?
|
const prompt = `## 解決方案反思與評估\n\n### 任務摘要\n\`\`\`\n${summary}\n\`\`\`\n\n### 詳細分析結果\n\`\`\`\n${analysis}\n\`\`\`\n\n## 批判性評估指引\n\n請從以下多個維度對您的解決方案進行全面且批判性的審查:\n\n### 1. 技術完整性評估\n- 方案是否存在技術缺陷或邏輯漏洞?
|
||||||
- 是否考慮了邊緣情況和異常處理?
|
- 是否考慮了邊緣情況和異常處理?
|
||||||
- 所有數據流和控制流是否清晰定義?
|
- 所有數據流和控制流是否清晰定義?
|
||||||
@ -166,9 +216,36 @@ export async function splitTasks({
|
|||||||
isOverwrite,
|
isOverwrite,
|
||||||
tasks,
|
tasks,
|
||||||
}: z.infer<typeof splitTasksSchema>) {
|
}: z.infer<typeof splitTasksSchema>) {
|
||||||
|
// 記錄任務拆分
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`拆分任務:${isOverwrite ? "覆蓋模式" : "新增模式"},任務數量:${
|
||||||
|
tasks.length
|
||||||
|
}`,
|
||||||
|
undefined,
|
||||||
|
"任務拆分"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
// 批量創建任務
|
// 批量創建任務
|
||||||
const createdTasks = await batchCreateOrUpdateTasks(tasks, isOverwrite);
|
const createdTasks = await batchCreateOrUpdateTasks(tasks, isOverwrite);
|
||||||
|
|
||||||
|
// 記錄任務創建成功
|
||||||
|
try {
|
||||||
|
const taskNames = createdTasks.map((task) => task.name).join(", ");
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`成功創建任務:${taskNames}`,
|
||||||
|
undefined,
|
||||||
|
"任務創建"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
// 獲取所有任務,用於顯示完整的依賴關係
|
// 獲取所有任務,用於顯示完整的依賴關係
|
||||||
const allTasks = await getAllTasks();
|
const allTasks = await getAllTasks();
|
||||||
|
|
||||||
@ -229,6 +306,18 @@ export async function splitTasks({
|
|||||||
|
|
||||||
// 列出任務工具
|
// 列出任務工具
|
||||||
export async function listTasks() {
|
export async function listTasks() {
|
||||||
|
// 記錄查詢任務列表
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
"查詢所有任務列表",
|
||||||
|
undefined,
|
||||||
|
"任務列表查詢"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const tasks = await getAllTasks();
|
const tasks = await getAllTasks();
|
||||||
|
|
||||||
if (tasks.length === 0) {
|
if (tasks.length === 0) {
|
||||||
@ -316,6 +405,18 @@ export async function executeTask({
|
|||||||
const task = await getTaskById(taskId);
|
const task = await getTaskById(taskId);
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
|
// 記錄錯誤日誌
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`執行任務失敗:找不到ID為 ${taskId} 的任務`,
|
||||||
|
undefined,
|
||||||
|
"錯誤"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -328,6 +429,18 @@ export async function executeTask({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (task.status === TaskStatus.COMPLETED) {
|
if (task.status === TaskStatus.COMPLETED) {
|
||||||
|
// 記錄已完成任務的嘗試執行
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`嘗試執行已完成的任務:${task.name} (ID: ${task.id})`,
|
||||||
|
task.id,
|
||||||
|
"狀態通知"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -352,6 +465,18 @@ export async function executeTask({
|
|||||||
: `ID: \`${id}\``;
|
: `ID: \`${id}\``;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 記錄任務被阻擋的情況
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`任務 ${task.name} (ID: ${task.id}) 被依賴阻擋,等待完成的依賴任務數量: ${blockedBy.length}`,
|
||||||
|
task.id,
|
||||||
|
"依賴阻擋"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -370,6 +495,18 @@ export async function executeTask({
|
|||||||
// 更新任務狀態為進行中
|
// 更新任務狀態為進行中
|
||||||
await updateTaskStatus(taskId, TaskStatus.IN_PROGRESS);
|
await updateTaskStatus(taskId, TaskStatus.IN_PROGRESS);
|
||||||
|
|
||||||
|
// 記錄任務開始執行
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`開始執行任務:${task.name} (ID: ${task.id})`,
|
||||||
|
task.id,
|
||||||
|
"任務啟動"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const prompt = `## 任務執行指示\n\n### 任務詳情\n\n- **名稱:** ${
|
const prompt = `## 任務執行指示\n\n### 任務詳情\n\n- **名稱:** ${
|
||||||
task.name
|
task.name
|
||||||
}\n- **ID:** \`${task.id}\`\n- **描述:** ${task.description}\n${
|
}\n- **ID:** \`${task.id}\`\n- **描述:** ${task.description}\n${
|
||||||
@ -408,6 +545,18 @@ export async function verifyTask({ taskId }: z.infer<typeof verifyTaskSchema>) {
|
|||||||
const task = await getTaskById(taskId);
|
const task = await getTaskById(taskId);
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
|
// 記錄錯誤日誌
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`驗證任務失敗:找不到ID為 ${taskId} 的任務`,
|
||||||
|
undefined,
|
||||||
|
"錯誤"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -420,6 +569,18 @@ export async function verifyTask({ taskId }: z.infer<typeof verifyTaskSchema>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (task.status !== TaskStatus.IN_PROGRESS) {
|
if (task.status !== TaskStatus.IN_PROGRESS) {
|
||||||
|
// 記錄狀態錯誤
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`驗證任務狀態錯誤:任務 ${task.name} (ID: ${task.id}) 當前狀態為 ${task.status},不處於進行中狀態`,
|
||||||
|
task.id,
|
||||||
|
"狀態錯誤"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -431,6 +592,18 @@ export async function verifyTask({ taskId }: z.infer<typeof verifyTaskSchema>) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 記錄開始驗證
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`開始驗證任務:${task.name} (ID: ${task.id})`,
|
||||||
|
task.id,
|
||||||
|
"任務驗證"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const prompt = `## 任務驗證評估\n\n### 任務資料\n\n- **名稱:** ${
|
const prompt = `## 任務驗證評估\n\n### 任務資料\n\n- **名稱:** ${
|
||||||
task.name
|
task.name
|
||||||
}\n- **ID:** \`${task.id}\`\n- **描述:** ${task.description}\n${
|
}\n- **ID:** \`${task.id}\`\n- **描述:** ${task.description}\n${
|
||||||
@ -476,6 +649,18 @@ export async function completeTask({
|
|||||||
const task = await getTaskById(taskId);
|
const task = await getTaskById(taskId);
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
|
// 記錄錯誤日誌
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`完成任務失敗:找不到ID為 ${taskId} 的任務`,
|
||||||
|
undefined,
|
||||||
|
"錯誤"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -488,6 +673,18 @@ export async function completeTask({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (task.status !== TaskStatus.IN_PROGRESS) {
|
if (task.status !== TaskStatus.IN_PROGRESS) {
|
||||||
|
// 記錄狀態錯誤
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`完成任務狀態錯誤:任務 ${task.name} (ID: ${task.id}) 當前狀態為 ${task.status},不是進行中狀態`,
|
||||||
|
task.id,
|
||||||
|
"狀態錯誤"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -502,6 +699,18 @@ export async function completeTask({
|
|||||||
// 更新任務狀態為已完成
|
// 更新任務狀態為已完成
|
||||||
await updateTaskStatus(taskId, TaskStatus.COMPLETED);
|
await updateTaskStatus(taskId, TaskStatus.COMPLETED);
|
||||||
|
|
||||||
|
// 記錄任務完成
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`任務成功完成:${task.name} (ID: ${task.id})`,
|
||||||
|
task.id,
|
||||||
|
"任務完成"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const prompt = `## 任務完成確認\n\n任務 "${task.name}" (ID: \`${
|
const prompt = `## 任務完成確認\n\n任務 "${task.name}" (ID: \`${
|
||||||
task.id
|
task.id
|
||||||
}\`) 已於 ${new Date().toISOString()} 成功標記為完成。\n\n## 任務報告要求\n\n請提供全面且結構化的任務完成報告,必須包含以下章節:\n\n### 1. 任務概述 (20%)\n- 簡要說明任務目標及其在整體系統中的角色\n- 概述任務的範圍和界限\n- 說明任務的重要性和價值\n\n### 2. 實施摘要 (30%)\n- 詳述採用的技術方案和架構決策\n- 說明關鍵算法和數據結構的選擇\n- 列出使用的外部依賴和API\n\n### 3. 挑戰與解決方案 (20%)\n- 描述在實施過程中遇到的主要技術挑戰\n- 解釋每個挑戰的解決方案及其理由\n- 討論探索過但未採用的替代方案\n\n### 4. 質量保證措施 (15%)\n- 總結執行的測試類型和範圍\n- 報告性能測量結果(如適用)\n- 描述實施的安全措施(如適用)\n\n### 5. 後續步驟與建議 (15%)\n- 提出可能的進一步改進或優化\n- 識別潛在的風險或技術債務\n- 建議下一步行動和優先事項`;
|
}\`) 已於 ${new Date().toISOString()} 成功標記為完成。\n\n## 任務報告要求\n\n請提供全面且結構化的任務完成報告,必須包含以下章節:\n\n### 1. 任務概述 (20%)\n- 簡要說明任務目標及其在整體系統中的角色\n- 概述任務的範圍和界限\n- 說明任務的重要性和價值\n\n### 2. 實施摘要 (30%)\n- 詳述採用的技術方案和架構決策\n- 說明關鍵算法和數據結構的選擇\n- 列出使用的外部依賴和API\n\n### 3. 挑戰與解決方案 (20%)\n- 描述在實施過程中遇到的主要技術挑戰\n- 解釋每個挑戰的解決方案及其理由\n- 討論探索過但未採用的替代方案\n\n### 4. 質量保證措施 (15%)\n- 總結執行的測試類型和範圍\n- 報告性能測量結果(如適用)\n- 描述實施的安全措施(如適用)\n\n### 5. 後續步驟與建議 (15%)\n- 提出可能的進一步改進或優化\n- 識別潛在的風險或技術債務\n- 建議下一步行動和優先事項`;
|
||||||
@ -527,6 +736,18 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
|
|||||||
const task = await getTaskById(taskId);
|
const task = await getTaskById(taskId);
|
||||||
|
|
||||||
if (!task) {
|
if (!task) {
|
||||||
|
// 記錄錯誤日誌
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`刪除任務失敗:找不到ID為 ${taskId} 的任務`,
|
||||||
|
undefined,
|
||||||
|
"錯誤"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -539,6 +760,18 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (task.status === TaskStatus.COMPLETED) {
|
if (task.status === TaskStatus.COMPLETED) {
|
||||||
|
// 記錄操作被拒絕
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`刪除操作被拒絕:嘗試刪除已完成的任務 ${task.name} (ID: ${task.id})`,
|
||||||
|
task.id,
|
||||||
|
"操作被拒絕"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
@ -550,8 +783,34 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 記錄要刪除的任務
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`正在刪除任務:${task.name} (ID: ${task.id})`,
|
||||||
|
task.id,
|
||||||
|
"任務刪除"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
const result = await modelDeleteTask(taskId);
|
const result = await modelDeleteTask(taskId);
|
||||||
|
|
||||||
|
// 記錄刪除結果
|
||||||
|
try {
|
||||||
|
await addConversationEntry(
|
||||||
|
ConversationParticipant.MCP,
|
||||||
|
`任務刪除${result.success ? "成功" : "失敗"}:${task.name} (ID: ${
|
||||||
|
task.id
|
||||||
|
}),原因:${result.message}`,
|
||||||
|
result.success ? undefined : task.id,
|
||||||
|
result.success ? "任務刪除成功" : "任務刪除失敗"
|
||||||
|
);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("記錄對話日誌時發生錯誤:", error);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
content: [
|
content: [
|
||||||
{
|
{
|
||||||
|
@ -70,3 +70,28 @@ export interface VerifyTaskArgs {
|
|||||||
export interface CompleteTaskArgs {
|
export interface CompleteTaskArgs {
|
||||||
taskId: string; // 待標記為完成的任務唯一標識符,必須是狀態為「進行中」的有效任務ID
|
taskId: string; // 待標記為完成的任務唯一標識符,必須是狀態為「進行中」的有效任務ID
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 對話參與者類型:定義對話中的參與方身份
|
||||||
|
export enum ConversationParticipant {
|
||||||
|
MCP = "MCP", // 系統方(MCP)
|
||||||
|
LLM = "LLM", // 模型方(LLM)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 對話日誌條目:記錄 MCP 與 LLM 之間的重要對話內容
|
||||||
|
export interface ConversationEntry {
|
||||||
|
id: string; // 日誌條目的唯一標識符
|
||||||
|
timestamp: Date; // 記錄的時間戳
|
||||||
|
participant: ConversationParticipant; // 對話參與者(MCP 或 LLM)
|
||||||
|
summary: string; // 消息摘要,只記錄關鍵信息點而非完整對話
|
||||||
|
relatedTaskId?: string; // 關聯的任務 ID(選填),用於將對話與特定任務關聯
|
||||||
|
context?: string; // 額外的上下文信息(選填),提供對話發生的背景
|
||||||
|
}
|
||||||
|
|
||||||
|
// 對話日誌列表參數:用於查詢對話日誌的參數
|
||||||
|
export interface ListConversationLogArgs {
|
||||||
|
taskId?: string; // 按任務 ID 過濾(選填)
|
||||||
|
startDate?: Date; // 起始日期過濾(選填)
|
||||||
|
endDate?: Date; // 結束日期過濾(選填)
|
||||||
|
limit?: number; // 返回結果數量限制(選填)
|
||||||
|
offset?: number; // 分頁偏移量(選填)
|
||||||
|
}
|
||||||
|
276
src/utils/summaryExtractor.ts
Normal file
276
src/utils/summaryExtractor.ts
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
/**
|
||||||
|
* 摘要提取工具:從對話內容中提取關鍵信息
|
||||||
|
*
|
||||||
|
* 本模塊提供從完整對話中提取關鍵信息的功能,使用多種策略來識別重要內容:
|
||||||
|
* 1. 關鍵詞匹配:識別含有特定關鍵詞的句子
|
||||||
|
* 2. 句子重要性評分:基於位置、長度等因素評估句子重要性
|
||||||
|
* 3. 上下文關聯:考慮句子間的邏輯關聯
|
||||||
|
*/
|
||||||
|
|
||||||
|
// 定義關鍵詞與其權重
|
||||||
|
const KEYWORDS = {
|
||||||
|
// 任務相關
|
||||||
|
任務: 1.5,
|
||||||
|
功能: 1.3,
|
||||||
|
實現: 1.3,
|
||||||
|
開發: 1.3,
|
||||||
|
完成: 1.2,
|
||||||
|
執行: 1.2,
|
||||||
|
驗證: 1.2,
|
||||||
|
錯誤: 1.5,
|
||||||
|
問題: 1.5,
|
||||||
|
修復: 1.5,
|
||||||
|
失敗: 1.8,
|
||||||
|
成功: 1.5,
|
||||||
|
依賴: 1.2,
|
||||||
|
阻擋: 1.4,
|
||||||
|
風險: 1.4,
|
||||||
|
優化: 1.3,
|
||||||
|
改進: 1.3,
|
||||||
|
|
||||||
|
// 決策相關
|
||||||
|
決定: 1.6,
|
||||||
|
選擇: 1.5,
|
||||||
|
決策: 1.6,
|
||||||
|
方案: 1.5,
|
||||||
|
架構: 1.5,
|
||||||
|
設計: 1.4,
|
||||||
|
結構: 1.4,
|
||||||
|
|
||||||
|
// 技術相關
|
||||||
|
代碼: 1.3,
|
||||||
|
測試: 1.3,
|
||||||
|
函數: 1.2,
|
||||||
|
接口: 1.2,
|
||||||
|
類型: 1.2,
|
||||||
|
模塊: 1.2,
|
||||||
|
組件: 1.2,
|
||||||
|
數據: 1.3,
|
||||||
|
文件: 1.2,
|
||||||
|
路徑: 1.1,
|
||||||
|
|
||||||
|
// 系統狀態
|
||||||
|
狀態: 1.3,
|
||||||
|
啟動: 1.3,
|
||||||
|
停止: 1.3,
|
||||||
|
創建: 1.3,
|
||||||
|
刪除: 1.4,
|
||||||
|
更新: 1.3,
|
||||||
|
查詢: 1.2,
|
||||||
|
|
||||||
|
// 負面信息(需要重點關注)
|
||||||
|
警告: 1.8,
|
||||||
|
異常: 1.8,
|
||||||
|
崩潰: 2.0,
|
||||||
|
嚴重: 1.8,
|
||||||
|
危險: 1.8,
|
||||||
|
緊急: 1.9,
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 從一段文本中提取關鍵信息作為摘要
|
||||||
|
*
|
||||||
|
* @param text 完整的對話文本
|
||||||
|
* @param maxLength 摘要的最大長度(字符數)
|
||||||
|
* @returns 提取的摘要文本
|
||||||
|
*/
|
||||||
|
export function extractSummary(text: string, maxLength: number = 200): string {
|
||||||
|
// 防禦性檢查
|
||||||
|
if (!text || text.trim().length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 將文本分割為句子
|
||||||
|
const sentences = splitIntoSentences(text);
|
||||||
|
|
||||||
|
// 如果只有一個句子且小於最大長度,直接返回
|
||||||
|
if (sentences.length === 1 && sentences[0].length <= maxLength) {
|
||||||
|
return sentences[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// 為每個句子評分
|
||||||
|
const scoredSentences = sentences.map((sentence, index) => ({
|
||||||
|
text: sentence,
|
||||||
|
score: scoreSentence(sentence, index, sentences.length),
|
||||||
|
index,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 按評分排序
|
||||||
|
scoredSentences.sort((a, b) => b.score - a.score);
|
||||||
|
|
||||||
|
// 選擇評分最高的句子,直到達到最大長度
|
||||||
|
let summary = "";
|
||||||
|
let sentencesToInclude: { text: string; index: number }[] = [];
|
||||||
|
|
||||||
|
for (const scored of scoredSentences) {
|
||||||
|
if ((summary + scored.text).length <= maxLength) {
|
||||||
|
sentencesToInclude.push({
|
||||||
|
text: scored.text,
|
||||||
|
index: scored.index,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// 如果還沒有選中任何句子,選擇第一個句子並截斷
|
||||||
|
if (sentencesToInclude.length === 0) {
|
||||||
|
return scored.text.substring(0, maxLength);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按原文順序排列選中的句子
|
||||||
|
sentencesToInclude.sort((a, b) => a.index - b.index);
|
||||||
|
|
||||||
|
// 組合成最終摘要
|
||||||
|
summary = sentencesToInclude.map((s) => s.text).join(" ");
|
||||||
|
|
||||||
|
// 如果摘要仍然太長,進行截斷
|
||||||
|
if (summary.length > maxLength) {
|
||||||
|
summary = summary.substring(0, maxLength - 3) + "...";
|
||||||
|
}
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 將文本分割為句子
|
||||||
|
*
|
||||||
|
* @param text 要分割的文本
|
||||||
|
* @returns 句子數組
|
||||||
|
*/
|
||||||
|
function splitIntoSentences(text: string): string[] {
|
||||||
|
// 使用正則表達式分割句子
|
||||||
|
// 匹配中文和英文的句號、問號、驚嘆號,以及換行符
|
||||||
|
const sentenceSplitters = /(?<=[。.!!??\n])\s*/g;
|
||||||
|
const sentences = text
|
||||||
|
.split(sentenceSplitters)
|
||||||
|
.filter((s) => s.trim().length > 0);
|
||||||
|
|
||||||
|
return sentences;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 為句子評分,決定其在摘要中的重要性
|
||||||
|
*
|
||||||
|
* @param sentence 要評分的句子
|
||||||
|
* @param index 句子在原文中的位置索引
|
||||||
|
* @param totalSentences 原文中的總句子數
|
||||||
|
* @returns 句子的重要性評分
|
||||||
|
*/
|
||||||
|
function scoreSentence(
|
||||||
|
sentence: string,
|
||||||
|
index: number,
|
||||||
|
totalSentences: number
|
||||||
|
): number {
|
||||||
|
let score = 1.0;
|
||||||
|
|
||||||
|
// 位置因素:文檔開頭和結尾的句子通常更重要
|
||||||
|
if (index === 0 || index === totalSentences - 1) {
|
||||||
|
score *= 1.5;
|
||||||
|
} else if (
|
||||||
|
index < Math.ceil(totalSentences * 0.2) ||
|
||||||
|
index >= Math.floor(totalSentences * 0.8)
|
||||||
|
) {
|
||||||
|
score *= 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 句子長度因素:過短的句子可能信息量較少,過長的句子可能包含太多信息
|
||||||
|
const wordCount = sentence.split(/\s+/).length;
|
||||||
|
if (wordCount < 3) {
|
||||||
|
score *= 0.8;
|
||||||
|
} else if (wordCount > 25) {
|
||||||
|
score *= 0.9;
|
||||||
|
} else if (wordCount >= 5 && wordCount <= 15) {
|
||||||
|
score *= 1.2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 關鍵詞因素:包含關鍵詞的句子更重要
|
||||||
|
for (const [keyword, weight] of Object.entries(KEYWORDS)) {
|
||||||
|
if (sentence.includes(keyword)) {
|
||||||
|
score *= weight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 句子結構因素:特殊句式可能更重要
|
||||||
|
if (
|
||||||
|
sentence.includes("總結") ||
|
||||||
|
sentence.includes("結論") ||
|
||||||
|
sentence.includes("因此") ||
|
||||||
|
sentence.includes("所以")
|
||||||
|
) {
|
||||||
|
score *= 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 數字和專有名詞因素:包含數字和專有名詞的句子通常更重要
|
||||||
|
if (/\d+/.test(sentence)) {
|
||||||
|
score *= 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 從指定內容中提取一個簡短標題
|
||||||
|
*
|
||||||
|
* @param content 要提取標題的內容
|
||||||
|
* @param maxLength 標題的最大長度
|
||||||
|
* @returns 提取的標題
|
||||||
|
*/
|
||||||
|
export function extractTitle(content: string, maxLength: number = 50): string {
|
||||||
|
// 防禦性檢查
|
||||||
|
if (!content || content.trim().length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 分割為句子
|
||||||
|
const sentences = splitIntoSentences(content);
|
||||||
|
if (sentences.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 先考慮第一個句子
|
||||||
|
let title = sentences[0];
|
||||||
|
|
||||||
|
// 如果第一個句子太長,找到第一個逗號或其他分隔符截斷
|
||||||
|
if (title.length > maxLength) {
|
||||||
|
const firstPart = title.split(/[,,::]/)[0];
|
||||||
|
if (firstPart && firstPart.length < maxLength) {
|
||||||
|
title = firstPart;
|
||||||
|
} else {
|
||||||
|
title = title.substring(0, maxLength - 3) + "...";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 基於對話上下文智能提取摘要
|
||||||
|
*
|
||||||
|
* @param messages 對話消息列表,每條消息包含角色和內容
|
||||||
|
* @param maxLength 摘要的最大長度
|
||||||
|
* @returns 提取的摘要
|
||||||
|
*/
|
||||||
|
export function extractSummaryFromConversation(
|
||||||
|
messages: Array<{ role: string; content: string }>,
|
||||||
|
maxLength: number = 200
|
||||||
|
): string {
|
||||||
|
// 防禦性檢查
|
||||||
|
if (!messages || messages.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// 如果只有一條消息,直接提取其摘要
|
||||||
|
if (messages.length === 1) {
|
||||||
|
return extractSummary(messages[0].content, maxLength);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 連接所有消息,但保留角色信息
|
||||||
|
const combinedText = messages
|
||||||
|
.map((msg) => `${msg.role}: ${msg.content}`)
|
||||||
|
.join("\n");
|
||||||
|
|
||||||
|
// 從組合文本提取摘要
|
||||||
|
const summary = extractSummary(combinedText, maxLength);
|
||||||
|
|
||||||
|
return summary;
|
||||||
|
}
|
Loading…
x
Reference in New Issue
Block a user