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. **依賴管理**:處理任務間的依賴關係,確保正確的執行順序
|
||||
4. **執行追蹤**:監控任務執行進度和狀態
|
||||
5. **任務驗證**:確保任務符合預期要求
|
||||
6. **工作日誌**:記錄和查詢對話歷史,提供任務執行過程的完整紀錄
|
||||
|
||||
## 任務管理工作流程
|
||||
|
||||
@ -23,6 +24,123 @@
|
||||
7. **檢驗任務 (verify_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,
|
||||
} from "./tools/taskTools.js";
|
||||
|
||||
// 導入日誌工具函數
|
||||
import {
|
||||
listConversationLog,
|
||||
listConversationLogSchema,
|
||||
clearConversationLog,
|
||||
clearConversationLogSchema,
|
||||
} from "./tools/logTools.js";
|
||||
|
||||
// 導入提示模板
|
||||
import {
|
||||
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(
|
||||
"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,
|
||||
deleteTask as modelDeleteTask,
|
||||
} 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({
|
||||
@ -24,6 +26,20 @@ export async function planTask({
|
||||
description,
|
||||
requirements,
|
||||
}: 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`;
|
||||
|
||||
if (requirements) {
|
||||
@ -68,6 +84,23 @@ export async function analyzeTask({
|
||||
initialConcept,
|
||||
previousAnalysis,
|
||||
}: 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`;
|
||||
|
||||
prompt += `## 技術審核指引\n\n請執行以下分析步驟:\n\n1. 檢查現有程式碼庫中的相似實現或可重用組件
|
||||
@ -107,6 +140,23 @@ export async function reflectTask({
|
||||
summary,
|
||||
analysis,
|
||||
}: 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- 方案是否存在技術缺陷或邏輯漏洞?
|
||||
- 是否考慮了邊緣情況和異常處理?
|
||||
- 所有數據流和控制流是否清晰定義?
|
||||
@ -166,9 +216,36 @@ export async function splitTasks({
|
||||
isOverwrite,
|
||||
tasks,
|
||||
}: z.infer<typeof splitTasksSchema>) {
|
||||
// 記錄任務拆分
|
||||
try {
|
||||
await addConversationEntry(
|
||||
ConversationParticipant.MCP,
|
||||
`拆分任務:${isOverwrite ? "覆蓋模式" : "新增模式"},任務數量:${
|
||||
tasks.length
|
||||
}`,
|
||||
undefined,
|
||||
"任務拆分"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("記錄對話日誌時發生錯誤:", error);
|
||||
}
|
||||
|
||||
// 批量創建任務
|
||||
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();
|
||||
|
||||
@ -229,6 +306,18 @@ export async function splitTasks({
|
||||
|
||||
// 列出任務工具
|
||||
export async function listTasks() {
|
||||
// 記錄查詢任務列表
|
||||
try {
|
||||
await addConversationEntry(
|
||||
ConversationParticipant.MCP,
|
||||
"查詢所有任務列表",
|
||||
undefined,
|
||||
"任務列表查詢"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("記錄對話日誌時發生錯誤:", error);
|
||||
}
|
||||
|
||||
const tasks = await getAllTasks();
|
||||
|
||||
if (tasks.length === 0) {
|
||||
@ -316,6 +405,18 @@ export async function executeTask({
|
||||
const task = await getTaskById(taskId);
|
||||
|
||||
if (!task) {
|
||||
// 記錄錯誤日誌
|
||||
try {
|
||||
await addConversationEntry(
|
||||
ConversationParticipant.MCP,
|
||||
`執行任務失敗:找不到ID為 ${taskId} 的任務`,
|
||||
undefined,
|
||||
"錯誤"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("記錄對話日誌時發生錯誤:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@ -328,6 +429,18 @@ export async function executeTask({
|
||||
}
|
||||
|
||||
if (task.status === TaskStatus.COMPLETED) {
|
||||
// 記錄已完成任務的嘗試執行
|
||||
try {
|
||||
await addConversationEntry(
|
||||
ConversationParticipant.MCP,
|
||||
`嘗試執行已完成的任務:${task.name} (ID: ${task.id})`,
|
||||
task.id,
|
||||
"狀態通知"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("記錄對話日誌時發生錯誤:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@ -352,6 +465,18 @@ export async function executeTask({
|
||||
: `ID: \`${id}\``;
|
||||
});
|
||||
|
||||
// 記錄任務被阻擋的情況
|
||||
try {
|
||||
await addConversationEntry(
|
||||
ConversationParticipant.MCP,
|
||||
`任務 ${task.name} (ID: ${task.id}) 被依賴阻擋,等待完成的依賴任務數量: ${blockedBy.length}`,
|
||||
task.id,
|
||||
"依賴阻擋"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("記錄對話日誌時發生錯誤:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@ -370,6 +495,18 @@ export async function executeTask({
|
||||
// 更新任務狀態為進行中
|
||||
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- **名稱:** ${
|
||||
task.name
|
||||
}\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);
|
||||
|
||||
if (!task) {
|
||||
// 記錄錯誤日誌
|
||||
try {
|
||||
await addConversationEntry(
|
||||
ConversationParticipant.MCP,
|
||||
`驗證任務失敗:找不到ID為 ${taskId} 的任務`,
|
||||
undefined,
|
||||
"錯誤"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("記錄對話日誌時發生錯誤:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@ -420,6 +569,18 @@ export async function verifyTask({ taskId }: z.infer<typeof verifyTaskSchema>) {
|
||||
}
|
||||
|
||||
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 {
|
||||
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- **名稱:** ${
|
||||
task.name
|
||||
}\n- **ID:** \`${task.id}\`\n- **描述:** ${task.description}\n${
|
||||
@ -476,6 +649,18 @@ export async function completeTask({
|
||||
const task = await getTaskById(taskId);
|
||||
|
||||
if (!task) {
|
||||
// 記錄錯誤日誌
|
||||
try {
|
||||
await addConversationEntry(
|
||||
ConversationParticipant.MCP,
|
||||
`完成任務失敗:找不到ID為 ${taskId} 的任務`,
|
||||
undefined,
|
||||
"錯誤"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("記錄對話日誌時發生錯誤:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@ -488,6 +673,18 @@ export async function completeTask({
|
||||
}
|
||||
|
||||
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 {
|
||||
content: [
|
||||
{
|
||||
@ -502,6 +699,18 @@ export async function completeTask({
|
||||
// 更新任務狀態為已完成
|
||||
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: \`${
|
||||
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- 建議下一步行動和優先事項`;
|
||||
@ -527,6 +736,18 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
|
||||
const task = await getTaskById(taskId);
|
||||
|
||||
if (!task) {
|
||||
// 記錄錯誤日誌
|
||||
try {
|
||||
await addConversationEntry(
|
||||
ConversationParticipant.MCP,
|
||||
`刪除任務失敗:找不到ID為 ${taskId} 的任務`,
|
||||
undefined,
|
||||
"錯誤"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("記錄對話日誌時發生錯誤:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
content: [
|
||||
{
|
||||
@ -539,6 +760,18 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
|
||||
}
|
||||
|
||||
if (task.status === TaskStatus.COMPLETED) {
|
||||
// 記錄操作被拒絕
|
||||
try {
|
||||
await addConversationEntry(
|
||||
ConversationParticipant.MCP,
|
||||
`刪除操作被拒絕:嘗試刪除已完成的任務 ${task.name} (ID: ${task.id})`,
|
||||
task.id,
|
||||
"操作被拒絕"
|
||||
);
|
||||
} catch (error) {
|
||||
console.error("記錄對話日誌時發生錯誤:", error);
|
||||
}
|
||||
|
||||
return {
|
||||
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);
|
||||
|
||||
// 記錄刪除結果
|
||||
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 {
|
||||
content: [
|
||||
{
|
||||
|
@ -70,3 +70,28 @@ export interface VerifyTaskArgs {
|
||||
export interface CompleteTaskArgs {
|
||||
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