feat: 新增對ListRoots協議的支援,優化DATA_DIR配置方式,並更新相關文檔說明

This commit is contained in:
siage 2025-07-06 20:48:11 +08:00
parent 8771a5bf6f
commit 99baa0fa63
39 changed files with 787 additions and 392 deletions

214
README.md
View File

@ -215,54 +215,25 @@ Shrimp Task Manager can be used with any client that supports the Model Context
Shrimp Task Manager offers two configuration methods: global configuration and project-specific configuration. Shrimp Task Manager offers two configuration methods: global configuration and project-specific configuration.
#### ListRoots Protocol Support
Shrimp Task Manager now supports the **ListRoots protocol**, which enables automatic project isolation and flexible path configuration:
- **If your client supports ListRoots** (e.g., Cursor IDE):
- **Absolute path mode**: Create a project folder within the specified DATA_DIR, enabling you to use a global mcp.json configuration while Shrimp automatically isolates projects
- **Relative path mode**: Create the DATA_DIR within your project root directory for project-specific data storage
- **If your client doesn't support ListRoots**:
- DATA_DIR maintains the legacy behavior (absolute paths recommended)
- We recommend asking your client vendor to support the ListRoots protocol for enhanced functionality
#### Global Configuration #### Global Configuration
1. Open the Cursor IDE global configuration file (usually located at `~/.cursor/mcp.json`) 1. Open the Cursor IDE global configuration file (usually located at `~/.cursor/mcp.json`)
2. Add the following configuration in the `mcpServers` section: 2. Add the following configuration in the `mcpServers` section:
```json **Option A: Absolute Path (Project Isolation Mode)**
{
"mcpServers": {
"shrimp-task-manager": {
"command": "node",
"args": ["/mcp-shrimp-task-manager/dist/index.js"],
"env": {
"DATA_DIR": "/path/to/project/data", // 必須使用絕對路徑
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
}
}
}
or
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "/mcp-shrimp-task-manager/data",
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
}
}
}
```
> ⚠️ Please replace `/mcp-shrimp-task-manager` with your actual path.
>
> 💡 **Optional:** You can add `"WEB_PORT": "3000"` to the `env` section to specify a custom port for the web GUI. If not specified, an available port will be automatically selected.
#### Project-Specific Configuration
You can also set up dedicated configurations for each project to use independent data directories for different projects:
1. Create a `.cursor` directory in the project root
2. Create an `mcp.json` file in this directory with the following content:
```json ```json
{ {
@ -271,24 +242,7 @@ You can also set up dedicated configurations for each project to use independent
"command": "node", "command": "node",
"args": ["/path/to/mcp-shrimp-task-manager/dist/index.js"], "args": ["/path/to/mcp-shrimp-task-manager/dist/index.js"],
"env": { "env": {
"DATA_DIR": "/path/to/project/data", // Must use absolute path "DATA_DIR": "/Users/username/ShrimpData", // Absolute path - creates project folders automatically
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
}
}
}
or
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "/path/to/project/data", // Must use absolute path
"TEMPLATES_USE": "en", "TEMPLATES_USE": "en",
"ENABLE_GUI": "false" "ENABLE_GUI": "false"
} }
@ -297,19 +251,153 @@ or
} }
``` ```
**Option B: NPX with Absolute Path**
```json
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "/Users/username/ShrimpData", // Absolute path - creates project folders automatically
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
}
}
}
```
> ⚠️ Please replace `/path/to/mcp-shrimp-task-manager` and `/Users/username/ShrimpData` with your actual paths.
>
> 💡 **Absolute Path Advantage**: With ListRoots support, Shrimp automatically creates separate folders for each project (e.g., `/Users/username/ShrimpData/my-project/`, `/Users/username/ShrimpData/another-project/`), enabling perfect project isolation with a single global configuration.
>
> 💡 **Optional:** You can add `"WEB_PORT": "3000"` to the `env` section to specify a custom port for the web GUI. If not specified, an available port will be automatically selected.
#### Project-Specific Configuration
You can also set up dedicated configurations for each project. This method allows using relative paths for project-contained data storage:
1. Create a `.cursor` directory in the project root
2. Create an `mcp.json` file in this directory with the following content:
**Option A: Relative Path (Project-Contained Mode)**
```json
{
"mcpServers": {
"shrimp-task-manager": {
"command": "node",
"args": ["/path/to/mcp-shrimp-task-manager/dist/index.js"],
"env": {
"DATA_DIR": ".shrimp", // Relative path - creates folder within project root
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
}
}
}
```
**Option B: NPX with Relative Path**
```json
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "shrimp-data", // Relative path - creates folder within project root
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
}
}
}
```
**Option C: Absolute Path (Alternative)**
```json
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "/Users/username/ShrimpData", // Absolute path with project isolation
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
}
}
}
```
> ⚠️ Please replace `/path/to/mcp-shrimp-task-manager` with your actual path.
>
> 💡 **Relative Path Advantage**: Data is stored within your project directory (e.g., `./shrimp-data/`), making it easy to include or exclude from version control as needed.
>
> 💡 **Optional:** You can add `"WEB_PORT": "3000"` to the `env` section to specify a custom port for the web GUI. If not specified, an available port will be automatically selected. > 💡 **Optional:** You can add `"WEB_PORT": "3000"` to the `env` section to specify a custom port for the web GUI. If not specified, an available port will be automatically selected.
### ⚠️ Important Configuration Notes ### ⚠️ Important Configuration Notes
The **DATA_DIR parameter** is the directory where Shrimp Task Manager stores task data, conversation logs, and other information. Setting this parameter correctly is crucial for the normal operation of the system. This parameter must use an **absolute path**; using a relative path may cause the system to incorrectly locate the data directory, resulting in data loss or function failure. The **DATA_DIR parameter** is the directory where Shrimp Task Manager stores task data, conversation logs, and other information. The new implementation supports both absolute and relative paths with intelligent behavior based on your client's capabilities.
> **Warning**: Using relative paths may cause the following issues: #### 🚀 With ListRoots Protocol Support (Recommended)
If your client supports the **ListRoots protocol** (like Cursor IDE), Shrimp Task Manager automatically detects your project root and provides enhanced functionality:
**Absolute Path Mode (Project Isolation):**
- Configuration: `"DATA_DIR": "/Users/username/ShrimpData"`
- Behavior: Creates `{DATA_DIR}/{project-name}/` automatically
- Example: For project "my-app" → `/Users/username/ShrimpData/my-app/`
- **Advantage**: Use one global configuration for all projects with perfect isolation
**Relative Path Mode (Project-Contained):**
- Configuration: `"DATA_DIR": ".shrimp"` or `"DATA_DIR": "shrimp-data"`
- Behavior: Creates `{project-root}/{DATA_DIR}/` within your project
- Example: For DATA_DIR "shrimp-data" → `./shrimp-data/`
- **Advantage**: Data stays with your project, easy to include/exclude from version control
#### ⚠️ Without ListRoots Protocol Support (Legacy Mode)
If your client **doesn't support ListRoots**, the system falls back to legacy behavior:
- **Absolute paths are strongly recommended** to avoid path resolution issues
- Relative paths may cause inconsistent behavior across different environments
- Consider requesting ListRoots support from your client vendor for enhanced functionality
> **Legacy Warning**: Without ListRoots support, using relative paths may cause:
> >
> - Data files not found, causing system initialization failure > - Data files not found, causing system initialization failure
> - Task status loss or inability to save correctly > - Task status loss or inability to save correctly
> - Inconsistent application behavior across different environments > - Inconsistent application behavior across different environments
> - System crashes or failure to start > - System crashes or failure to start
#### 💡 Choosing the Right Configuration
**Use Absolute Path (Global) when:**
- You want to manage multiple projects with one configuration
- You prefer centralized data storage
- You want automatic project isolation
**Use Relative Path (Project-Specific) when:**
- You want data to stay within the project directory
- You work on projects in different environments
- You need fine control over what gets included in version control
**Use Legacy Mode when:**
- Your client doesn't support ListRoots protocol
- You need compatibility with older client versions
### 🔧 Environment Variable Configuration ### 🔧 Environment Variable Configuration
Shrimp Task Manager supports customizing prompt behavior through environment variables, allowing you to fine-tune AI assistant responses without modifying code. You can set these variables in the configuration or through an `.env` file: Shrimp Task Manager supports customizing prompt behavior through environment variables, allowing you to fine-tune AI assistant responses without modifying code. You can set these variables in the configuration or through an `.env` file:

View File

@ -212,53 +212,25 @@ npm run build
蝦米任務管理器提供兩種配置方式:全局配置和專案特定配置。 蝦米任務管理器提供兩種配置方式:全局配置和專案特定配置。
#### ListRoots 協議支援
蝦米任務管理器現在支援 **ListRoots 協議**,提供自動專案隔離和靈活的路徑配置功能:
- **如果您的客戶端支援 ListRoots** (例如 Cursor IDE)
- **絕對路徑模式**:在指定的 DATA_DIR 中建立專案資料夾,讓您可以使用全域 mcp.json 配置Shrimp 會自動隔離專案
- **相對路徑模式**:在專案根目錄中建立 DATA_DIR 來存放 Shrimp 資料
- **如果您的客戶端不支援 ListRoots**
- DATA_DIR 保持舊版邏輯(建議使用絕對路徑)
- 建議向您的客戶端廠商要求支援 ListRoots 協議以獲得增強功能
#### 全局配置 #### 全局配置
1. 開啟 Cursor IDE 的全局設定檔案(通常位於 `~/.cursor/mcp.json` 1. 開啟 Cursor IDE 的全局設定檔案(通常位於 `~/.cursor/mcp.json`
2. 在 `mcpServers` 區段中添加以下配置: 2. 在 `mcpServers` 區段中添加以下配置:
```json **選項 A絕對路徑專案隔離模式**
{
"mcpServers": {
"shrimp-task-manager": {
"command": "node",
"args": ["/mcp-shrimp-task-manager/dist/index.js"],
"env": {
"DATA_DIR": "/mcp-shrimp-task-manager/data",
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
}
}
}
or
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "/mcp-shrimp-task-manager/data",
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false"
}
}
}
}
```
> ⚠️ 請將 `/mcp-shrimp-task-manager` 替換為您的實際路徑。
>
> 💡 **可選設定:** 您可以在 `env` 區段中添加 `"WEB_PORT": "3000"` 來指定網頁 GUI 的自訂埠號。若未指定,系統將自動選擇可用的埠號。
#### 專案特定配置
您也可以為每個專案設定專屬配置,以便針對不同專案使用獨立的數據目錄:
1. 在專案根目錄創建 `.cursor` 目錄
2. 在該目錄下創建 `mcp.json` 文件,內容如下:
```json ```json
{ {
@ -267,24 +239,8 @@ or
"command": "node", "command": "node",
"args": ["/path/to/mcp-shrimp-task-manager/dist/index.js"], "args": ["/path/to/mcp-shrimp-task-manager/dist/index.js"],
"env": { "env": {
"DATA_DIR": "/path/to/project/data", // 必須使用絕對路徑 "DATA_DIR": "/Users/username/ShrimpData", // 絕對路徑 - 自動建立專案資料夾
"TEMPLATES_USE": "en", "TEMPLATES_USE": "zh",
"ENABLE_GUI": "false"
}
}
}
}
or
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "/path/to/project/data", // 必須使用絕對路徑
"TEMPLATES_USE": "en",
"ENABLE_GUI": "false" "ENABLE_GUI": "false"
} }
} }
@ -292,19 +248,153 @@ or
} }
``` ```
**選項 BNPX 配合絕對路徑**
```json
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "/Users/username/ShrimpData", // 絕對路徑 - 自動建立專案資料夾
"TEMPLATES_USE": "zh",
"ENABLE_GUI": "false"
}
}
}
}
```
> ⚠️ 請將 `/path/to/mcp-shrimp-task-manager``/Users/username/ShrimpData` 替換為您的實際路徑。
>
> 💡 **絕對路徑優勢**:透過 ListRoots 支援Shrimp 會自動為每個專案建立獨立資料夾(例如 `/Users/username/ShrimpData/my-project/``/Users/username/ShrimpData/another-project/`),實現完美的專案隔離,只需要一個全域配置。
>
> 💡 **可選設定:** 您可以在 `env` 區段中添加 `"WEB_PORT": "3000"` 來指定網頁 GUI 的自訂埠號。若未指定,系統將自動選擇可用的埠號。
#### 專案特定配置
您也可以為每個專案設定專屬配置。此方法允許使用相對路徑進行專案內數據存放:
1. 在專案根目錄創建 `.cursor` 目錄
2. 在該目錄下創建 `mcp.json` 文件,內容如下:
**選項 A相對路徑專案內存放模式**
```json
{
"mcpServers": {
"shrimp-task-manager": {
"command": "node",
"args": ["/path/to/mcp-shrimp-task-manager/dist/index.js"],
"env": {
"DATA_DIR": ".shrimp", // 相對路徑 - 在專案根目錄建立資料夾
"TEMPLATES_USE": "zh",
"ENABLE_GUI": "false"
}
}
}
}
```
**選項 BNPX 配合相對路徑**
```json
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "shrimp-data", // 相對路徑 - 在專案根目錄建立資料夾
"TEMPLATES_USE": "zh",
"ENABLE_GUI": "false"
}
}
}
}
```
**選項 C絕對路徑替代方案**
```json
{
"mcpServers": {
"shrimp-task-manager": {
"command": "npx",
"args": ["-y", "mcp-shrimp-task-manager"],
"env": {
"DATA_DIR": "/Users/username/ShrimpData", // 絕對路徑配合專案隔離
"TEMPLATES_USE": "zh",
"ENABLE_GUI": "false"
}
}
}
}
```
> ⚠️ 請將 `/path/to/mcp-shrimp-task-manager` 替換為您的實際路徑。
>
> 💡 **相對路徑優勢**:資料存放在專案目錄內(例如 `./shrimp-data/`),方便根據需要選擇是否納入版本控制。
>
> 💡 **可選設定:** 您可以在 `env` 區段中添加 `"WEB_PORT": "3000"` 來指定網頁 GUI 的自訂埠號。若未指定,系統將自動選擇可用的埠號。 > 💡 **可選設定:** 您可以在 `env` 區段中添加 `"WEB_PORT": "3000"` 來指定網頁 GUI 的自訂埠號。若未指定,系統將自動選擇可用的埠號。
### ⚠️ 重要配置說明 ### ⚠️ 重要配置說明
**DATA_DIR 參數**是蝦米任務管理器存儲任務數據、對話記錄等信息的目錄,正確設置此參數對於系統的正常運行至關重要。此參數必須使用**絕對路徑**,使用相對路徑可能導致系統無法正確定位數據目錄,造成數據丟失或功能失效。 **DATA_DIR 參數**是蝦米任務管理器存儲任務數據、對話記錄等資訊的目錄。新的實作支援絕對路徑和相對路徑,並根據您的客戶端功能提供智慧化行為
> **警告**:使用相對路徑可能導致以下問題: #### 🚀 支援 ListRoots 協議(建議)
如果您的客戶端支援 **ListRoots 協議**(如 Cursor IDE蝦米任務管理器會自動偵測您的專案根目錄並提供增強功能
**絕對路徑模式(專案隔離):**
- 配置:`"DATA_DIR": "/Users/username/ShrimpData"`
- 行為:自動建立 `{DATA_DIR}/{專案名稱}/`
- 範例:對於專案 "my-app" → `/Users/username/ShrimpData/my-app/`
- **優勢**:使用一個全域配置管理所有專案,完美隔離
**相對路徑模式(專案內存放):**
- 配置:`"DATA_DIR": ".shrimp"``"DATA_DIR": "shrimp-data"`
- 行為:在專案內建立 `{專案根目錄}/{DATA_DIR}/`
- 範例:對於 DATA_DIR "shrimp-data" → `./shrimp-data/`
- **優勢**:資料與專案一起存放,方便選擇是否納入版本控制
#### ⚠️ 不支援 ListRoots 協議(舊版模式)
如果您的客戶端**不支援 ListRoots**,系統會回退到舊版行為:
- **強烈建議使用絕對路徑**以避免路徑解析問題
- 相對路徑可能在不同環境下導致行為不一致
- 建議向您的客戶端廠商要求 ListRoots 支援以獲得增強功能
> **舊版警告**:沒有 ListRoots 支援時,使用相對路徑可能導致:
> >
> - 數據檔案找不到,導致系統初始化失敗 > - 數據檔案找不到,導致系統初始化失敗
> - 任務狀態丟失或無法正確保存 > - 任務狀態丟失或無法正確保存
> - 應用程式在不同環境下行為不一致 > - 應用程式在不同環境下行為不一致
> - 系統崩潰或無法啟動 > - 系統崩潰或無法啟動
#### 💡 選擇合適的配置
**使用絕對路徑(全域)當:**
- 您想用一個配置管理多個專案
- 您偏好集中式資料存放
- 您想要自動專案隔離
**使用相對路徑(專案特定)當:**
- 您想讓資料存放在專案目錄內
- 您在不同環境中工作
- 您需要精確控制版本控制的內容
**使用舊版模式當:**
- 您的客戶端不支援 ListRoots 協議
- 您需要與較舊的客戶端版本相容
### 🔧 環境變數配置 ### 🔧 環境變數配置
蝦米任務管理器支援透過環境變數自定義提示詞行為,讓您無需修改程式碼即可微調 AI 助手的回應。您可以在配置中或透過 `.env` 文件設置這些變數: 蝦米任務管理器支援透過環境變數自定義提示詞行為,讓您無需修改程式碼即可微調 AI 助手的回應。您可以在配置中或透過 `.env` 文件設置這些變數:

View File

@ -7,13 +7,10 @@ import {
CallToolRequest, CallToolRequest,
CallToolRequestSchema, CallToolRequestSchema,
ListToolsRequestSchema, ListToolsRequestSchema,
InitializedNotificationSchema,
} from "@modelcontextprotocol/sdk/types.js"; } from "@modelcontextprotocol/sdk/types.js";
import express, { Request, Response, NextFunction } from "express"; import { setGlobalServer } from "./utils/paths.js";
import getPort from "get-port"; import { createWebServer } from "./web/webServer.js";
import path from "path";
import fs from "fs";
import fsPromises from "fs/promises";
import { fileURLToPath } from "url";
// 導入所有工具函數和 schema // 導入所有工具函數和 schema
import { import {
@ -54,131 +51,8 @@ import {
async function main() { async function main() {
try { try {
const ENABLE_GUI = process.env.ENABLE_GUI === "true"; const ENABLE_GUI = process.env.ENABLE_GUI === "true";
let webServerInstance: Awaited<ReturnType<typeof createWebServer>> | null =
if (ENABLE_GUI) { null;
// 創建 Express 應用
const app = express();
// 儲存 SSE 客戶端的列表
let sseClients: Response[] = [];
// 發送 SSE 事件的輔助函數
function sendSseUpdate() {
sseClients.forEach((client) => {
// 檢查客戶端是否仍然連接
if (!client.writableEnded) {
client.write(
`event: update\ndata: ${JSON.stringify({
timestamp: Date.now(),
})}\n\n`
);
}
});
// 清理已斷開的客戶端 (可選,但建議)
sseClients = sseClients.filter((client) => !client.writableEnded);
}
// 設置靜態文件目錄
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const publicPath = path.join(__dirname, "public");
const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, "data");
const TASKS_FILE_PATH = path.join(DATA_DIR, "tasks.json"); // 提取檔案路徑
app.use(express.static(publicPath));
// 設置 API 路由
app.get("/api/tasks", async (req: Request, res: Response) => {
try {
// 使用 fsPromises 保持異步讀取
const tasksData = await fsPromises.readFile(TASKS_FILE_PATH, "utf-8");
res.json(JSON.parse(tasksData));
} catch (error) {
// 確保檔案不存在時返回空任務列表
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
res.json({ tasks: [] });
} else {
res.status(500).json({ error: "Failed to read tasks data" });
}
}
});
// 新增SSE 端點
app.get("/api/tasks/stream", (req: Request, res: Response) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
// 可選: CORS 頭,如果前端和後端不在同一個 origin
// "Access-Control-Allow-Origin": "*",
});
// 發送一個初始事件或保持連接
res.write("data: connected\n\n");
// 將客戶端添加到列表
sseClients.push(res);
// 當客戶端斷開連接時,將其從列表中移除
req.on("close", () => {
sseClients = sseClients.filter((client) => client !== res);
});
});
// 獲取可用埠
const port = process.env.WEB_PORT || (await getPort());
// 啟動 HTTP 伺服器
const httpServer = app.listen(port, () => {
// 在伺服器啟動後開始監聽檔案變化
try {
// 檢查檔案是否存在,如果不存在則不監聽 (避免 watch 報錯)
if (fs.existsSync(TASKS_FILE_PATH)) {
fs.watch(TASKS_FILE_PATH, (eventType, filename) => {
if (
filename &&
(eventType === "change" || eventType === "rename")
) {
// 稍微延遲發送,以防短時間內多次觸發 (例如編輯器保存)
// debounce sendSseUpdate if needed
sendSseUpdate();
}
});
}
} catch (watchError) {}
});
// 將 URL 寫入 WebGUI.md
try {
// 讀取 TEMPLATES_USE 環境變數並轉換為語言代碼
const templatesUse = process.env.TEMPLATES_USE || "en";
const getLanguageFromTemplate = (template: string): string => {
if (template === "zh") return "zh-TW";
if (template === "en") return "en";
// 自訂範本預設使用英文
return "en";
};
const language = getLanguageFromTemplate(templatesUse);
const websiteUrl = `[Task Manager UI](http://localhost:${port}?lang=${language})`;
const websiteFilePath = path.join(DATA_DIR, "WebGUI.md");
await fsPromises.writeFile(websiteFilePath, websiteUrl, "utf-8");
} catch (error) {}
// 設置進程終止事件處理 (確保移除 watcher)
const shutdownHandler = async () => {
// 關閉所有 SSE 連接
sseClients.forEach((client) => client.end());
sseClients = [];
// 關閉 HTTP 伺服器
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
process.exit(0);
};
process.on("SIGINT", shutdownHandler);
process.on("SIGTERM", shutdownHandler);
}
// 創建MCP服務器 // 創建MCP服務器
const server = new Server( const server = new Server(
@ -189,112 +63,128 @@ async function main() {
{ {
capabilities: { capabilities: {
tools: {}, tools: {},
logging: {},
}, },
} }
); );
// 設置全局 server 實例
setGlobalServer(server);
// 監聽 initialized 通知來啟動 web 服務器
if (ENABLE_GUI) {
server.setNotificationHandler(InitializedNotificationSchema, async () => {
try {
webServerInstance = await createWebServer();
await webServerInstance.startServer();
} catch (error) {}
});
}
server.setRequestHandler(ListToolsRequestSchema, async () => { server.setRequestHandler(ListToolsRequestSchema, async () => {
return { return {
tools: [ tools: [
{ {
name: "plan_task", name: "plan_task",
description: loadPromptFromTemplate("toolsDescription/planTask.md"), description: await loadPromptFromTemplate(
"toolsDescription/planTask.md"
),
inputSchema: zodToJsonSchema(planTaskSchema), inputSchema: zodToJsonSchema(planTaskSchema),
}, },
{ {
name: "analyze_task", name: "analyze_task",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/analyzeTask.md" "toolsDescription/analyzeTask.md"
), ),
inputSchema: zodToJsonSchema(analyzeTaskSchema), inputSchema: zodToJsonSchema(analyzeTaskSchema),
}, },
{ {
name: "reflect_task", name: "reflect_task",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/reflectTask.md" "toolsDescription/reflectTask.md"
), ),
inputSchema: zodToJsonSchema(reflectTaskSchema), inputSchema: zodToJsonSchema(reflectTaskSchema),
}, },
{ {
name: "split_tasks", name: "split_tasks",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/splitTasks.md" "toolsDescription/splitTasks.md"
), ),
inputSchema: zodToJsonSchema(splitTasksRawSchema), inputSchema: zodToJsonSchema(splitTasksRawSchema),
}, },
{ {
name: "list_tasks", name: "list_tasks",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/listTasks.md" "toolsDescription/listTasks.md"
), ),
inputSchema: zodToJsonSchema(listTasksSchema), inputSchema: zodToJsonSchema(listTasksSchema),
}, },
{ {
name: "execute_task", name: "execute_task",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/executeTask.md" "toolsDescription/executeTask.md"
), ),
inputSchema: zodToJsonSchema(executeTaskSchema), inputSchema: zodToJsonSchema(executeTaskSchema),
}, },
{ {
name: "verify_task", name: "verify_task",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/verifyTask.md" "toolsDescription/verifyTask.md"
), ),
inputSchema: zodToJsonSchema(verifyTaskSchema), inputSchema: zodToJsonSchema(verifyTaskSchema),
}, },
{ {
name: "delete_task", name: "delete_task",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/deleteTask.md" "toolsDescription/deleteTask.md"
), ),
inputSchema: zodToJsonSchema(deleteTaskSchema), inputSchema: zodToJsonSchema(deleteTaskSchema),
}, },
{ {
name: "clear_all_tasks", name: "clear_all_tasks",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/clearAllTasks.md" "toolsDescription/clearAllTasks.md"
), ),
inputSchema: zodToJsonSchema(clearAllTasksSchema), inputSchema: zodToJsonSchema(clearAllTasksSchema),
}, },
{ {
name: "update_task", name: "update_task",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/updateTask.md" "toolsDescription/updateTask.md"
), ),
inputSchema: zodToJsonSchema(updateTaskContentSchema), inputSchema: zodToJsonSchema(updateTaskContentSchema),
}, },
{ {
name: "query_task", name: "query_task",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/queryTask.md" "toolsDescription/queryTask.md"
), ),
inputSchema: zodToJsonSchema(queryTaskSchema), inputSchema: zodToJsonSchema(queryTaskSchema),
}, },
{ {
name: "get_task_detail", name: "get_task_detail",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/getTaskDetail.md" "toolsDescription/getTaskDetail.md"
), ),
inputSchema: zodToJsonSchema(getTaskDetailSchema), inputSchema: zodToJsonSchema(getTaskDetailSchema),
}, },
{ {
name: "process_thought", name: "process_thought",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/processThought.md" "toolsDescription/processThought.md"
), ),
inputSchema: zodToJsonSchema(processThoughtSchema), inputSchema: zodToJsonSchema(processThoughtSchema),
}, },
{ {
name: "init_project_rules", name: "init_project_rules",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/initProjectRules.md" "toolsDescription/initProjectRules.md"
), ),
inputSchema: zodToJsonSchema(initProjectRulesSchema), inputSchema: zodToJsonSchema(initProjectRulesSchema),
}, },
{ {
name: "research_mode", name: "research_mode",
description: loadPromptFromTemplate( description: await loadPromptFromTemplate(
"toolsDescription/researchMode.md" "toolsDescription/researchMode.md"
), ),
inputSchema: zodToJsonSchema(researchModeSchema), inputSchema: zodToJsonSchema(researchModeSchema),

View File

@ -13,21 +13,25 @@ import { v4 as uuidv4 } from "uuid";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { exec } from "child_process"; import { exec } from "child_process";
import { promisify } from "util"; import { promisify } from "util";
import { getDataDir, getTasksFilePath, getMemoryDir } from "../utils/paths.js";
// 確保獲取專案資料夾路徑 // 確保獲取專案資料夾路徑
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.resolve(__dirname, "../.."); const PROJECT_ROOT = path.resolve(__dirname, "../..");
// 數據文件路徑 // 數據文件路徑(改為異步獲取)
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, "data"); // const DATA_DIR = getDataDir();
const TASKS_FILE = path.join(DATA_DIR, "tasks.json"); // const TASKS_FILE = getTasksFilePath();
// 將exec轉換為Promise形式 // 將exec轉換為Promise形式
const execPromise = promisify(exec); const execPromise = promisify(exec);
// 確保數據目錄存在 // 確保數據目錄存在
async function ensureDataDir() { async function ensureDataDir() {
const DATA_DIR = await getDataDir();
const TASKS_FILE = await getTasksFilePath();
try { try {
await fs.access(DATA_DIR); await fs.access(DATA_DIR);
} catch (error) { } catch (error) {
@ -44,6 +48,7 @@ async function ensureDataDir() {
// 讀取所有任務 // 讀取所有任務
async function readTasks(): Promise<Task[]> { async function readTasks(): Promise<Task[]> {
await ensureDataDir(); await ensureDataDir();
const TASKS_FILE = await getTasksFilePath();
const data = await fs.readFile(TASKS_FILE, "utf-8"); const data = await fs.readFile(TASKS_FILE, "utf-8");
const tasks = JSON.parse(data).tasks; const tasks = JSON.parse(data).tasks;
@ -59,6 +64,7 @@ async function readTasks(): Promise<Task[]> {
// 寫入所有任務 // 寫入所有任務
async function writeTasks(tasks: Task[]): Promise<void> { async function writeTasks(tasks: Task[]): Promise<void> {
await ensureDataDir(); await ensureDataDir();
const TASKS_FILE = await getTasksFilePath();
await fs.writeFile(TASKS_FILE, JSON.stringify({ tasks }, null, 2)); await fs.writeFile(TASKS_FILE, JSON.stringify({ tasks }, null, 2));
} }
@ -697,7 +703,7 @@ export async function clearAllTasks(): Promise<{
const backupFileName = `tasks_memory_${timestamp}.json`; const backupFileName = `tasks_memory_${timestamp}.json`;
// 確保 memory 目錄存在 // 確保 memory 目錄存在
const MEMORY_DIR = path.join(DATA_DIR, "memory"); const MEMORY_DIR = await getMemoryDir();
try { try {
await fs.access(MEMORY_DIR); await fs.access(MEMORY_DIR);
} catch (error) { } catch (error) {
@ -751,7 +757,7 @@ export async function searchTasksWithCommand(
let memoryTasks: Task[] = []; let memoryTasks: Task[] = [];
// 搜尋記憶資料夾中的任務 // 搜尋記憶資料夾中的任務
const MEMORY_DIR = path.join(DATA_DIR, "memory"); const MEMORY_DIR = await getMemoryDir();
try { try {
// 確保記憶資料夾存在 // 確保記憶資料夾存在

View File

@ -23,10 +23,14 @@ export interface AnalyzeTaskPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getAnalyzeTaskPrompt(params: AnalyzeTaskPromptParams): string { export async function getAnalyzeTaskPrompt(
const indexTemplate = loadPromptFromTemplate("analyzeTask/index.md"); params: AnalyzeTaskPromptParams
): Promise<string> {
const indexTemplate = await loadPromptFromTemplate("analyzeTask/index.md");
const iterationTemplate = loadPromptFromTemplate("analyzeTask/iteration.md"); const iterationTemplate = await loadPromptFromTemplate(
"analyzeTask/iteration.md"
);
let iterationPrompt = ""; let iterationPrompt = "";
if (params.previousAnalysis) { if (params.previousAnalysis) {

View File

@ -25,20 +25,24 @@ export interface ClearAllTasksPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getClearAllTasksPrompt( export async function getClearAllTasksPrompt(
params: ClearAllTasksPromptParams params: ClearAllTasksPromptParams
): string { ): Promise<string> {
const { confirm, success, message, backupFile, isEmpty } = params; const { confirm, success, message, backupFile, isEmpty } = params;
// 處理未確認的情況 // 處理未確認的情況
if (confirm === false) { if (confirm === false) {
const cancelTemplate = loadPromptFromTemplate("clearAllTasks/cancel.md"); const cancelTemplate = await loadPromptFromTemplate(
"clearAllTasks/cancel.md"
);
return generatePrompt(cancelTemplate, {}); return generatePrompt(cancelTemplate, {});
} }
// 處理無任務需要清除的情況 // 處理無任務需要清除的情況
if (isEmpty) { if (isEmpty) {
const emptyTemplate = loadPromptFromTemplate("clearAllTasks/empty.md"); const emptyTemplate = await loadPromptFromTemplate(
"clearAllTasks/empty.md"
);
return generatePrompt(emptyTemplate, {}); return generatePrompt(emptyTemplate, {});
} }
@ -47,12 +51,15 @@ export function getClearAllTasksPrompt(
// 使用模板生成 backupInfo // 使用模板生成 backupInfo
const backupInfo = backupFile const backupInfo = backupFile
? generatePrompt(loadPromptFromTemplate("clearAllTasks/backupInfo.md"), { ? generatePrompt(
backupFile, await loadPromptFromTemplate("clearAllTasks/backupInfo.md"),
}) {
backupFile,
}
)
: ""; : "";
const indexTemplate = loadPromptFromTemplate("clearAllTasks/index.md"); const indexTemplate = await loadPromptFromTemplate("clearAllTasks/index.md");
const prompt = generatePrompt(indexTemplate, { const prompt = generatePrompt(indexTemplate, {
responseTitle, responseTitle,
message, message,

View File

@ -23,12 +23,12 @@ export interface CompleteTaskPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getCompleteTaskPrompt( export async function getCompleteTaskPrompt(
params: CompleteTaskPromptParams params: CompleteTaskPromptParams
): string { ): Promise<string> {
const { task, completionTime } = params; const { task, completionTime } = params;
const indexTemplate = loadPromptFromTemplate("completeTask/index.md"); const indexTemplate = await loadPromptFromTemplate("completeTask/index.md");
// 開始構建基本 prompt // 開始構建基本 prompt
let prompt = generatePrompt(indexTemplate, { let prompt = generatePrompt(indexTemplate, {

View File

@ -26,12 +26,16 @@ export interface DeleteTaskPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getDeleteTaskPrompt(params: DeleteTaskPromptParams): string { export async function getDeleteTaskPrompt(
params: DeleteTaskPromptParams
): Promise<string> {
const { taskId, task, success, message, isTaskCompleted } = params; const { taskId, task, success, message, isTaskCompleted } = params;
// 處理任務不存在的情況 // 處理任務不存在的情況
if (!task) { if (!task) {
const notFoundTemplate = loadPromptFromTemplate("deleteTask/notFound.md"); const notFoundTemplate = await loadPromptFromTemplate(
"deleteTask/notFound.md"
);
return generatePrompt(notFoundTemplate, { return generatePrompt(notFoundTemplate, {
taskId, taskId,
}); });
@ -39,7 +43,9 @@ export function getDeleteTaskPrompt(params: DeleteTaskPromptParams): string {
// 處理任務已完成的情況 // 處理任務已完成的情況
if (isTaskCompleted) { if (isTaskCompleted) {
const completedTemplate = loadPromptFromTemplate("deleteTask/completed.md"); const completedTemplate = await loadPromptFromTemplate(
"deleteTask/completed.md"
);
return generatePrompt(completedTemplate, { return generatePrompt(completedTemplate, {
taskId: task.id, taskId: task.id,
taskName: task.name, taskName: task.name,
@ -48,7 +54,7 @@ export function getDeleteTaskPrompt(params: DeleteTaskPromptParams): string {
// 處理刪除成功或失敗的情況 // 處理刪除成功或失敗的情況
const responseTitle = success ? "Success" : "Failure"; const responseTitle = success ? "Success" : "Failure";
const indexTemplate = loadPromptFromTemplate("deleteTask/index.md"); const indexTemplate = await loadPromptFromTemplate("deleteTask/index.md");
const prompt = generatePrompt(indexTemplate, { const prompt = generatePrompt(indexTemplate, {
responseTitle, responseTitle,
message, message,

View File

@ -55,11 +55,13 @@ function getComplexityStyle(level: string): string {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getExecuteTaskPrompt(params: ExecuteTaskPromptParams): string { export async function getExecuteTaskPrompt(
params: ExecuteTaskPromptParams
): Promise<string> {
const { task, complexityAssessment, relatedFilesSummary, dependencyTasks } = const { task, complexityAssessment, relatedFilesSummary, dependencyTasks } =
params; params;
const notesTemplate = loadPromptFromTemplate("executeTask/notes.md"); const notesTemplate = await loadPromptFromTemplate("executeTask/notes.md");
let notesPrompt = ""; let notesPrompt = "";
if (task.notes) { if (task.notes) {
notesPrompt = generatePrompt(notesTemplate, { notesPrompt = generatePrompt(notesTemplate, {
@ -67,7 +69,7 @@ export function getExecuteTaskPrompt(params: ExecuteTaskPromptParams): string {
}); });
} }
const implementationGuideTemplate = loadPromptFromTemplate( const implementationGuideTemplate = await loadPromptFromTemplate(
"executeTask/implementationGuide.md" "executeTask/implementationGuide.md"
); );
let implementationGuidePrompt = ""; let implementationGuidePrompt = "";
@ -77,7 +79,7 @@ export function getExecuteTaskPrompt(params: ExecuteTaskPromptParams): string {
}); });
} }
const verificationCriteriaTemplate = loadPromptFromTemplate( const verificationCriteriaTemplate = await loadPromptFromTemplate(
"executeTask/verificationCriteria.md" "executeTask/verificationCriteria.md"
); );
let verificationCriteriaPrompt = ""; let verificationCriteriaPrompt = "";
@ -87,7 +89,7 @@ export function getExecuteTaskPrompt(params: ExecuteTaskPromptParams): string {
}); });
} }
const analysisResultTemplate = loadPromptFromTemplate( const analysisResultTemplate = await loadPromptFromTemplate(
"executeTask/analysisResult.md" "executeTask/analysisResult.md"
); );
let analysisResultPrompt = ""; let analysisResultPrompt = "";
@ -97,7 +99,7 @@ export function getExecuteTaskPrompt(params: ExecuteTaskPromptParams): string {
}); });
} }
const dependencyTasksTemplate = loadPromptFromTemplate( const dependencyTasksTemplate = await loadPromptFromTemplate(
"executeTask/dependencyTasks.md" "executeTask/dependencyTasks.md"
); );
let dependencyTasksPrompt = ""; let dependencyTasksPrompt = "";
@ -119,7 +121,7 @@ export function getExecuteTaskPrompt(params: ExecuteTaskPromptParams): string {
} }
} }
const relatedFilesSummaryTemplate = loadPromptFromTemplate( const relatedFilesSummaryTemplate = await loadPromptFromTemplate(
"executeTask/relatedFilesSummary.md" "executeTask/relatedFilesSummary.md"
); );
let relatedFilesSummaryPrompt = ""; let relatedFilesSummaryPrompt = "";
@ -127,7 +129,7 @@ export function getExecuteTaskPrompt(params: ExecuteTaskPromptParams): string {
relatedFilesSummary: relatedFilesSummary || "當前任務沒有關聯的文件。", relatedFilesSummary: relatedFilesSummary || "當前任務沒有關聯的文件。",
}); });
const complexityTemplate = loadPromptFromTemplate( const complexityTemplate = await loadPromptFromTemplate(
"executeTask/complexity.md" "executeTask/complexity.md"
); );
let complexityPrompt = ""; let complexityPrompt = "";
@ -151,7 +153,7 @@ export function getExecuteTaskPrompt(params: ExecuteTaskPromptParams): string {
}); });
} }
const indexTemplate = loadPromptFromTemplate("executeTask/index.md"); const indexTemplate = await loadPromptFromTemplate("executeTask/index.md");
let prompt = generatePrompt(indexTemplate, { let prompt = generatePrompt(indexTemplate, {
name: task.name, name: task.name,
id: task.id, id: task.id,

View File

@ -24,14 +24,16 @@ export interface GetTaskDetailPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getGetTaskDetailPrompt( export async function getGetTaskDetailPrompt(
params: GetTaskDetailPromptParams params: GetTaskDetailPromptParams
): string { ): Promise<string> {
const { taskId, task, error } = params; const { taskId, task, error } = params;
// 如果有錯誤,顯示錯誤訊息 // 如果有錯誤,顯示錯誤訊息
if (error) { if (error) {
const errorTemplate = loadPromptFromTemplate("getTaskDetail/error.md"); const errorTemplate = await loadPromptFromTemplate(
"getTaskDetail/error.md"
);
return generatePrompt(errorTemplate, { return generatePrompt(errorTemplate, {
errorMessage: error, errorMessage: error,
}); });
@ -39,7 +41,7 @@ export function getGetTaskDetailPrompt(
// 如果找不到任務,顯示找不到任務的訊息 // 如果找不到任務,顯示找不到任務的訊息
if (!task) { if (!task) {
const notFoundTemplate = loadPromptFromTemplate( const notFoundTemplate = await loadPromptFromTemplate(
"getTaskDetail/notFound.md" "getTaskDetail/notFound.md"
); );
return generatePrompt(notFoundTemplate, { return generatePrompt(notFoundTemplate, {
@ -49,7 +51,9 @@ export function getGetTaskDetailPrompt(
let notesPrompt = ""; let notesPrompt = "";
if (task.notes) { if (task.notes) {
const notesTemplate = loadPromptFromTemplate("getTaskDetail/notes.md"); const notesTemplate = await loadPromptFromTemplate(
"getTaskDetail/notes.md"
);
notesPrompt = generatePrompt(notesTemplate, { notesPrompt = generatePrompt(notesTemplate, {
notes: task.notes, notes: task.notes,
}); });
@ -57,7 +61,7 @@ export function getGetTaskDetailPrompt(
let dependenciesPrompt = ""; let dependenciesPrompt = "";
if (task.dependencies && task.dependencies.length > 0) { if (task.dependencies && task.dependencies.length > 0) {
const dependenciesTemplate = loadPromptFromTemplate( const dependenciesTemplate = await loadPromptFromTemplate(
"getTaskDetail/dependencies.md" "getTaskDetail/dependencies.md"
); );
dependenciesPrompt = generatePrompt(dependenciesTemplate, { dependenciesPrompt = generatePrompt(dependenciesTemplate, {
@ -69,7 +73,7 @@ export function getGetTaskDetailPrompt(
let implementationGuidePrompt = ""; let implementationGuidePrompt = "";
if (task.implementationGuide) { if (task.implementationGuide) {
const implementationGuideTemplate = loadPromptFromTemplate( const implementationGuideTemplate = await loadPromptFromTemplate(
"getTaskDetail/implementationGuide.md" "getTaskDetail/implementationGuide.md"
); );
implementationGuidePrompt = generatePrompt(implementationGuideTemplate, { implementationGuidePrompt = generatePrompt(implementationGuideTemplate, {
@ -79,7 +83,7 @@ export function getGetTaskDetailPrompt(
let verificationCriteriaPrompt = ""; let verificationCriteriaPrompt = "";
if (task.verificationCriteria) { if (task.verificationCriteria) {
const verificationCriteriaTemplate = loadPromptFromTemplate( const verificationCriteriaTemplate = await loadPromptFromTemplate(
"getTaskDetail/verificationCriteria.md" "getTaskDetail/verificationCriteria.md"
); );
verificationCriteriaPrompt = generatePrompt(verificationCriteriaTemplate, { verificationCriteriaPrompt = generatePrompt(verificationCriteriaTemplate, {
@ -89,7 +93,7 @@ export function getGetTaskDetailPrompt(
let relatedFilesPrompt = ""; let relatedFilesPrompt = "";
if (task.relatedFiles && task.relatedFiles.length > 0) { if (task.relatedFiles && task.relatedFiles.length > 0) {
const relatedFilesTemplate = loadPromptFromTemplate( const relatedFilesTemplate = await loadPromptFromTemplate(
"getTaskDetail/relatedFiles.md" "getTaskDetail/relatedFiles.md"
); );
relatedFilesPrompt = generatePrompt(relatedFilesTemplate, { relatedFilesPrompt = generatePrompt(relatedFilesTemplate, {
@ -106,7 +110,7 @@ export function getGetTaskDetailPrompt(
let complatedSummaryPrompt = ""; let complatedSummaryPrompt = "";
if (task.completedAt) { if (task.completedAt) {
const complatedSummaryTemplate = loadPromptFromTemplate( const complatedSummaryTemplate = await loadPromptFromTemplate(
"getTaskDetail/complatedSummary.md" "getTaskDetail/complatedSummary.md"
); );
complatedSummaryPrompt = generatePrompt(complatedSummaryTemplate, { complatedSummaryPrompt = generatePrompt(complatedSummaryTemplate, {
@ -115,7 +119,7 @@ export function getGetTaskDetailPrompt(
}); });
} }
const indexTemplate = loadPromptFromTemplate("getTaskDetail/index.md"); const indexTemplate = await loadPromptFromTemplate("getTaskDetail/index.md");
// 開始構建基本 prompt // 開始構建基本 prompt
let prompt = generatePrompt(indexTemplate, { let prompt = generatePrompt(indexTemplate, {

View File

@ -16,10 +16,12 @@ export interface InitProjectRulesPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getInitProjectRulesPrompt( export async function getInitProjectRulesPrompt(
params?: InitProjectRulesPromptParams params?: InitProjectRulesPromptParams
): string { ): Promise<string> {
const indexTemplate = loadPromptFromTemplate("initProjectRules/index.md"); const indexTemplate = await loadPromptFromTemplate(
"initProjectRules/index.md"
);
// 載入可能的自定義 prompt (通過環境變數覆蓋或追加) // 載入可能的自定義 prompt (通過環境變數覆蓋或追加)
return loadPrompt(indexTemplate, "INIT_PROJECT_RULES"); return loadPrompt(indexTemplate, "INIT_PROJECT_RULES");

View File

@ -24,12 +24,16 @@ export interface ListTasksPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getListTasksPrompt(params: ListTasksPromptParams): string { export async function getListTasksPrompt(
params: ListTasksPromptParams
): Promise<string> {
const { status, tasks, allTasks } = params; const { status, tasks, allTasks } = params;
// 如果沒有任務,顯示通知 // 如果沒有任務,顯示通知
if (allTasks.length === 0) { if (allTasks.length === 0) {
const notFoundTemplate = loadPromptFromTemplate("listTasks/notFound.md"); const notFoundTemplate = await loadPromptFromTemplate(
"listTasks/notFound.md"
);
const statusText = status === "all" ? "任何" : `任何 ${status}`; const statusText = status === "all" ? "任何" : `任何 ${status}`;
return generatePrompt(notFoundTemplate, { return generatePrompt(notFoundTemplate, {
statusText: statusText, statusText: statusText,
@ -58,7 +62,9 @@ export function getListTasksPrompt(params: ListTasksPromptParams): string {
} }
let taskDetails = ""; let taskDetails = "";
let taskDetailsTemplate = loadPromptFromTemplate("listTasks/taskDetails.md"); let taskDetailsTemplate = await loadPromptFromTemplate(
"listTasks/taskDetails.md"
);
// 添加每個狀態下的詳細任務 // 添加每個狀態下的詳細任務
for (const statusType of Object.values(TaskStatus)) { for (const statusType of Object.values(TaskStatus)) {
const tasksWithStatus = tasks[statusType] || []; const tasksWithStatus = tasks[statusType] || [];
@ -88,7 +94,7 @@ export function getListTasksPrompt(params: ListTasksPromptParams): string {
} }
} }
const indexTemplate = loadPromptFromTemplate("listTasks/index.md"); const indexTemplate = await loadPromptFromTemplate("listTasks/index.md");
let prompt = generatePrompt(indexTemplate, { let prompt = generatePrompt(indexTemplate, {
statusCount: statusCounts, statusCount: statusCounts,
taskDetailsTemplate: taskDetails, taskDetailsTemplate: taskDetails,

View File

@ -27,7 +27,9 @@ export interface PlanTaskPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getPlanTaskPrompt(params: PlanTaskPromptParams): string { export async function getPlanTaskPrompt(
params: PlanTaskPromptParams
): Promise<string> {
let tasksContent = ""; let tasksContent = "";
if ( if (
params.existingTasksReference && params.existingTasksReference &&
@ -101,7 +103,7 @@ export function getPlanTaskPrompt(params: PlanTaskPromptParams): string {
}); });
} }
const tasksTemplate = loadPromptFromTemplate("planTask/tasks.md"); const tasksTemplate = await loadPromptFromTemplate("planTask/tasks.md");
tasksContent = generatePrompt(tasksTemplate, { tasksContent = generatePrompt(tasksTemplate, {
completedTasks: completeTasksContent, completedTasks: completeTasksContent,
unfinishedTasks: unfinishedTasksContent, unfinishedTasks: unfinishedTasksContent,
@ -111,11 +113,11 @@ export function getPlanTaskPrompt(params: PlanTaskPromptParams): string {
let thoughtTemplate = ""; let thoughtTemplate = "";
if (process.env.ENABLE_THOUGHT_CHAIN !== "false") { if (process.env.ENABLE_THOUGHT_CHAIN !== "false") {
thoughtTemplate = loadPromptFromTemplate("planTask/hasThought.md"); thoughtTemplate = await loadPromptFromTemplate("planTask/hasThought.md");
} else { } else {
thoughtTemplate = loadPromptFromTemplate("planTask/noThought.md"); thoughtTemplate = await loadPromptFromTemplate("planTask/noThought.md");
} }
const indexTemplate = loadPromptFromTemplate("planTask/index.md"); const indexTemplate = await loadPromptFromTemplate("planTask/index.md");
let prompt = generatePrompt(indexTemplate, { let prompt = generatePrompt(indexTemplate, {
description: params.description, description: params.description,
requirements: params.requirements || "No requirements", requirements: params.requirements || "No requirements",

View File

@ -15,19 +15,21 @@ export interface ProcessThoughtPromptParams {
assumptions_challenged: string[]; assumptions_challenged: string[];
} }
export function getProcessThoughtPrompt( export async function getProcessThoughtPrompt(
param: ProcessThoughtPromptParams param: ProcessThoughtPromptParams
): string { ): Promise<string> {
let nextThoughtNeeded = ""; let nextThoughtNeeded = "";
if (param.nextThoughtNeeded) { if (param.nextThoughtNeeded) {
nextThoughtNeeded = loadPromptFromTemplate("processThought/moreThought.md"); nextThoughtNeeded = await loadPromptFromTemplate(
"processThought/moreThought.md"
);
} else { } else {
nextThoughtNeeded = loadPromptFromTemplate( nextThoughtNeeded = await loadPromptFromTemplate(
"processThought/complatedThought.md" "processThought/complatedThought.md"
); );
} }
const indexTemplate = loadPromptFromTemplate("processThought/index.md"); const indexTemplate = await loadPromptFromTemplate("processThought/index.md");
const prompt = generatePrompt(indexTemplate, { const prompt = generatePrompt(indexTemplate, {
thought: param.thought, thought: param.thought,

View File

@ -28,17 +28,21 @@ export interface QueryTaskPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getQueryTaskPrompt(params: QueryTaskPromptParams): string { export async function getQueryTaskPrompt(
params: QueryTaskPromptParams
): Promise<string> {
const { query, isId, tasks, totalTasks, page, pageSize, totalPages } = params; const { query, isId, tasks, totalTasks, page, pageSize, totalPages } = params;
if (tasks.length === 0) { if (tasks.length === 0) {
const notFoundTemplate = loadPromptFromTemplate("queryTask/notFound.md"); const notFoundTemplate = await loadPromptFromTemplate(
"queryTask/notFound.md"
);
return generatePrompt(notFoundTemplate, { return generatePrompt(notFoundTemplate, {
query, query,
}); });
} }
const taskDetailsTemplate = loadPromptFromTemplate( const taskDetailsTemplate = await loadPromptFromTemplate(
"queryTask/taskDetails.md" "queryTask/taskDetails.md"
); );
let tasksContent = ""; let tasksContent = "";
@ -55,7 +59,7 @@ export function getQueryTaskPrompt(params: QueryTaskPromptParams): string {
}); });
} }
const indexTemplate = loadPromptFromTemplate("queryTask/index.md"); const indexTemplate = await loadPromptFromTemplate("queryTask/index.md");
const prompt = generatePrompt(indexTemplate, { const prompt = generatePrompt(indexTemplate, {
tasksContent, tasksContent,
page, page,

View File

@ -22,8 +22,10 @@ export interface ReflectTaskPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getReflectTaskPrompt(params: ReflectTaskPromptParams): string { export async function getReflectTaskPrompt(
const indexTemplate = loadPromptFromTemplate("reflectTask/index.md"); params: ReflectTaskPromptParams
): Promise<string> {
const indexTemplate = await loadPromptFromTemplate("reflectTask/index.md");
const prompt = generatePrompt(indexTemplate, { const prompt = generatePrompt(indexTemplate, {
summary: params.summary, summary: params.summary,
analysis: params.analysis, analysis: params.analysis,

View File

@ -25,13 +25,13 @@ export interface ResearchModePromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getResearchModePrompt( export async function getResearchModePrompt(
params: ResearchModePromptParams params: ResearchModePromptParams
): string { ): Promise<string> {
// 處理之前的研究狀態 // 處理之前的研究狀態
let previousStateContent = ""; let previousStateContent = "";
if (params.previousState && params.previousState.trim() !== "") { if (params.previousState && params.previousState.trim() !== "") {
const previousStateTemplate = loadPromptFromTemplate( const previousStateTemplate = await loadPromptFromTemplate(
"researchMode/previousState.md" "researchMode/previousState.md"
); );
previousStateContent = generatePrompt(previousStateTemplate, { previousStateContent = generatePrompt(previousStateTemplate, {
@ -42,7 +42,7 @@ export function getResearchModePrompt(
} }
// 載入主要模板 // 載入主要模板
const indexTemplate = loadPromptFromTemplate("researchMode/index.md"); const indexTemplate = await loadPromptFromTemplate("researchMode/index.md");
let prompt = generatePrompt(indexTemplate, { let prompt = generatePrompt(indexTemplate, {
topic: params.topic, topic: params.topic,
previousStateContent: previousStateContent, previousStateContent: previousStateContent,

View File

@ -24,8 +24,10 @@ export interface SplitTasksPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getSplitTasksPrompt(params: SplitTasksPromptParams): string { export async function getSplitTasksPrompt(
const taskDetailsTemplate = loadPromptFromTemplate( params: SplitTasksPromptParams
): Promise<string> {
const taskDetailsTemplate = await loadPromptFromTemplate(
"splitTasks/taskDetails.md" "splitTasks/taskDetails.md"
); );
@ -72,7 +74,7 @@ export function getSplitTasksPrompt(params: SplitTasksPromptParams): string {
}) })
.join("\n"); .join("\n");
const indexTemplate = loadPromptFromTemplate("splitTasks/index.md"); const indexTemplate = await loadPromptFromTemplate("splitTasks/index.md");
const prompt = generatePrompt(indexTemplate, { const prompt = generatePrompt(indexTemplate, {
updateMode: params.updateMode, updateMode: params.updateMode,
tasksContent, tasksContent,

View File

@ -28,9 +28,9 @@ export interface UpdateTaskContentPromptParams {
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getUpdateTaskContentPrompt( export async function getUpdateTaskContentPrompt(
params: UpdateTaskContentPromptParams params: UpdateTaskContentPromptParams
): string { ): Promise<string> {
const { const {
taskId, taskId,
task, task,
@ -43,7 +43,7 @@ export function getUpdateTaskContentPrompt(
// 處理任務不存在的情況 // 處理任務不存在的情況
if (!task) { if (!task) {
const notFoundTemplate = loadPromptFromTemplate( const notFoundTemplate = await loadPromptFromTemplate(
"updateTaskContent/notFound.md" "updateTaskContent/notFound.md"
); );
return generatePrompt(notFoundTemplate, { return generatePrompt(notFoundTemplate, {
@ -53,7 +53,7 @@ export function getUpdateTaskContentPrompt(
// 處理驗證錯誤的情況 // 處理驗證錯誤的情況
if (validationError) { if (validationError) {
const validationTemplate = loadPromptFromTemplate( const validationTemplate = await loadPromptFromTemplate(
"updateTaskContent/validation.md" "updateTaskContent/validation.md"
); );
return generatePrompt(validationTemplate, { return generatePrompt(validationTemplate, {
@ -63,7 +63,7 @@ export function getUpdateTaskContentPrompt(
// 處理空更新的情況 // 處理空更新的情況
if (emptyUpdate) { if (emptyUpdate) {
const emptyUpdateTemplate = loadPromptFromTemplate( const emptyUpdateTemplate = await loadPromptFromTemplate(
"updateTaskContent/emptyUpdate.md" "updateTaskContent/emptyUpdate.md"
); );
return generatePrompt(emptyUpdateTemplate, {}); return generatePrompt(emptyUpdateTemplate, {});
@ -75,14 +75,14 @@ export function getUpdateTaskContentPrompt(
// 更新成功且有更新後的任務詳情 // 更新成功且有更新後的任務詳情
if (success && updatedTask) { if (success && updatedTask) {
const successTemplate = loadPromptFromTemplate( const successTemplate = await loadPromptFromTemplate(
"updateTaskContent/success.md" "updateTaskContent/success.md"
); );
// 編合相關文件信息 // 編合相關文件信息
let filesContent = ""; let filesContent = "";
if (updatedTask.relatedFiles && updatedTask.relatedFiles.length > 0) { if (updatedTask.relatedFiles && updatedTask.relatedFiles.length > 0) {
const fileDetailsTemplate = loadPromptFromTemplate( const fileDetailsTemplate = await loadPromptFromTemplate(
"updateTaskContent/fileDetails.md" "updateTaskContent/fileDetails.md"
); );
@ -130,7 +130,9 @@ export function getUpdateTaskContentPrompt(
}); });
} }
const indexTemplate = loadPromptFromTemplate("updateTaskContent/index.md"); const indexTemplate = await loadPromptFromTemplate(
"updateTaskContent/index.md"
);
const prompt = generatePrompt(indexTemplate, { const prompt = generatePrompt(indexTemplate, {
responseTitle, responseTitle,
message: content, message: content,

View File

@ -44,10 +44,12 @@ function extractSummary(
* @param params prompt * @param params prompt
* @returns prompt * @returns prompt
*/ */
export function getVerifyTaskPrompt(params: VerifyTaskPromptParams): string { export async function getVerifyTaskPrompt(
params: VerifyTaskPromptParams
): Promise<string> {
const { task, score, summary } = params; const { task, score, summary } = params;
if (score < 80) { if (score < 80) {
const noPassTemplate = loadPromptFromTemplate("verifyTask/noPass.md"); const noPassTemplate = await loadPromptFromTemplate("verifyTask/noPass.md");
const prompt = generatePrompt(noPassTemplate, { const prompt = generatePrompt(noPassTemplate, {
name: task.name, name: task.name,
id: task.id, id: task.id,
@ -55,7 +57,7 @@ export function getVerifyTaskPrompt(params: VerifyTaskPromptParams): string {
}); });
return prompt; return prompt;
} }
const indexTemplate = loadPromptFromTemplate("verifyTask/index.md"); const indexTemplate = await loadPromptFromTemplate("verifyTask/index.md");
const prompt = generatePrompt(indexTemplate, { const prompt = generatePrompt(indexTemplate, {
name: task.name, name: task.name,
id: task.id, id: task.id,

View File

@ -6,6 +6,7 @@
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { getDataDir } from "../utils/paths.js";
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
@ -79,22 +80,22 @@ export function generatePrompt(
* @returns * @returns
* @throws Error * @throws Error
*/ */
export function loadPromptFromTemplate(templatePath: string): string { export async function loadPromptFromTemplate(
templatePath: string
): Promise<string> {
const templateSetName = process.env.TEMPLATES_USE || "en"; const templateSetName = process.env.TEMPLATES_USE || "en";
const dataDir = process.env.DATA_DIR; const dataDir = await getDataDir();
const builtInTemplatesBaseDir = __dirname; const builtInTemplatesBaseDir = __dirname;
let finalPath = ""; let finalPath = "";
const checkedPaths: string[] = []; // 用於更詳細的錯誤報告 const checkedPaths: string[] = []; // 用於更詳細的錯誤報告
// 1. 檢查 DATA_DIR 中的自定義路徑 // 1. 檢查 DATA_DIR 中的自定義路徑
if (dataDir) { // path.resolve 可以處理 templateSetName 是絕對路徑的情況
// path.resolve 可以處理 templateSetName 是絕對路徑的情況 const customFilePath = path.resolve(dataDir, templateSetName, templatePath);
const customFilePath = path.resolve(dataDir, templateSetName, templatePath); checkedPaths.push(`Custom: ${customFilePath}`);
checkedPaths.push(`Custom: ${customFilePath}`); if (fs.existsSync(customFilePath)) {
if (fs.existsSync(customFilePath)) { finalPath = customFilePath;
finalPath = customFilePath;
}
} }
// 2. 如果未找到自定義路徑,檢查特定的內建模板目錄 // 2. 如果未找到自定義路徑,檢查特定的內建模板目錄

View File

@ -11,7 +11,7 @@ export const initProjectRulesSchema = z.object({});
export async function initProjectRules() { export async function initProjectRules() {
try { try {
// 從生成器獲取提示詞 // 從生成器獲取提示詞
const promptContent = getInitProjectRulesPrompt(); const promptContent = await getInitProjectRulesPrompt();
// 返回成功響應 // 返回成功響應
return { return {

View File

@ -2,6 +2,7 @@ import { z } from "zod";
import path from "path"; import path from "path";
import { fileURLToPath } from "url"; import { fileURLToPath } from "url";
import { getResearchModePrompt } from "../../prompts/index.js"; import { getResearchModePrompt } from "../../prompts/index.js";
import { getMemoryDir } from "../../utils/paths.js";
// 研究模式工具 // 研究模式工具
export const researchModeSchema = z.object({ export const researchModeSchema = z.object({
@ -40,11 +41,10 @@ export async function researchMode({
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.resolve(__dirname, "../../.."); const PROJECT_ROOT = path.resolve(__dirname, "../../..");
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, "data"); const MEMORY_DIR = await getMemoryDir();
const MEMORY_DIR = path.join(DATA_DIR, "memory");
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getResearchModePrompt({ const prompt = await getResearchModePrompt({
topic, topic,
previousState, previousState,
currentState, currentState,

View File

@ -32,7 +32,7 @@ export async function analyzeTask({
previousAnalysis, previousAnalysis,
}: z.infer<typeof analyzeTaskSchema>) { }: z.infer<typeof analyzeTaskSchema>) {
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getAnalyzeTaskPrompt({ const prompt = await getAnalyzeTaskPrompt({
summary, summary,
initialConcept, initialConcept,
previousAnalysis, previousAnalysis,

View File

@ -25,7 +25,7 @@ export async function clearAllTasks({
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: getClearAllTasksPrompt({ confirm: false }), text: await getClearAllTasksPrompt({ confirm: false }),
}, },
], ],
}; };
@ -38,7 +38,7 @@ export async function clearAllTasks({
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: getClearAllTasksPrompt({ isEmpty: true }), text: await getClearAllTasksPrompt({ isEmpty: true }),
}, },
], ],
}; };
@ -51,7 +51,7 @@ export async function clearAllTasks({
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: getClearAllTasksPrompt({ text: await getClearAllTasksPrompt({
success: result.success, success: result.success,
message: result.message, message: result.message,
backupFile: result.backupFile, backupFile: result.backupFile,

View File

@ -25,7 +25,7 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: getDeleteTaskPrompt({ taskId }), text: await getDeleteTaskPrompt({ taskId }),
}, },
], ],
isError: true, isError: true,
@ -37,7 +37,11 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: getDeleteTaskPrompt({ taskId, task, isTaskCompleted: true }), text: await getDeleteTaskPrompt({
taskId,
task,
isTaskCompleted: true,
}),
}, },
], ],
isError: true, isError: true,
@ -50,7 +54,7 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: getDeleteTaskPrompt({ text: await getDeleteTaskPrompt({
taskId, taskId,
task, task,
success: result.success, success: result.success,

View File

@ -126,7 +126,7 @@ export async function executeTask({
} }
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getExecuteTaskPrompt({ const prompt = await getExecuteTaskPrompt({
task, task,
complexityAssessment, complexityAssessment,
relatedFilesSummary, relatedFilesSummary,

View File

@ -38,7 +38,7 @@ export async function getTaskDetail({
const task = result.tasks[0]; const task = result.tasks[0];
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getGetTaskDetailPrompt({ const prompt = await getGetTaskDetailPrompt({
taskId, taskId,
task, task,
}); });
@ -53,7 +53,7 @@ export async function getTaskDetail({
}; };
} catch (error) { } catch (error) {
// 使用prompt生成器獲取錯誤訊息 // 使用prompt生成器獲取錯誤訊息
const errorPrompt = getGetTaskDetailPrompt({ const errorPrompt = await getGetTaskDetailPrompt({
taskId, taskId,
error: error instanceof Error ? error.message : String(error), error: error instanceof Error ? error.message : String(error),
}); });

View File

@ -55,7 +55,7 @@ export async function listTasks({ status }: z.infer<typeof listTasksSchema>) {
}, {} as Record<string, typeof tasks>); }, {} as Record<string, typeof tasks>);
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getListTasksPrompt({ const prompt = await getListTasksPrompt({
status, status,
tasks: tasksByStatus, tasks: tasksByStatus,
allTasks: filteredTasks, allTasks: filteredTasks,

View File

@ -4,6 +4,7 @@ import { fileURLToPath } from "url";
import { getAllTasks } from "../../models/taskModel.js"; import { getAllTasks } from "../../models/taskModel.js";
import { TaskStatus, Task } from "../../types/index.js"; import { TaskStatus, Task } from "../../types/index.js";
import { getPlanTaskPrompt } from "../../prompts/index.js"; import { getPlanTaskPrompt } from "../../prompts/index.js";
import { getMemoryDir } from "../../utils/paths.js";
// 開始規劃工具 // 開始規劃工具
export const planTaskSchema = z.object({ export const planTaskSchema = z.object({
@ -33,8 +34,7 @@ export async function planTask({
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.resolve(__dirname, "../../.."); const PROJECT_ROOT = path.resolve(__dirname, "../../..");
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, "data"); const MEMORY_DIR = await getMemoryDir();
const MEMORY_DIR = path.join(DATA_DIR, "memory");
// 準備所需參數 // 準備所需參數
let completedTasks: Task[] = []; let completedTasks: Task[] = [];
@ -56,7 +56,7 @@ export async function planTask({
} }
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getPlanTaskPrompt({ const prompt = await getPlanTaskPrompt({
description, description,
requirements, requirements,
existingTasksReference, existingTasksReference,

View File

@ -44,7 +44,7 @@ export async function queryTask({
const results = await searchTasksWithCommand(query, isId, page, pageSize); const results = await searchTasksWithCommand(query, isId, page, pageSize);
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getQueryTaskPrompt({ const prompt = await getQueryTaskPrompt({
query, query,
isId, isId,
tasks: results.tasks, tasks: results.tasks,

View File

@ -24,7 +24,7 @@ export async function reflectTask({
analysis, analysis,
}: z.infer<typeof reflectTaskSchema>) { }: z.infer<typeof reflectTaskSchema>) {
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getReflectTaskPrompt({ const prompt = await getReflectTaskPrompt({
summary, summary,
analysis, analysis,
}); });

View File

@ -209,7 +209,7 @@ export async function splitTasks({
} }
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getSplitTasksPrompt({ const prompt = await getSplitTasksPrompt({
updateMode, updateMode,
createdTasks, createdTasks,
allTasks, allTasks,

View File

@ -247,7 +247,7 @@ export async function splitTasksRaw({
} }
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getSplitTasksPrompt({ const prompt = await getSplitTasksPrompt({
updateMode, updateMode,
createdTasks, createdTasks,
allTasks, allTasks,

View File

@ -84,7 +84,7 @@ export async function updateTaskContent({
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: getUpdateTaskContentPrompt({ text: await getUpdateTaskContentPrompt({
taskId, taskId,
validationError: validationError:
"行號設置無效:必須同時設置起始行和結束行,且起始行必須小於結束行", "行號設置無效:必須同時設置起始行和結束行,且起始行必須小於結束行",
@ -111,7 +111,7 @@ export async function updateTaskContent({
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: getUpdateTaskContentPrompt({ text: await getUpdateTaskContentPrompt({
taskId, taskId,
emptyUpdate: true, emptyUpdate: true,
}), }),
@ -128,7 +128,7 @@ export async function updateTaskContent({
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: getUpdateTaskContentPrompt({ text: await getUpdateTaskContentPrompt({
taskId, taskId,
}), }),
}, },
@ -164,7 +164,7 @@ export async function updateTaskContent({
content: [ content: [
{ {
type: "text" as const, type: "text" as const,
text: getUpdateTaskContentPrompt({ text: await getUpdateTaskContentPrompt({
taskId, taskId,
task, task,
success: result.success, success: result.success,

View File

@ -69,7 +69,7 @@ export async function verifyTask({
} }
// 使用prompt生成器獲取最終prompt // 使用prompt生成器獲取最終prompt
const prompt = getVerifyTaskPrompt({ task, score, summary }); const prompt = await getVerifyTaskPrompt({ task, score, summary });
return { return {
content: [ content: [

View File

@ -74,7 +74,7 @@ export async function processThought(
} }
// 格式化思維輸出 // 格式化思維輸出
const formattedThought = getProcessThoughtPrompt(thoughtData); const formattedThought = await getProcessThoughtPrompt(thoughtData);
// 返回成功響應 // 返回成功響應
return { return {

116
src/utils/paths.ts Normal file
View File

@ -0,0 +1,116 @@
import path from "path";
import { fileURLToPath } from "url";
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import fs from "fs";
// 取得專案根目錄
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const PROJECT_ROOT = path.resolve(__dirname, "../..");
// 全局 server 實例
let globalServer: Server | null = null;
/**
* server
*/
export function setGlobalServer(server: Server): void {
globalServer = server;
}
/**
* server
*/
export function getGlobalServer(): Server | null {
return globalServer;
}
/**
* DATA_DIR
* server listRoots使 file:// 開頭的 root + "/data"
* 使
*/
export async function getDataDir(): Promise<string> {
const server = getGlobalServer();
let rootPath: string | null = null;
if (server) {
try {
const roots = await server.listRoots();
// 找出第一筆 file:// 開頭的 root
if (roots.roots && roots.roots.length > 0) {
const firstFileRoot = roots.roots.find((root) =>
root.uri.startsWith("file://")
);
if (firstFileRoot) {
// 從 file:// URI 中提取實際路徑
rootPath = firstFileRoot.uri.replace("file://", "");
}
}
} catch (error) {
console.error("Failed to get roots:", error);
}
}
// 處理 process.env.DATA_DIR
if (process.env.DATA_DIR) {
if (path.isAbsolute(process.env.DATA_DIR)) {
// 如果 DATA_DIR 是絕對路徑,返回 "DATA_DIR/rootPath最後一個資料夾名稱"
if (rootPath) {
const lastFolderName = path.basename(rootPath);
return path.join(process.env.DATA_DIR, lastFolderName);
} else {
// 如果沒有 rootPath直接返回 DATA_DIR
return process.env.DATA_DIR;
}
} else {
// 如果 DATA_DIR 是相對路徑,返回 "rootPath/DATA_DIR"
if (rootPath) {
return path.join(rootPath, process.env.DATA_DIR);
} else {
// 如果沒有 rootPath使用 PROJECT_ROOT
return path.join(PROJECT_ROOT, process.env.DATA_DIR);
}
}
}
// 如果沒有 DATA_DIR使用預設邏輯
if (rootPath) {
return path.join(rootPath, "data");
}
// 最後回退到專案根目錄
return path.join(PROJECT_ROOT, "data");
}
/**
*
*/
export async function getTasksFilePath(): Promise<string> {
const dataDir = await getDataDir();
return path.join(dataDir, "tasks.json");
}
/**
*
*/
export async function getMemoryDir(): Promise<string> {
const dataDir = await getDataDir();
return path.join(dataDir, "memory");
}
/**
* WebGUI
*/
export async function getWebGuiFilePath(): Promise<string> {
const dataDir = await getDataDir();
return path.join(dataDir, "WebGUI.md");
}
/**
*
*/
export function getProjectRoot(): string {
return PROJECT_ROOT;
}

153
src/web/webServer.ts Normal file
View File

@ -0,0 +1,153 @@
import express, { Request, Response } from "express";
import getPort from "get-port";
import path from "path";
import fs from "fs";
import fsPromises from "fs/promises";
import { fileURLToPath } from "url";
import {
getDataDir,
getTasksFilePath,
getWebGuiFilePath,
} from "../utils/paths.js";
export async function createWebServer() {
// 創建 Express 應用
const app = express();
// 儲存 SSE 客戶端的列表
let sseClients: Response[] = [];
// 發送 SSE 事件的輔助函數
function sendSseUpdate() {
sseClients.forEach((client) => {
// 檢查客戶端是否仍然連接
if (!client.writableEnded) {
client.write(
`event: update\ndata: ${JSON.stringify({
timestamp: Date.now(),
})}\n\n`
);
}
});
// 清理已斷開的客戶端 (可選,但建議)
sseClients = sseClients.filter((client) => !client.writableEnded);
}
// 設置靜態文件目錄
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const publicPath = path.join(__dirname, "..", "..", "src", "public");
const TASKS_FILE_PATH = await getTasksFilePath(); // 使用工具函數取得檔案路徑
app.use(express.static(publicPath));
// 設置 API 路由
app.get("/api/tasks", async (req: Request, res: Response) => {
try {
// 使用 fsPromises 保持異步讀取
const tasksData = await fsPromises.readFile(TASKS_FILE_PATH, "utf-8");
res.json(JSON.parse(tasksData));
} catch (error) {
// 確保檔案不存在時返回空任務列表
if ((error as NodeJS.ErrnoException).code === "ENOENT") {
res.json({ tasks: [] });
} else {
res.status(500).json({ error: "Failed to read tasks data" });
}
}
});
// 新增SSE 端點
app.get("/api/tasks/stream", (req: Request, res: Response) => {
res.writeHead(200, {
"Content-Type": "text/event-stream",
"Cache-Control": "no-cache",
Connection: "keep-alive",
// 可選: CORS 頭,如果前端和後端不在同一個 origin
// "Access-Control-Allow-Origin": "*",
});
// 發送一個初始事件或保持連接
res.write("data: connected\n\n");
// 將客戶端添加到列表
sseClients.push(res);
// 當客戶端斷開連接時,將其從列表中移除
req.on("close", () => {
sseClients = sseClients.filter((client) => client !== res);
});
});
// 定義 writeWebGuiFile 函數
async function writeWebGuiFile(port: number | string) {
try {
// 讀取 TEMPLATES_USE 環境變數並轉換為語言代碼
const templatesUse = process.env.TEMPLATES_USE || "en";
const getLanguageFromTemplate = (template: string): string => {
if (template === "zh") return "zh-TW";
if (template === "en") return "en";
// 自訂範本預設使用英文
return "en";
};
const language = getLanguageFromTemplate(templatesUse);
const websiteUrl = `[Task Manager UI](http://localhost:${port}?lang=${language})`;
const websiteFilePath = await getWebGuiFilePath();
const DATA_DIR = await getDataDir();
try {
await fsPromises.access(DATA_DIR);
} catch (error) {
await fsPromises.mkdir(DATA_DIR, { recursive: true });
}
await fsPromises.writeFile(websiteFilePath, websiteUrl, "utf-8");
} catch (error) {}
}
return {
app,
sendSseUpdate,
async startServer() {
// 獲取可用埠
const port = process.env.WEB_PORT || (await getPort());
// 啟動 HTTP 伺服器
const httpServer = app.listen(port, () => {
// 在伺服器啟動後開始監聽檔案變化
try {
// 檢查檔案是否存在,如果不存在則不監聽 (避免 watch 報錯)
if (fs.existsSync(TASKS_FILE_PATH)) {
fs.watch(TASKS_FILE_PATH, (eventType, filename) => {
if (
filename &&
(eventType === "change" || eventType === "rename")
) {
// 稍微延遲發送,以防短時間內多次觸發 (例如編輯器保存)
// debounce sendSseUpdate if needed
sendSseUpdate();
}
});
}
} catch (watchError) {}
// 將 URL 寫入 WebGUI.md
writeWebGuiFile(port).catch((error) => {});
});
// 設置進程終止事件處理 (確保移除 watcher)
const shutdownHandler = async () => {
// 關閉所有 SSE 連接
sseClients.forEach((client) => client.end());
sseClients = [];
// 關閉 HTTP 伺服器
await new Promise<void>((resolve) => httpServer.close(() => resolve()));
};
process.on("SIGINT", shutdownHandler);
process.on("SIGTERM", shutdownHandler);
return httpServer;
},
};
}