mirror of
https://github.com/cjo4m06/mcp-shrimp-task-manager.git
synced 2025-07-27 08:32:27 +08:00
Compare commits
No commits in common. "main" and "v1.0.19" have entirely different histories.
@ -23,9 +23,6 @@ TEMPLATES_USE=en
|
||||
# Default is false.
|
||||
ENABLE_GUI=false
|
||||
|
||||
# WebGUI port
|
||||
WEB_PORT=3000
|
||||
|
||||
# ============================
|
||||
# Prompt Customization
|
||||
# ============================
|
||||
|
44
.github/PULL_REQUEST_TEMPLATE.md
vendored
44
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,44 +0,0 @@
|
||||
### Description
|
||||
|
||||
<!--
|
||||
Please include a summary of the change and which issue is fixed if applicable.
|
||||
Please also include relevant motivation and context.
|
||||
List any dependencies that are required for this change.
|
||||
-->
|
||||
|
||||
### Type of change
|
||||
|
||||
<!-- Please delete options that are not relevant. -->
|
||||
|
||||
- [ ] Bug fix (non-breaking change which fixes an issue)
|
||||
- [ ] New feature (non-breaking change which adds functionality)
|
||||
- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected)
|
||||
- [ ] This change requires a documentation update
|
||||
|
||||
### How Has This Been Tested?
|
||||
|
||||
<!--
|
||||
Please describe the tests that you ran to verify your changes.
|
||||
Provide instructions so we can reproduce.
|
||||
Please also list any relevant details for your test configuration.
|
||||
-->
|
||||
|
||||
- [ ] `npm run test` (following the project's testing guidelines)
|
||||
- [ ] Manual testing with the following configuration:
|
||||
|
||||
**Test Configuration**:
|
||||
* **Client**: (e.g., Cursor, Claude Code)
|
||||
* **OS**: (e.g., macOS, Windows, Linux)
|
||||
* **Node.js version**:
|
||||
|
||||
### Checklist:
|
||||
|
||||
- [ ] I have performed a self-review of my own code
|
||||
- [ ] I have commented my code, particularly in hard-to-understand areas
|
||||
- [ ] I have made corresponding changes to the documentation (`README.md`, `CHANGELOG.md`)
|
||||
- [ ] My changes generate no new warnings
|
||||
- [ ] I have added tests that prove my fix is effective or that my feature works
|
||||
- [ ] New and existing unit tests pass locally with my changes
|
||||
- [ ] I have checked to ensure my PR is focused on a single feature/fix
|
||||
- [ ] I have updated the version number in `package.json`
|
||||
|
27
CHANGELOG.md
27
CHANGELOG.md
@ -2,33 +2,6 @@
|
||||
|
||||
# Changelog
|
||||
|
||||
## [1.0.21]
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for ListRoots protocol and optimized DATA_DIR configuration method (99baa0f)
|
||||
- Added WEB_PORT environment variable to customize WebGUI port number (8771a5b)
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated documentation to reflect new configuration options (99baa0f, 8771a5b)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fix #56: Added configurable WebGUI port to avoid port conflicts (8771a5b)
|
||||
|
||||
## [1.0.20]
|
||||
|
||||
### Added
|
||||
|
||||
- Added reset button and thumbnail view
|
||||
- Enhanced interaction between dependency graph and task list, making the dependency graph respond to filtering and task list selection
|
||||
|
||||
### Changed
|
||||
|
||||
- Removed initial animation of dependency graph to avoid animation jumps
|
||||
- Optimized initial state of dependency graph
|
||||
|
||||
## [1.0.19]
|
||||
|
||||
### Added
|
||||
|
@ -1,95 +0,0 @@
|
||||
# Contributing to MCP Shrimp Task Manager
|
||||
|
||||
First off, thank you for considering contributing to MCP Shrimp Task Manager! It's people like you that make this such a great tool. We welcome any and all contributions.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
This project and everyone participating in it is governed by a simple code of conduct: be respectful. We are committed to providing a friendly, safe and welcoming environment for all.
|
||||
|
||||
## How Can I Contribute?
|
||||
|
||||
There are many ways to contribute, from writing tutorials or blog posts, improving the documentation, submitting bug reports and feature requests or writing code which can be incorporated into the main project.
|
||||
|
||||
### Reporting Bugs
|
||||
|
||||
If you find a bug, you can either open an issue on our [GitHub repository](https://github.com/cjo4m06/mcp-shrimp-task-manager/issues) or submit a direct pull request with the fix. For more complex bugs that require discussion, opening an issue first is recommended.
|
||||
|
||||
Here is a template you can use if you choose to open an issue:
|
||||
|
||||
```markdown
|
||||
**Describe the bug**
|
||||
A clear and concise description of what the bug is.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
1. **Configuration**: Detail your `mcp.json` or `.env` configuration.
|
||||
2. **Tool Call**: The exact tool call you made (e.g., `plan_task ...`).
|
||||
3. **Context**: Any relevant context, like the state of the `tasks.json` file.
|
||||
4. **Observed Behavior**: The error or incorrect output.
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Environment (please complete the following information):**
|
||||
- **OS**: [e.g. macOS, Linux, Windows]
|
||||
- **Client**: [e.g. Cursor, Claude Code, custom script]
|
||||
- **MCP Shrimp Task Manager Version**: [e.g. 1.0.19]
|
||||
|
||||
**Additional context**
|
||||
Add any other logs or context about the problem here.
|
||||
```
|
||||
|
||||
### Suggesting Enhancements
|
||||
|
||||
If you have an idea for a new feature or an enhancement to an existing one, you can either open an issue on our [GitHub repository](https://github.com/cjo4m06/mcp-shrimp-task-manager/issues) to discuss it, or submit a direct pull request for straightforward additions. For significant features that may impact the architecture, it's advisable to open an issue for discussion beforehand.
|
||||
|
||||
Here is a template you can use if you choose to open an issue:
|
||||
|
||||
```markdown
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen. This could include new tools, or changes to existing tool behaviors.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or mock-ups about the feature request here.
|
||||
```
|
||||
|
||||
### Your First Code Contribution
|
||||
|
||||
Unsure where to begin contributing to MCP Shrimp Task Manager? You can start by looking through these `good-first-issue` and `help-wanted` issues:
|
||||
|
||||
- [Good first issues](https://github.com/cjo4m06/mcp-shrimp-task-manager/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22) - issues which should only require a few lines of code, and a test or two.
|
||||
- [Help wanted issues](https://github.com/cjo4m06/mcp-shrimp-task-manager/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22) - issues which should be a bit more involved than `good-first-issue` issues.
|
||||
|
||||
## Contribution Workflow and Pull Requests
|
||||
|
||||
Since this project maintains a clean commit history and a focused development pace, we follow a strict contribution workflow. All contributions are made via pull requests from forked repositories.
|
||||
|
||||
1. **Fork & Clone**: Fork the repository on GitHub, then clone it to your local machine.
|
||||
2. **Install Dependencies**: Install the dependencies by running `npm install` in the project's root directory.
|
||||
3. **Branch**: Create a new branch for your work, following the convention `feat/your-feature-name` for features or `fix/your-bug-fix` for bug fixes. For example: `git checkout -b feat/new-research-tool`.
|
||||
4. **Make your changes**: Run the build command `npm run build` to make sure everything compiles correctly.
|
||||
5. **Focus Your Work**: Each pull request should address a single issue or feature. Please do not mix bug fixes and new features in the same PR. This focus helps streamline the review process and makes it easier to track changes.
|
||||
6. **Commit**: Commit your changes with a descriptive message that follows the [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) format.
|
||||
7. **Push**: Push your branch to your forked repository.
|
||||
8. **Open a Pull Request**:
|
||||
- Navigate to the [main repository](https://github.com/cjo4m06/mcp-shrimp-task-manager/pulls) and open a new pull request from your forked branch.
|
||||
- GitHub will automatically populate the PR description with our template. Please fill it out as completely as possible. The more context you provide, the faster your PR can be reviewed.
|
||||
9. **Code Review**: The repository owner (`@cjo4m06`) will review your pull request. They may ask for changes or clarification.
|
||||
10. **Merge**: Once the PR is approved, the owner will merge it into the `main` branch. We do not allow contributors to merge their own PRs.
|
||||
|
||||
As a part of the review process, please ensure your PR includes:
|
||||
- Corresponding changes to documentation (`README.md`).
|
||||
- Updates to the `CHANGELOG.md`.
|
||||
- An increase in the version number in `package.json` following [SemVer](http://semver.org/).
|
||||
|
||||
### Testing
|
||||
|
||||
As noted in PR [#35](https://github.com/cjo4m06/mcp-shrimp-task-manager/pull/35), we are in the process of migrating our testing framework to [Vitest](https://vitest.dev/). For any new code contributions, please include tests written with Vitest. This will help us accelerate the transition and ensure the quality of our codebase.
|
||||
|
||||
Thank you for your contribution!
|
124
README.md
124
README.md
@ -42,7 +42,7 @@ Shrimp Task Manager guides Agents through structured workflows for systematic pr
|
||||
- **Task Memory Function**: Automatically backup task history, providing long-term memory and reference capabilities
|
||||
- **Research Mode**: Systematic technical research capabilities with guided workflows for exploring technologies, best practices, and solution comparisons
|
||||
- **Project Rules Initialization**: Define project standards and rules to maintain consistency across large projects
|
||||
- **<a id="web-gui"></a>Web GUI**: Provides an optional web-based graphical user interface for task management. Enable by setting `ENABLE_GUI=true` in your `.env` file. When enabled, a `WebGUI.md` file containing the access address will be created in your `DATA_DIR`. You can customize the web port by setting `WEB_PORT` (if not specified, an available port will be automatically selected).
|
||||
- **<a id="web-gui"></a>Web GUI**: Provides an optional web-based graphical user interface for task management. Enable by setting `ENABLE_GUI=true` in your `.env` file. When enabled, a `WebGUI.md` file containing the access address will be created in your `DATA_DIR`.
|
||||
|
||||
## 🧭 <a id="usage-guide"></a>Usage Guide
|
||||
|
||||
@ -215,52 +215,36 @@ 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.
|
||||
|
||||
#### 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
|
||||
|
||||
1. Open the Cursor IDE global configuration file (usually located at `~/.cursor/mcp.json`)
|
||||
2. Add the following configuration in the `mcpServers` section:
|
||||
|
||||
**Option A: Absolute Path (Project Isolation Mode)**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"shrimp-task-manager": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/mcp-shrimp-task-manager/dist/index.js"],
|
||||
"args": ["/mcp-shrimp-task-manager/dist/index.js"],
|
||||
"env": {
|
||||
"DATA_DIR": "/Users/username/ShrimpData", // Absolute path - creates project folders automatically
|
||||
"DATA_DIR": "/path/to/project/data", // 必須使用絕對路徑
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Option B: NPX with Absolute Path**
|
||||
|
||||
```json
|
||||
or
|
||||
|
||||
{
|
||||
"mcpServers": {
|
||||
"shrimp-task-manager": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-shrimp-task-manager"],
|
||||
"env": {
|
||||
"DATA_DIR": "/Users/username/ShrimpData", // Absolute path - creates project folders automatically
|
||||
"DATA_DIR": "/mcp-shrimp-task-manager/data",
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
@ -269,21 +253,15 @@ Shrimp Task Manager now supports the **ListRoots protocol**, which enables autom
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ 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.
|
||||
> ⚠️ Please replace `/mcp-shrimp-task-manager` with your actual path.
|
||||
|
||||
#### Project-Specific Configuration
|
||||
|
||||
You can also set up dedicated configurations for each project. This method allows using relative paths for project-contained data storage:
|
||||
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:
|
||||
|
||||
**Option A: Relative Path (Project-Contained Mode)**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
@ -291,25 +269,24 @@ You can also set up dedicated configurations for each project. This method allow
|
||||
"command": "node",
|
||||
"args": ["/path/to/mcp-shrimp-task-manager/dist/index.js"],
|
||||
"env": {
|
||||
"DATA_DIR": ".shrimp", // Relative path - creates folder within project root
|
||||
"DATA_DIR": "/path/to/project/data", // Must use absolute path
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Option B: NPX with Relative Path**
|
||||
|
||||
```json
|
||||
or
|
||||
|
||||
{
|
||||
"mcpServers": {
|
||||
"shrimp-task-manager": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-shrimp-task-manager"],
|
||||
"env": {
|
||||
"DATA_DIR": "shrimp-data", // Relative path - creates folder within project root
|
||||
"DATA_DIR": "/path/to/project/data", // Must use absolute path
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
@ -318,86 +295,17 @@ You can also set up dedicated configurations for each project. This method allow
|
||||
}
|
||||
```
|
||||
|
||||
**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.
|
||||
|
||||
### ⚠️ Important Configuration Notes
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
#### 🚀 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:
|
||||
> **Warning**: Using relative paths may cause the following issues:
|
||||
>
|
||||
> - Data files not found, causing system initialization failure
|
||||
> - Task status loss or inability to save correctly
|
||||
> - Inconsistent application behavior across different environments
|
||||
> - 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
|
||||
|
||||
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:
|
||||
@ -428,9 +336,7 @@ There are two customization methods:
|
||||
Additionally, there are other system configuration variables:
|
||||
|
||||
- **DATA_DIR**: Specifies the directory where task data is stored
|
||||
- **TEMPLATES_USE**: Specifies the template set to use for prompts. Defaults to `en`. Currently available options are `en` and `zh`. To use custom templates, copy the `src/prompts/templates_en` directory to the location specified by `DATA_DIR`, rename the copied directory (e.g., to `my_templates`), and set `TEMPLATES_USE` to the new directory name (e.g., `my_templates`)
|
||||
- **ENABLE_GUI**: Enables or disables the web-based graphical user interface. Set to `true` to enable, `false` to disable (default)
|
||||
- **WEB_PORT**: Specifies the port for the web GUI. If not specified, an available port will be automatically selected. Only takes effect when `ENABLE_GUI` is set to `true`
|
||||
- **TEMPLATES_USE**: Specifies the template set to use for prompts. Defaults to `en`. Currently available options are `en` and `zh`. To use custom templates, copy the `src/prompts/templates_en` directory to the location specified by `DATA_DIR`, rename the copied directory (e.g., to `my_templates`), and set `TEMPLATES_USE` to the new directory name (e.g., `my_templates`).
|
||||
|
||||
For detailed instructions on customizing prompts, including supported parameters and examples, see the [Prompt Customization Guide](docs/en/prompt-customization.md).
|
||||
|
||||
|
@ -2,33 +2,6 @@
|
||||
|
||||
# 更新日誌
|
||||
|
||||
## [1.0.21]
|
||||
|
||||
### 新增
|
||||
|
||||
- 新增對 ListRoots 協議的支援,優化 DATA_DIR 配置方式 (99baa0f)
|
||||
- 新增 WEB_PORT 環境變數以自訂 WebGUI 埠號 (8771a5b)
|
||||
|
||||
### 變更
|
||||
|
||||
- 更新相關文檔說明以反映新的配置選項 (99baa0f, 8771a5b)
|
||||
|
||||
### 修復
|
||||
|
||||
- 修復 #56:新增可配置的 WebGUI 埠號以避免埠號衝突 (8771a5b)
|
||||
|
||||
## [1.0.20]
|
||||
|
||||
### 新增
|
||||
|
||||
- 新增重置按鈕和縮略圖
|
||||
- 增強關係依賴圖和任務列表交互,篩選和點擊任務列表使關係依賴圖響應變化
|
||||
|
||||
### 變更
|
||||
|
||||
- 移除關係依賴圖開頭動畫,避免動畫跳變
|
||||
- 優化關係依賴圖初始狀態
|
||||
|
||||
## [1.0.19]
|
||||
|
||||
### 新增
|
||||
|
@ -39,7 +39,7 @@
|
||||
- **思維鏈過程**:通過步驟化的推理系統性地分析複雜問題
|
||||
- **研究模式**:系統性技術研究功能,提供引導式工作流程來探索技術、最佳實踐和解決方案比較
|
||||
- **專案規範初始化**:定義專案標準和規則,維持大型專案的一致性
|
||||
- **<a id="網頁圖形介面"></a>網頁圖形介面**:提供選用的網頁圖形化使用者介面來管理任務。透過在您的 `.env` 檔案中設定 `ENABLE_GUI=true` 來啟用。啟用後,將會在您的 `DATA_DIR` 中建立一個包含存取網址的 `WebGUI.md` 檔案。您可以透過設定 `WEB_PORT` 來自訂網頁埠號(若未指定,系統將自動選擇可用的埠號)。
|
||||
- **<a id="網頁圖形介面"></a>網頁圖形介面**:提供選用的網頁圖形化使用者介面來管理任務。透過在您的 `.env` 檔案中設定 `ENABLE_GUI=true` 來啟用。啟用後,將會在您的 `DATA_DIR` 中建立一個包含存取網址的 `WebGUI.md` 檔案。
|
||||
|
||||
## 🧭 <a id="使用指南"></a>使用指南
|
||||
|
||||
@ -212,53 +212,36 @@ 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`)
|
||||
2. 在 `mcpServers` 區段中添加以下配置:
|
||||
|
||||
**選項 A:絕對路徑(專案隔離模式)**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"shrimp-task-manager": {
|
||||
"command": "node",
|
||||
"args": ["/path/to/mcp-shrimp-task-manager/dist/index.js"],
|
||||
"args": ["/mcp-shrimp-task-manager/dist/index.js"],
|
||||
"env": {
|
||||
"DATA_DIR": "/Users/username/ShrimpData", // 絕對路徑 - 自動建立專案資料夾
|
||||
"TEMPLATES_USE": "zh",
|
||||
"DATA_DIR": "/mcp-shrimp-task-manager/data",
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**選項 B:NPX 配合絕對路徑**
|
||||
or
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"shrimp-task-manager": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-shrimp-task-manager"],
|
||||
"env": {
|
||||
"DATA_DIR": "/Users/username/ShrimpData", // 絕對路徑 - 自動建立專案資料夾
|
||||
"TEMPLATES_USE": "zh",
|
||||
"DATA_DIR": "/mcp-shrimp-task-manager/data",
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
}
|
||||
@ -266,21 +249,15 @@ npm run build
|
||||
}
|
||||
```
|
||||
|
||||
> ⚠️ 請將 `/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 的自訂埠號。若未指定,系統將自動選擇可用的埠號。
|
||||
> ⚠️ 請將 `/mcp-shrimp-task-manager` 替換為您的實際路徑。
|
||||
|
||||
#### 專案特定配置
|
||||
|
||||
您也可以為每個專案設定專屬配置。此方法允許使用相對路徑進行專案內數據存放:
|
||||
您也可以為每個專案設定專屬配置,以便針對不同專案使用獨立的數據目錄:
|
||||
|
||||
1. 在專案根目錄創建 `.cursor` 目錄
|
||||
2. 在該目錄下創建 `mcp.json` 文件,內容如下:
|
||||
|
||||
**選項 A:相對路徑(專案內存放模式)**
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
@ -288,26 +265,24 @@ npm run build
|
||||
"command": "node",
|
||||
"args": ["/path/to/mcp-shrimp-task-manager/dist/index.js"],
|
||||
"env": {
|
||||
"DATA_DIR": ".shrimp", // 相對路徑 - 在專案根目錄建立資料夾
|
||||
"TEMPLATES_USE": "zh",
|
||||
"DATA_DIR": "/path/to/project/data", // 必須使用絕對路徑
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**選項 B:NPX 配合相對路徑**
|
||||
or
|
||||
|
||||
```json
|
||||
{
|
||||
"mcpServers": {
|
||||
"shrimp-task-manager": {
|
||||
"command": "npx",
|
||||
"args": ["-y", "mcp-shrimp-task-manager"],
|
||||
"env": {
|
||||
"DATA_DIR": "shrimp-data", // 相對路徑 - 在專案根目錄建立資料夾
|
||||
"TEMPLATES_USE": "zh",
|
||||
"DATA_DIR": "/path/to/project/data", // 必須使用絕對路徑
|
||||
"TEMPLATES_USE": "en",
|
||||
"ENABLE_GUI": "false"
|
||||
}
|
||||
}
|
||||
@ -315,86 +290,17 @@ npm run build
|
||||
}
|
||||
```
|
||||
|
||||
**選項 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 的自訂埠號。若未指定,系統將自動選擇可用的埠號。
|
||||
|
||||
### ⚠️ 重要配置說明
|
||||
|
||||
**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` 文件設置這些變數:
|
||||
@ -425,9 +331,7 @@ npm run build
|
||||
此外,還有其他系統配置變數:
|
||||
|
||||
- **DATA_DIR**:指定任務數據存儲的目錄
|
||||
- **TEMPLATES_USE**:指定提示詞使用的模板集。預設為 `en`。目前可用的選項有 `en` 和 `zh`。若要使用自定義模板,請將 `src/prompts/templates_en` 目錄複製到 `DATA_DIR` 指定的位置,重新命名複製的目錄(例如,`my_templates`),並將 `TEMPLATES_USE` 設置為新的目錄名稱(例如,`my_templates`)
|
||||
- **ENABLE_GUI**:啟用或停用網頁圖形化使用者介面。設定為 `true` 以啟用,`false` 以停用(預設)
|
||||
- **WEB_PORT**:指定網頁 GUI 的埠號。若未指定,系統將自動選擇可用的埠號。僅在 `ENABLE_GUI` 設定為 `true` 時生效
|
||||
- **TEMPLATES_USE**:指定提示詞使用的模板集。預設為 `en`。目前可用的選項有 `en` 和 `zh`。若要使用自定義模板,請將 `src/prompts/templates_en` 目錄複製到 `DATA_DIR` 指定的位置,重新命名複製的目錄(例如,`my_templates`),並將 `TEMPLATES_USE` 設置為新的目錄名稱(例如,`my_templates`)。
|
||||
|
||||
有關自定義提示詞的詳細說明,包括支援的參數和範例,請參閱[提示詞自定義指南](prompt-customization.md)。
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "mcp-shrimp-task-manager",
|
||||
"version": "1.0.21",
|
||||
"version": "1.0.19",
|
||||
"description": "Shrimp Task Manager is a task tool built for AI Agents, emphasizing chain-of-thought, reflection, and style consistency. It converts natural language into structured dev tasks with dependency tracking and iterative refinement, enabling agent-like developer behavior in reasoning AI systems",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
@ -16,7 +16,7 @@
|
||||
"mcp-shrimp-task-manager": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "tsc && copyfiles -u 1 \"src/**/*.md\" dist && copyfiles -u 1 \"src/public/**/*\" dist && node scripts/add-shebang.js",
|
||||
"build": "tsc && copyfiles -u 1 './src/**/*.md' dist && copyfiles -u 1 './src/public/**/*' dist && node scripts/add-shebang.js",
|
||||
"dev": "ts-node src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
|
@ -1,251 +0,0 @@
|
||||
# Development Guidelines
|
||||
|
||||
This document provides the dedicated specifications for AI Agents performing development tasks in the `mcp-shrimp-task-manager` project.
|
||||
|
||||
## 1. Project Overview
|
||||
|
||||
- **Project Name**: `mcp-shrimp-task-manager`
|
||||
- **Purpose**: A task management tool designed for AI Agents, emphasizing chain-of-thought, reflection, and style consistency. It converts natural language into structured development tasks with dependency tracking and iterative refinement capabilities.
|
||||
- **Technology Stack**:
|
||||
- **Primary Language**: TypeScript
|
||||
- **Runtime Environment**: Node.js (ES Module)
|
||||
- **Main Frameworks/Libraries**: Express.js (for potential API or WebGUI), Zod (for data validation)
|
||||
- **Package Manager**: npm
|
||||
- **Core Features**:
|
||||
- Natural language task parsing
|
||||
- Structured task generation and management
|
||||
- Task dependency tracking
|
||||
- Task execution and verification assistance
|
||||
- Integration with AI Agent's thought processes
|
||||
|
||||
## 2. Project Architecture
|
||||
|
||||
- **Main Source Code Directory**: `src/`
|
||||
- `src/index.ts`: Main application entry point or module export point. **Modifications to this file require careful impact assessment.**
|
||||
- `src/utils/`: General-purpose utility functions.
|
||||
- `src/types/`: TypeScript type definitions. **When adding or modifying types, ensure consistency with Zod schemas (if applicable).**
|
||||
- `src/tools/`: Project-specific tools or modules for integration with external services.
|
||||
- `src/models/`: Data model definitions (potentially related to Zod schemas).
|
||||
- `src/prompts/`: Prompt templates related to AI interaction. **Modifying or adding prompts requires consideration of the potential impact on AI Agent behavior.**
|
||||
- `src/public/`: WebGUI or other static assets.
|
||||
- `src/tests/`: Unit and integration tests.
|
||||
- **Compiled Output Directory**: `dist/` (This directory is auto-generated by `tsc`. **Do not manually modify its contents.**).
|
||||
- **Configuration Files**:
|
||||
- `package.json`: Project dependencies and scripts. **After adding dependencies, you must run `npm install`.**
|
||||
- `tsconfig.json`: TypeScript compilation settings. **Do not modify the `"strict": true` setting unless absolutely necessary.**
|
||||
- `.env.example` & `.env`: Environment variable settings. **Sensitive information must not be committed to version control.**
|
||||
- **Documentation**:
|
||||
- `README.md`: Main project documentation.
|
||||
- `docs/`: May contain more detailed architecture, API documentation, etc.
|
||||
- `CHANGELOG.md`: Record of version changes. **Must be updated before releasing a new version.**
|
||||
- `data/WebGUI.md`: Contains the link to the Task Manager UI.
|
||||
|
||||
## 3. Code Specifications
|
||||
|
||||
### 3.1. Naming Conventions
|
||||
|
||||
- **Variables and Functions**: Use camelCase.
|
||||
- _Do_: `const taskName = "example"; function processTask() {}`
|
||||
- _Don't_: `const Task_Name = "example"; function Process_Task() {}`
|
||||
- **Classes and Interfaces**: Use PascalCase.
|
||||
- _Do_: `class TaskManager {}; interface ITaskOptions {}`
|
||||
- _Don't_: `class taskManager {}; interface iTaskOptions {}`
|
||||
- **File Names**: Use camelCase or kebab-case for `.ts` files.
|
||||
- _Do_: `taskProcessor.ts`, `task-utils.ts`
|
||||
- _Don't_: `TaskProcessor.ts`, `task_utils.ts`
|
||||
- **Constants**: Use UPPER_SNAKE_CASE.
|
||||
- _Do_: `const MAX_RETRIES = 3;`
|
||||
- _Don't_: `const maxRetries = 3;`
|
||||
|
||||
### 3.2. Formatting Requirements
|
||||
|
||||
- **Indentation**: Use 2 spaces for indentation. **Do not use Tab characters.**
|
||||
- **Semicolons**: Statements must end with a semicolon.
|
||||
- **Quotes**: Prefer single quotes (`'`) for strings, unless the string itself contains single quotes.
|
||||
- _Do_: `const message = 'Hello World'; const complex = "It\\'s complex";`
|
||||
- _Don't_: `const message = "Hello World";`
|
||||
- **Max Line Length**: Recommended not to exceed 120 characters.
|
||||
- **Comments**:
|
||||
- Use `//` for single-line comments.
|
||||
- Use `/* ... */` for multi-line comments.
|
||||
- Use JSDoc-style comments for public functions, classes, and methods.
|
||||
- _Do_:
|
||||
```typescript
|
||||
/**
|
||||
* Processes a given task.
|
||||
* @param taskId The ID of the task to process.
|
||||
* @returns True if successful, false otherwise.
|
||||
*/
|
||||
function processTaskById(taskId: string): boolean {
|
||||
// implementation
|
||||
return true;
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3. TypeScript Specific Guidelines
|
||||
|
||||
- **Type Annotations**: All function parameters, return values, and variable declarations should have explicit type annotations. **The use of the `any` type is forbidden, except in very specific and unavoidable circumstances, which require a comment explaining the reason.**
|
||||
- _Do_: `function greet(name: string): string { return \`Hello, ${name}\`; }`
|
||||
- _Don't_: `function greet(name): any { return "Hello, " + name; }`
|
||||
- **Interfaces vs. Type Aliases**: Prefer Interfaces for defining object shapes and Type Aliases for defining union types, tuples, or other complex types.
|
||||
- **ES Modules**: Use `import` and `export` syntax.
|
||||
- _Do_: `import { Task } from './models/task'; export class TaskService {}`
|
||||
- _Don't_: `const Task = require('./models/task'); module.exports = TaskService;`
|
||||
- **Strict Mode**: The project has `"strict": true` enabled. All TypeScript strict mode errors must be resolved.
|
||||
|
||||
## 4. Feature Implementation Guidelines
|
||||
|
||||
### 4.1. General Principles
|
||||
|
||||
- **Single Responsibility Principle (SRP)**: Each function and class should be responsible for only one piece of functionality.
|
||||
- **Keep It Simple, Stupid (KISS)**: Avoid overly complex solutions.
|
||||
- **Don't Repeat Yourself (DRY)**: Extract common logic into reusable functions or classes, stored in `src/utils/` or relevant modules.
|
||||
- **Error Handling**:
|
||||
- Use `try...catch` to handle expected errors.
|
||||
- Provide clear error messages for critical operations.
|
||||
- Consider using custom error classes to provide richer error information.
|
||||
- **Logging**:
|
||||
- Add logging for critical operations, error handling, and important state changes.
|
||||
- Consider using structured logging.
|
||||
- **Do not log sensitive information (e.g., passwords, API Keys).**
|
||||
|
||||
### 4.2. Zod Usage
|
||||
|
||||
- Data structures defined in `src/models/` or `src/types/` should primarily use Zod schemas for definition and validation.
|
||||
- Zod schemas should be kept in sync with TypeScript types. You can use `z.infer<typeof schema>` to generate types.
|
||||
|
||||
- _Do_:
|
||||
|
||||
```typescript
|
||||
import { z } from "zod";
|
||||
|
||||
export const TaskSchema = z.object({
|
||||
id: z.string().uuid(),
|
||||
name: z.string().min(1),
|
||||
description: z.string().optional(),
|
||||
});
|
||||
|
||||
export type Task = z.infer<typeof TaskSchema>;
|
||||
```
|
||||
|
||||
### 4.3. Express.js Usage (if API/WebGUI exists)
|
||||
|
||||
- Route definitions should be clear and follow RESTful principles (for APIs).
|
||||
- Middleware should be effectively organized, e.g., error handling middleware, logging middleware.
|
||||
- All external input (request parameters, body, query) **must** be validated using Zod or a similar mechanism.
|
||||
|
||||
## 5. Framework/Plugin/Third-Party Library Usage Guidelines
|
||||
|
||||
- **Adding Dependencies**:
|
||||
- **Must** first evaluate the necessity, maintenance status, and security of the dependency.
|
||||
- Use `npm install <package-name>` (for runtime dependencies) or `npm install --save-dev <package-name>` (for development dependencies).
|
||||
- **Must** specify a clear version range in `package.json` (e.g., `^1.2.3` or `~1.2.3`), avoid using `*`.
|
||||
- **Updating Dependencies**: Regularly check and update dependencies to the latest stable version to get security patches and new features. Assess potential breaking changes before updating.
|
||||
- **Removing Dependencies**: If a dependency is no longer needed, remove it using `npm uninstall <package-name>` and remove related references from the code.
|
||||
|
||||
## 6. Workflow Guidelines
|
||||
|
||||
### 6.1. Development Process
|
||||
|
||||
1. **Understand the Task**: Carefully read the task description, requirements, and acceptance criteria.
|
||||
2. **Branch Management**: Create a new feature branch from the latest `main` (or `develop`) branch. Branch names should be concise and clear, e.g., `feature/add-task-editing` or `fix/login-bug`.
|
||||
3. **Coding and Testing**:
|
||||
- Code according to these guidelines.
|
||||
- **Must** write unit tests (stored in `src/tests/`) for new features or bug fixes.
|
||||
- Run `npm run build` to ensure the code compiles successfully.
|
||||
- Run `npm run dev` or `npm run start` locally for testing.
|
||||
4. **Code Committing**:
|
||||
- Git commit messages should follow the Conventional Commits specification (e.g., `feat: add user authentication`, `fix: resolve issue with task sorting`).
|
||||
- **Do not commit code with `console.log` or other debugging messages to the main branches.**
|
||||
5. **Pull Request (PR)**:
|
||||
- Push the feature branch to the remote repository and create a Pull Request to the `main` (or `develop`) branch.
|
||||
- The PR description should clearly explain the changes and their purpose.
|
||||
6. **Code Review**: Wait for other developers or AI Agents to conduct a code review.
|
||||
7. **Merge and Deploy**: After the code review is passed, merge the PR. The deployment process follows the project setup.
|
||||
|
||||
### 6.2. Version Control (Git)
|
||||
|
||||
- **Main Branches**:
|
||||
- `main`: Represents the stable and deployable product version.
|
||||
- `develop` (if used): Represents the latest in-development version.
|
||||
- **Commit Frequency**: Small, frequent, and meaningful commits are recommended.
|
||||
- **Resolving Conflicts**: When merging or rebasing branches, if conflicts occur, they **must** be resolved carefully to ensure the correctness and integrity of the code.
|
||||
|
||||
### 6.3. CHANGELOG Updates
|
||||
|
||||
- Before releasing a new version, `CHANGELOG.md` **must** be updated.
|
||||
- The log should include the version number, release date, and a list of new features, bug fixes, and breaking changes.
|
||||
|
||||
## 7. Key File Interaction Guidelines
|
||||
|
||||
- **Modifying `src/types/` or `src/models/` (especially Zod schemas)**:
|
||||
- **Must** check and update all files that reference these types or schemas to ensure type consistency.
|
||||
- **Must** re-run relevant tests.
|
||||
- **Modifying `src/index.ts`**:
|
||||
- If the module's exported API is changed, **must** check all dependent projects or files and make necessary adjustments.
|
||||
- **Modifying `package.json` (especially `dependencies` or `scripts`)**:
|
||||
- **Must** notify team members or relevant AI Agents to run `npm install`.
|
||||
- If `scripts` are modified, ensure the CI/CD process (if any) is updated accordingly.
|
||||
- **Modifying `.env.example`**:
|
||||
- **Must** synchronously update `.env` files in all development environments and notify team members.
|
||||
- **Modifying `README.md` or documents in `docs/`**:
|
||||
- If changes involve core features or usage, **must** ensure the accuracy and timeliness of the documentation.
|
||||
|
||||
## 8. AI Decision-Making Guidelines
|
||||
|
||||
### 8.1. Handling Vague Requests
|
||||
|
||||
- When receiving a vague development instruction (e.g., "optimize the task list display"):
|
||||
1. **Attempt to Clarify**: If possible, request more specific details or expected outcomes from the user or task initiator.
|
||||
2. **Analyze Context**: Check related code (`src/`), existing UI (if any), and relevant issues (if any) to infer intent.
|
||||
3. **Propose Solutions**: Based on the analysis, propose 1-2 concrete implementation plans, explaining their pros, cons, and estimated effort.
|
||||
4. **Wait for Confirmation**: Do not proceed with large-scale code modifications until clear instructions are received.
|
||||
|
||||
### 8.2. Error/Exception Handling Strategy
|
||||
|
||||
- **Priorities**:
|
||||
1. **User Experience**: Avoid program crashes and provide friendly error messages.
|
||||
2. **Data Integrity**: Ensure errors do not lead to data corruption or inconsistency.
|
||||
3. **System Stability**: Log detailed error information for troubleshooting.
|
||||
- **Choices**:
|
||||
- For predictable errors (e.g., invalid user input), handle them within the operation's context and provide feedback.
|
||||
- For unexpected system errors, catch, log, and potentially re-throw or trigger a global error handling mechanism.
|
||||
|
||||
### 8.3. Dependency Selection
|
||||
|
||||
- When needing to introduce a new third-party library:
|
||||
1. **Check Existing**: Verify if a similar library that meets the requirements already exists in the project.
|
||||
2. **Evaluate Options**:
|
||||
- **Activity and Community Support**: Choose well-maintained libraries with active communities.
|
||||
- **Lightweight**: Avoid introducing libraries that are too large or have redundant features.
|
||||
- **Security**: Check for known security vulnerabilities.
|
||||
- **License**: Ensure compatibility with the project's license.
|
||||
3. **Minimization Principle**: Only introduce libraries that are genuinely needed.
|
||||
|
||||
## 9. Prohibitions
|
||||
|
||||
- **Do not directly modify any files in the `dist/` directory.** This directory contains compiled output.
|
||||
- **Do not assume a new dependency is available without running `npm install`.**
|
||||
- **Do not commit untested or unfinished code directly to the main branches (`main` or `develop`).** Use feature branches.
|
||||
- **Do not commit code containing API Keys, passwords, or other sensitive information to the version control system.** Use the `.env` file for such information.
|
||||
- **Do not make major changes to the core architecture or public API without notification and approval.**
|
||||
- **Do not ignore TypeScript type errors.** All errors reported by `tsc` must be resolved.
|
||||
- **Do not use the `any` type without a very good reason and a corresponding comment.**
|
||||
- **Do not leave large amounts of `console.log` or other temporary debugging code in the codebase.**
|
||||
- **Do not release a new version without updating `CHANGELOG.md`.**
|
||||
- **Do not introduce third-party libraries incompatible with the project's MIT license.**
|
||||
|
||||
## 10. Updating This Guideline Document (`shrimp-rules.md`)
|
||||
|
||||
- When the project's tech stack, core architecture, main workflow, or important guidelines change, this document **must** be updated accordingly.
|
||||
- Update requests should clearly specify the sections and content to be changed.
|
||||
- If a vague "update rules" instruction is received, the AI Agent **must**:
|
||||
1. Autonomously analyze the current codebase for changes (e.g., `git diff`, recent commits).
|
||||
2. Compare the existing `shrimp-rules.md` with the project's current state to find inconsistent or outdated rules.
|
||||
3. List the inferred update points and their justifications in the `process_thought` stage.
|
||||
4. Propose specific modifications or directly edit this document.
|
||||
5. **Strictly prohibited** to seek clarification from the user on a vague request before performing an autonomous analysis.
|
||||
|
||||
---
|
||||
|
||||
This development guide aims to ensure that AI Agents can participate in the development of the `mcp-shrimp-task-manager` project efficiently, consistently, and securely.
|
182
src/index.ts
182
src/index.ts
@ -7,10 +7,13 @@ import {
|
||||
CallToolRequest,
|
||||
CallToolRequestSchema,
|
||||
ListToolsRequestSchema,
|
||||
InitializedNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { setGlobalServer } from "./utils/paths.js";
|
||||
import { createWebServer } from "./web/webServer.js";
|
||||
import express, { Request, Response, NextFunction } from "express";
|
||||
import getPort from "get-port";
|
||||
import path from "path";
|
||||
import fs from "fs";
|
||||
import fsPromises from "fs/promises";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
// 導入所有工具函數和 schema
|
||||
import {
|
||||
@ -51,8 +54,131 @@ import {
|
||||
async function main() {
|
||||
try {
|
||||
const ENABLE_GUI = process.env.ENABLE_GUI === "true";
|
||||
let webServerInstance: Awaited<ReturnType<typeof createWebServer>> | null =
|
||||
null;
|
||||
|
||||
if (ENABLE_GUI) {
|
||||
// 創建 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 = 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服務器
|
||||
const server = new Server(
|
||||
@ -63,128 +189,112 @@ async function main() {
|
||||
{
|
||||
capabilities: {
|
||||
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 () => {
|
||||
return {
|
||||
tools: [
|
||||
{
|
||||
name: "plan_task",
|
||||
description: await loadPromptFromTemplate(
|
||||
"toolsDescription/planTask.md"
|
||||
),
|
||||
description: loadPromptFromTemplate("toolsDescription/planTask.md"),
|
||||
inputSchema: zodToJsonSchema(planTaskSchema),
|
||||
},
|
||||
{
|
||||
name: "analyze_task",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/analyzeTask.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(analyzeTaskSchema),
|
||||
},
|
||||
{
|
||||
name: "reflect_task",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/reflectTask.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(reflectTaskSchema),
|
||||
},
|
||||
{
|
||||
name: "split_tasks",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/splitTasks.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(splitTasksRawSchema),
|
||||
},
|
||||
{
|
||||
name: "list_tasks",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/listTasks.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(listTasksSchema),
|
||||
},
|
||||
{
|
||||
name: "execute_task",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/executeTask.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(executeTaskSchema),
|
||||
},
|
||||
{
|
||||
name: "verify_task",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/verifyTask.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(verifyTaskSchema),
|
||||
},
|
||||
{
|
||||
name: "delete_task",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/deleteTask.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(deleteTaskSchema),
|
||||
},
|
||||
{
|
||||
name: "clear_all_tasks",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/clearAllTasks.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(clearAllTasksSchema),
|
||||
},
|
||||
{
|
||||
name: "update_task",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/updateTask.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(updateTaskContentSchema),
|
||||
},
|
||||
{
|
||||
name: "query_task",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/queryTask.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(queryTaskSchema),
|
||||
},
|
||||
{
|
||||
name: "get_task_detail",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/getTaskDetail.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(getTaskDetailSchema),
|
||||
},
|
||||
{
|
||||
name: "process_thought",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/processThought.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(processThoughtSchema),
|
||||
},
|
||||
{
|
||||
name: "init_project_rules",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/initProjectRules.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(initProjectRulesSchema),
|
||||
},
|
||||
{
|
||||
name: "research_mode",
|
||||
description: await loadPromptFromTemplate(
|
||||
description: loadPromptFromTemplate(
|
||||
"toolsDescription/researchMode.md"
|
||||
),
|
||||
inputSchema: zodToJsonSchema(researchModeSchema),
|
||||
|
@ -13,25 +13,21 @@ import { v4 as uuidv4 } from "uuid";
|
||||
import { fileURLToPath } from "url";
|
||||
import { exec } from "child_process";
|
||||
import { promisify } from "util";
|
||||
import { getDataDir, getTasksFilePath, getMemoryDir } from "../utils/paths.js";
|
||||
|
||||
// 確保獲取專案資料夾路徑
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "../..");
|
||||
|
||||
// 數據文件路徑(改為異步獲取)
|
||||
// const DATA_DIR = getDataDir();
|
||||
// const TASKS_FILE = getTasksFilePath();
|
||||
// 數據文件路徑
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, "data");
|
||||
const TASKS_FILE = path.join(DATA_DIR, "tasks.json");
|
||||
|
||||
// 將exec轉換為Promise形式
|
||||
const execPromise = promisify(exec);
|
||||
|
||||
// 確保數據目錄存在
|
||||
async function ensureDataDir() {
|
||||
const DATA_DIR = await getDataDir();
|
||||
const TASKS_FILE = await getTasksFilePath();
|
||||
|
||||
try {
|
||||
await fs.access(DATA_DIR);
|
||||
} catch (error) {
|
||||
@ -48,7 +44,6 @@ async function ensureDataDir() {
|
||||
// 讀取所有任務
|
||||
async function readTasks(): Promise<Task[]> {
|
||||
await ensureDataDir();
|
||||
const TASKS_FILE = await getTasksFilePath();
|
||||
const data = await fs.readFile(TASKS_FILE, "utf-8");
|
||||
const tasks = JSON.parse(data).tasks;
|
||||
|
||||
@ -64,7 +59,6 @@ async function readTasks(): Promise<Task[]> {
|
||||
// 寫入所有任務
|
||||
async function writeTasks(tasks: Task[]): Promise<void> {
|
||||
await ensureDataDir();
|
||||
const TASKS_FILE = await getTasksFilePath();
|
||||
await fs.writeFile(TASKS_FILE, JSON.stringify({ tasks }, null, 2));
|
||||
}
|
||||
|
||||
@ -703,7 +697,7 @@ export async function clearAllTasks(): Promise<{
|
||||
const backupFileName = `tasks_memory_${timestamp}.json`;
|
||||
|
||||
// 確保 memory 目錄存在
|
||||
const MEMORY_DIR = await getMemoryDir();
|
||||
const MEMORY_DIR = path.join(DATA_DIR, "memory");
|
||||
try {
|
||||
await fs.access(MEMORY_DIR);
|
||||
} catch (error) {
|
||||
@ -757,7 +751,7 @@ export async function searchTasksWithCommand(
|
||||
let memoryTasks: Task[] = [];
|
||||
|
||||
// 搜尋記憶資料夾中的任務
|
||||
const MEMORY_DIR = await getMemoryDir();
|
||||
const MEMORY_DIR = path.join(DATA_DIR, "memory");
|
||||
|
||||
try {
|
||||
// 確保記憶資料夾存在
|
||||
|
@ -23,14 +23,10 @@ export interface AnalyzeTaskPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getAnalyzeTaskPrompt(
|
||||
params: AnalyzeTaskPromptParams
|
||||
): Promise<string> {
|
||||
const indexTemplate = await loadPromptFromTemplate("analyzeTask/index.md");
|
||||
export function getAnalyzeTaskPrompt(params: AnalyzeTaskPromptParams): string {
|
||||
const indexTemplate = loadPromptFromTemplate("analyzeTask/index.md");
|
||||
|
||||
const iterationTemplate = await loadPromptFromTemplate(
|
||||
"analyzeTask/iteration.md"
|
||||
);
|
||||
const iterationTemplate = loadPromptFromTemplate("analyzeTask/iteration.md");
|
||||
|
||||
let iterationPrompt = "";
|
||||
if (params.previousAnalysis) {
|
||||
|
@ -25,24 +25,20 @@ export interface ClearAllTasksPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getClearAllTasksPrompt(
|
||||
export function getClearAllTasksPrompt(
|
||||
params: ClearAllTasksPromptParams
|
||||
): Promise<string> {
|
||||
): string {
|
||||
const { confirm, success, message, backupFile, isEmpty } = params;
|
||||
|
||||
// 處理未確認的情況
|
||||
if (confirm === false) {
|
||||
const cancelTemplate = await loadPromptFromTemplate(
|
||||
"clearAllTasks/cancel.md"
|
||||
);
|
||||
const cancelTemplate = loadPromptFromTemplate("clearAllTasks/cancel.md");
|
||||
return generatePrompt(cancelTemplate, {});
|
||||
}
|
||||
|
||||
// 處理無任務需要清除的情況
|
||||
if (isEmpty) {
|
||||
const emptyTemplate = await loadPromptFromTemplate(
|
||||
"clearAllTasks/empty.md"
|
||||
);
|
||||
const emptyTemplate = loadPromptFromTemplate("clearAllTasks/empty.md");
|
||||
return generatePrompt(emptyTemplate, {});
|
||||
}
|
||||
|
||||
@ -51,15 +47,12 @@ export async function getClearAllTasksPrompt(
|
||||
|
||||
// 使用模板生成 backupInfo
|
||||
const backupInfo = backupFile
|
||||
? generatePrompt(
|
||||
await loadPromptFromTemplate("clearAllTasks/backupInfo.md"),
|
||||
{
|
||||
backupFile,
|
||||
}
|
||||
)
|
||||
? generatePrompt(loadPromptFromTemplate("clearAllTasks/backupInfo.md"), {
|
||||
backupFile,
|
||||
})
|
||||
: "";
|
||||
|
||||
const indexTemplate = await loadPromptFromTemplate("clearAllTasks/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("clearAllTasks/index.md");
|
||||
const prompt = generatePrompt(indexTemplate, {
|
||||
responseTitle,
|
||||
message,
|
||||
|
@ -23,12 +23,12 @@ export interface CompleteTaskPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getCompleteTaskPrompt(
|
||||
export function getCompleteTaskPrompt(
|
||||
params: CompleteTaskPromptParams
|
||||
): Promise<string> {
|
||||
): string {
|
||||
const { task, completionTime } = params;
|
||||
|
||||
const indexTemplate = await loadPromptFromTemplate("completeTask/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("completeTask/index.md");
|
||||
|
||||
// 開始構建基本 prompt
|
||||
let prompt = generatePrompt(indexTemplate, {
|
||||
|
@ -26,16 +26,12 @@ export interface DeleteTaskPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getDeleteTaskPrompt(
|
||||
params: DeleteTaskPromptParams
|
||||
): Promise<string> {
|
||||
export function getDeleteTaskPrompt(params: DeleteTaskPromptParams): string {
|
||||
const { taskId, task, success, message, isTaskCompleted } = params;
|
||||
|
||||
// 處理任務不存在的情況
|
||||
if (!task) {
|
||||
const notFoundTemplate = await loadPromptFromTemplate(
|
||||
"deleteTask/notFound.md"
|
||||
);
|
||||
const notFoundTemplate = loadPromptFromTemplate("deleteTask/notFound.md");
|
||||
return generatePrompt(notFoundTemplate, {
|
||||
taskId,
|
||||
});
|
||||
@ -43,9 +39,7 @@ export async function getDeleteTaskPrompt(
|
||||
|
||||
// 處理任務已完成的情況
|
||||
if (isTaskCompleted) {
|
||||
const completedTemplate = await loadPromptFromTemplate(
|
||||
"deleteTask/completed.md"
|
||||
);
|
||||
const completedTemplate = loadPromptFromTemplate("deleteTask/completed.md");
|
||||
return generatePrompt(completedTemplate, {
|
||||
taskId: task.id,
|
||||
taskName: task.name,
|
||||
@ -54,7 +48,7 @@ export async function getDeleteTaskPrompt(
|
||||
|
||||
// 處理刪除成功或失敗的情況
|
||||
const responseTitle = success ? "Success" : "Failure";
|
||||
const indexTemplate = await loadPromptFromTemplate("deleteTask/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("deleteTask/index.md");
|
||||
const prompt = generatePrompt(indexTemplate, {
|
||||
responseTitle,
|
||||
message,
|
||||
|
@ -55,13 +55,11 @@ function getComplexityStyle(level: string): string {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getExecuteTaskPrompt(
|
||||
params: ExecuteTaskPromptParams
|
||||
): Promise<string> {
|
||||
export function getExecuteTaskPrompt(params: ExecuteTaskPromptParams): string {
|
||||
const { task, complexityAssessment, relatedFilesSummary, dependencyTasks } =
|
||||
params;
|
||||
|
||||
const notesTemplate = await loadPromptFromTemplate("executeTask/notes.md");
|
||||
const notesTemplate = loadPromptFromTemplate("executeTask/notes.md");
|
||||
let notesPrompt = "";
|
||||
if (task.notes) {
|
||||
notesPrompt = generatePrompt(notesTemplate, {
|
||||
@ -69,7 +67,7 @@ export async function getExecuteTaskPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
const implementationGuideTemplate = await loadPromptFromTemplate(
|
||||
const implementationGuideTemplate = loadPromptFromTemplate(
|
||||
"executeTask/implementationGuide.md"
|
||||
);
|
||||
let implementationGuidePrompt = "";
|
||||
@ -79,7 +77,7 @@ export async function getExecuteTaskPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
const verificationCriteriaTemplate = await loadPromptFromTemplate(
|
||||
const verificationCriteriaTemplate = loadPromptFromTemplate(
|
||||
"executeTask/verificationCriteria.md"
|
||||
);
|
||||
let verificationCriteriaPrompt = "";
|
||||
@ -89,7 +87,7 @@ export async function getExecuteTaskPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
const analysisResultTemplate = await loadPromptFromTemplate(
|
||||
const analysisResultTemplate = loadPromptFromTemplate(
|
||||
"executeTask/analysisResult.md"
|
||||
);
|
||||
let analysisResultPrompt = "";
|
||||
@ -99,7 +97,7 @@ export async function getExecuteTaskPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
const dependencyTasksTemplate = await loadPromptFromTemplate(
|
||||
const dependencyTasksTemplate = loadPromptFromTemplate(
|
||||
"executeTask/dependencyTasks.md"
|
||||
);
|
||||
let dependencyTasksPrompt = "";
|
||||
@ -121,7 +119,7 @@ export async function getExecuteTaskPrompt(
|
||||
}
|
||||
}
|
||||
|
||||
const relatedFilesSummaryTemplate = await loadPromptFromTemplate(
|
||||
const relatedFilesSummaryTemplate = loadPromptFromTemplate(
|
||||
"executeTask/relatedFilesSummary.md"
|
||||
);
|
||||
let relatedFilesSummaryPrompt = "";
|
||||
@ -129,7 +127,7 @@ export async function getExecuteTaskPrompt(
|
||||
relatedFilesSummary: relatedFilesSummary || "當前任務沒有關聯的文件。",
|
||||
});
|
||||
|
||||
const complexityTemplate = await loadPromptFromTemplate(
|
||||
const complexityTemplate = loadPromptFromTemplate(
|
||||
"executeTask/complexity.md"
|
||||
);
|
||||
let complexityPrompt = "";
|
||||
@ -153,7 +151,7 @@ export async function getExecuteTaskPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
const indexTemplate = await loadPromptFromTemplate("executeTask/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("executeTask/index.md");
|
||||
let prompt = generatePrompt(indexTemplate, {
|
||||
name: task.name,
|
||||
id: task.id,
|
||||
|
@ -24,16 +24,14 @@ export interface GetTaskDetailPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getGetTaskDetailPrompt(
|
||||
export function getGetTaskDetailPrompt(
|
||||
params: GetTaskDetailPromptParams
|
||||
): Promise<string> {
|
||||
): string {
|
||||
const { taskId, task, error } = params;
|
||||
|
||||
// 如果有錯誤,顯示錯誤訊息
|
||||
if (error) {
|
||||
const errorTemplate = await loadPromptFromTemplate(
|
||||
"getTaskDetail/error.md"
|
||||
);
|
||||
const errorTemplate = loadPromptFromTemplate("getTaskDetail/error.md");
|
||||
return generatePrompt(errorTemplate, {
|
||||
errorMessage: error,
|
||||
});
|
||||
@ -41,7 +39,7 @@ export async function getGetTaskDetailPrompt(
|
||||
|
||||
// 如果找不到任務,顯示找不到任務的訊息
|
||||
if (!task) {
|
||||
const notFoundTemplate = await loadPromptFromTemplate(
|
||||
const notFoundTemplate = loadPromptFromTemplate(
|
||||
"getTaskDetail/notFound.md"
|
||||
);
|
||||
return generatePrompt(notFoundTemplate, {
|
||||
@ -51,9 +49,7 @@ export async function getGetTaskDetailPrompt(
|
||||
|
||||
let notesPrompt = "";
|
||||
if (task.notes) {
|
||||
const notesTemplate = await loadPromptFromTemplate(
|
||||
"getTaskDetail/notes.md"
|
||||
);
|
||||
const notesTemplate = loadPromptFromTemplate("getTaskDetail/notes.md");
|
||||
notesPrompt = generatePrompt(notesTemplate, {
|
||||
notes: task.notes,
|
||||
});
|
||||
@ -61,7 +57,7 @@ export async function getGetTaskDetailPrompt(
|
||||
|
||||
let dependenciesPrompt = "";
|
||||
if (task.dependencies && task.dependencies.length > 0) {
|
||||
const dependenciesTemplate = await loadPromptFromTemplate(
|
||||
const dependenciesTemplate = loadPromptFromTemplate(
|
||||
"getTaskDetail/dependencies.md"
|
||||
);
|
||||
dependenciesPrompt = generatePrompt(dependenciesTemplate, {
|
||||
@ -73,7 +69,7 @@ export async function getGetTaskDetailPrompt(
|
||||
|
||||
let implementationGuidePrompt = "";
|
||||
if (task.implementationGuide) {
|
||||
const implementationGuideTemplate = await loadPromptFromTemplate(
|
||||
const implementationGuideTemplate = loadPromptFromTemplate(
|
||||
"getTaskDetail/implementationGuide.md"
|
||||
);
|
||||
implementationGuidePrompt = generatePrompt(implementationGuideTemplate, {
|
||||
@ -83,7 +79,7 @@ export async function getGetTaskDetailPrompt(
|
||||
|
||||
let verificationCriteriaPrompt = "";
|
||||
if (task.verificationCriteria) {
|
||||
const verificationCriteriaTemplate = await loadPromptFromTemplate(
|
||||
const verificationCriteriaTemplate = loadPromptFromTemplate(
|
||||
"getTaskDetail/verificationCriteria.md"
|
||||
);
|
||||
verificationCriteriaPrompt = generatePrompt(verificationCriteriaTemplate, {
|
||||
@ -93,7 +89,7 @@ export async function getGetTaskDetailPrompt(
|
||||
|
||||
let relatedFilesPrompt = "";
|
||||
if (task.relatedFiles && task.relatedFiles.length > 0) {
|
||||
const relatedFilesTemplate = await loadPromptFromTemplate(
|
||||
const relatedFilesTemplate = loadPromptFromTemplate(
|
||||
"getTaskDetail/relatedFiles.md"
|
||||
);
|
||||
relatedFilesPrompt = generatePrompt(relatedFilesTemplate, {
|
||||
@ -110,7 +106,7 @@ export async function getGetTaskDetailPrompt(
|
||||
|
||||
let complatedSummaryPrompt = "";
|
||||
if (task.completedAt) {
|
||||
const complatedSummaryTemplate = await loadPromptFromTemplate(
|
||||
const complatedSummaryTemplate = loadPromptFromTemplate(
|
||||
"getTaskDetail/complatedSummary.md"
|
||||
);
|
||||
complatedSummaryPrompt = generatePrompt(complatedSummaryTemplate, {
|
||||
@ -119,7 +115,7 @@ export async function getGetTaskDetailPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
const indexTemplate = await loadPromptFromTemplate("getTaskDetail/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("getTaskDetail/index.md");
|
||||
|
||||
// 開始構建基本 prompt
|
||||
let prompt = generatePrompt(indexTemplate, {
|
||||
|
@ -16,12 +16,10 @@ export interface InitProjectRulesPromptParams {
|
||||
* @param params prompt 參數(可選)
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getInitProjectRulesPrompt(
|
||||
export function getInitProjectRulesPrompt(
|
||||
params?: InitProjectRulesPromptParams
|
||||
): Promise<string> {
|
||||
const indexTemplate = await loadPromptFromTemplate(
|
||||
"initProjectRules/index.md"
|
||||
);
|
||||
): string {
|
||||
const indexTemplate = loadPromptFromTemplate("initProjectRules/index.md");
|
||||
|
||||
// 載入可能的自定義 prompt (通過環境變數覆蓋或追加)
|
||||
return loadPrompt(indexTemplate, "INIT_PROJECT_RULES");
|
||||
|
@ -24,16 +24,12 @@ export interface ListTasksPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getListTasksPrompt(
|
||||
params: ListTasksPromptParams
|
||||
): Promise<string> {
|
||||
export function getListTasksPrompt(params: ListTasksPromptParams): string {
|
||||
const { status, tasks, allTasks } = params;
|
||||
|
||||
// 如果沒有任務,顯示通知
|
||||
if (allTasks.length === 0) {
|
||||
const notFoundTemplate = await loadPromptFromTemplate(
|
||||
"listTasks/notFound.md"
|
||||
);
|
||||
const notFoundTemplate = loadPromptFromTemplate("listTasks/notFound.md");
|
||||
const statusText = status === "all" ? "任何" : `任何 ${status} 的`;
|
||||
return generatePrompt(notFoundTemplate, {
|
||||
statusText: statusText,
|
||||
@ -62,9 +58,7 @@ export async function getListTasksPrompt(
|
||||
}
|
||||
|
||||
let taskDetails = "";
|
||||
let taskDetailsTemplate = await loadPromptFromTemplate(
|
||||
"listTasks/taskDetails.md"
|
||||
);
|
||||
let taskDetailsTemplate = loadPromptFromTemplate("listTasks/taskDetails.md");
|
||||
// 添加每個狀態下的詳細任務
|
||||
for (const statusType of Object.values(TaskStatus)) {
|
||||
const tasksWithStatus = tasks[statusType] || [];
|
||||
@ -94,7 +88,7 @@ export async function getListTasksPrompt(
|
||||
}
|
||||
}
|
||||
|
||||
const indexTemplate = await loadPromptFromTemplate("listTasks/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("listTasks/index.md");
|
||||
let prompt = generatePrompt(indexTemplate, {
|
||||
statusCount: statusCounts,
|
||||
taskDetailsTemplate: taskDetails,
|
||||
|
@ -27,9 +27,7 @@ export interface PlanTaskPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getPlanTaskPrompt(
|
||||
params: PlanTaskPromptParams
|
||||
): Promise<string> {
|
||||
export function getPlanTaskPrompt(params: PlanTaskPromptParams): string {
|
||||
let tasksContent = "";
|
||||
if (
|
||||
params.existingTasksReference &&
|
||||
@ -103,7 +101,7 @@ export async function getPlanTaskPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
const tasksTemplate = await loadPromptFromTemplate("planTask/tasks.md");
|
||||
const tasksTemplate = loadPromptFromTemplate("planTask/tasks.md");
|
||||
tasksContent = generatePrompt(tasksTemplate, {
|
||||
completedTasks: completeTasksContent,
|
||||
unfinishedTasks: unfinishedTasksContent,
|
||||
@ -113,11 +111,11 @@ export async function getPlanTaskPrompt(
|
||||
|
||||
let thoughtTemplate = "";
|
||||
if (process.env.ENABLE_THOUGHT_CHAIN !== "false") {
|
||||
thoughtTemplate = await loadPromptFromTemplate("planTask/hasThought.md");
|
||||
thoughtTemplate = loadPromptFromTemplate("planTask/hasThought.md");
|
||||
} else {
|
||||
thoughtTemplate = await loadPromptFromTemplate("planTask/noThought.md");
|
||||
thoughtTemplate = loadPromptFromTemplate("planTask/noThought.md");
|
||||
}
|
||||
const indexTemplate = await loadPromptFromTemplate("planTask/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("planTask/index.md");
|
||||
let prompt = generatePrompt(indexTemplate, {
|
||||
description: params.description,
|
||||
requirements: params.requirements || "No requirements",
|
||||
|
@ -15,21 +15,19 @@ export interface ProcessThoughtPromptParams {
|
||||
assumptions_challenged: string[];
|
||||
}
|
||||
|
||||
export async function getProcessThoughtPrompt(
|
||||
export function getProcessThoughtPrompt(
|
||||
param: ProcessThoughtPromptParams
|
||||
): Promise<string> {
|
||||
): string {
|
||||
let nextThoughtNeeded = "";
|
||||
if (param.nextThoughtNeeded) {
|
||||
nextThoughtNeeded = await loadPromptFromTemplate(
|
||||
"processThought/moreThought.md"
|
||||
);
|
||||
nextThoughtNeeded = loadPromptFromTemplate("processThought/moreThought.md");
|
||||
} else {
|
||||
nextThoughtNeeded = await loadPromptFromTemplate(
|
||||
nextThoughtNeeded = loadPromptFromTemplate(
|
||||
"processThought/complatedThought.md"
|
||||
);
|
||||
}
|
||||
|
||||
const indexTemplate = await loadPromptFromTemplate("processThought/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("processThought/index.md");
|
||||
|
||||
const prompt = generatePrompt(indexTemplate, {
|
||||
thought: param.thought,
|
||||
|
@ -28,21 +28,17 @@ export interface QueryTaskPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getQueryTaskPrompt(
|
||||
params: QueryTaskPromptParams
|
||||
): Promise<string> {
|
||||
export function getQueryTaskPrompt(params: QueryTaskPromptParams): string {
|
||||
const { query, isId, tasks, totalTasks, page, pageSize, totalPages } = params;
|
||||
|
||||
if (tasks.length === 0) {
|
||||
const notFoundTemplate = await loadPromptFromTemplate(
|
||||
"queryTask/notFound.md"
|
||||
);
|
||||
const notFoundTemplate = loadPromptFromTemplate("queryTask/notFound.md");
|
||||
return generatePrompt(notFoundTemplate, {
|
||||
query,
|
||||
});
|
||||
}
|
||||
|
||||
const taskDetailsTemplate = await loadPromptFromTemplate(
|
||||
const taskDetailsTemplate = loadPromptFromTemplate(
|
||||
"queryTask/taskDetails.md"
|
||||
);
|
||||
let tasksContent = "";
|
||||
@ -59,7 +55,7 @@ export async function getQueryTaskPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
const indexTemplate = await loadPromptFromTemplate("queryTask/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("queryTask/index.md");
|
||||
const prompt = generatePrompt(indexTemplate, {
|
||||
tasksContent,
|
||||
page,
|
||||
|
@ -22,10 +22,8 @@ export interface ReflectTaskPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getReflectTaskPrompt(
|
||||
params: ReflectTaskPromptParams
|
||||
): Promise<string> {
|
||||
const indexTemplate = await loadPromptFromTemplate("reflectTask/index.md");
|
||||
export function getReflectTaskPrompt(params: ReflectTaskPromptParams): string {
|
||||
const indexTemplate = loadPromptFromTemplate("reflectTask/index.md");
|
||||
const prompt = generatePrompt(indexTemplate, {
|
||||
summary: params.summary,
|
||||
analysis: params.analysis,
|
||||
|
@ -25,13 +25,13 @@ export interface ResearchModePromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getResearchModePrompt(
|
||||
export function getResearchModePrompt(
|
||||
params: ResearchModePromptParams
|
||||
): Promise<string> {
|
||||
): string {
|
||||
// 處理之前的研究狀態
|
||||
let previousStateContent = "";
|
||||
if (params.previousState && params.previousState.trim() !== "") {
|
||||
const previousStateTemplate = await loadPromptFromTemplate(
|
||||
const previousStateTemplate = loadPromptFromTemplate(
|
||||
"researchMode/previousState.md"
|
||||
);
|
||||
previousStateContent = generatePrompt(previousStateTemplate, {
|
||||
@ -42,7 +42,7 @@ export async function getResearchModePrompt(
|
||||
}
|
||||
|
||||
// 載入主要模板
|
||||
const indexTemplate = await loadPromptFromTemplate("researchMode/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("researchMode/index.md");
|
||||
let prompt = generatePrompt(indexTemplate, {
|
||||
topic: params.topic,
|
||||
previousStateContent: previousStateContent,
|
||||
|
@ -24,10 +24,8 @@ export interface SplitTasksPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getSplitTasksPrompt(
|
||||
params: SplitTasksPromptParams
|
||||
): Promise<string> {
|
||||
const taskDetailsTemplate = await loadPromptFromTemplate(
|
||||
export function getSplitTasksPrompt(params: SplitTasksPromptParams): string {
|
||||
const taskDetailsTemplate = loadPromptFromTemplate(
|
||||
"splitTasks/taskDetails.md"
|
||||
);
|
||||
|
||||
@ -74,7 +72,7 @@ export async function getSplitTasksPrompt(
|
||||
})
|
||||
.join("\n");
|
||||
|
||||
const indexTemplate = await loadPromptFromTemplate("splitTasks/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("splitTasks/index.md");
|
||||
const prompt = generatePrompt(indexTemplate, {
|
||||
updateMode: params.updateMode,
|
||||
tasksContent,
|
||||
|
@ -28,9 +28,9 @@ export interface UpdateTaskContentPromptParams {
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getUpdateTaskContentPrompt(
|
||||
export function getUpdateTaskContentPrompt(
|
||||
params: UpdateTaskContentPromptParams
|
||||
): Promise<string> {
|
||||
): string {
|
||||
const {
|
||||
taskId,
|
||||
task,
|
||||
@ -43,7 +43,7 @@ export async function getUpdateTaskContentPrompt(
|
||||
|
||||
// 處理任務不存在的情況
|
||||
if (!task) {
|
||||
const notFoundTemplate = await loadPromptFromTemplate(
|
||||
const notFoundTemplate = loadPromptFromTemplate(
|
||||
"updateTaskContent/notFound.md"
|
||||
);
|
||||
return generatePrompt(notFoundTemplate, {
|
||||
@ -53,7 +53,7 @@ export async function getUpdateTaskContentPrompt(
|
||||
|
||||
// 處理驗證錯誤的情況
|
||||
if (validationError) {
|
||||
const validationTemplate = await loadPromptFromTemplate(
|
||||
const validationTemplate = loadPromptFromTemplate(
|
||||
"updateTaskContent/validation.md"
|
||||
);
|
||||
return generatePrompt(validationTemplate, {
|
||||
@ -63,7 +63,7 @@ export async function getUpdateTaskContentPrompt(
|
||||
|
||||
// 處理空更新的情況
|
||||
if (emptyUpdate) {
|
||||
const emptyUpdateTemplate = await loadPromptFromTemplate(
|
||||
const emptyUpdateTemplate = loadPromptFromTemplate(
|
||||
"updateTaskContent/emptyUpdate.md"
|
||||
);
|
||||
return generatePrompt(emptyUpdateTemplate, {});
|
||||
@ -75,14 +75,14 @@ export async function getUpdateTaskContentPrompt(
|
||||
|
||||
// 更新成功且有更新後的任務詳情
|
||||
if (success && updatedTask) {
|
||||
const successTemplate = await loadPromptFromTemplate(
|
||||
const successTemplate = loadPromptFromTemplate(
|
||||
"updateTaskContent/success.md"
|
||||
);
|
||||
|
||||
// 編合相關文件信息
|
||||
let filesContent = "";
|
||||
if (updatedTask.relatedFiles && updatedTask.relatedFiles.length > 0) {
|
||||
const fileDetailsTemplate = await loadPromptFromTemplate(
|
||||
const fileDetailsTemplate = loadPromptFromTemplate(
|
||||
"updateTaskContent/fileDetails.md"
|
||||
);
|
||||
|
||||
@ -130,9 +130,7 @@ export async function getUpdateTaskContentPrompt(
|
||||
});
|
||||
}
|
||||
|
||||
const indexTemplate = await loadPromptFromTemplate(
|
||||
"updateTaskContent/index.md"
|
||||
);
|
||||
const indexTemplate = loadPromptFromTemplate("updateTaskContent/index.md");
|
||||
const prompt = generatePrompt(indexTemplate, {
|
||||
responseTitle,
|
||||
message: content,
|
||||
|
@ -44,12 +44,10 @@ function extractSummary(
|
||||
* @param params prompt 參數
|
||||
* @returns 生成的 prompt
|
||||
*/
|
||||
export async function getVerifyTaskPrompt(
|
||||
params: VerifyTaskPromptParams
|
||||
): Promise<string> {
|
||||
export function getVerifyTaskPrompt(params: VerifyTaskPromptParams): string {
|
||||
const { task, score, summary } = params;
|
||||
if (score < 80) {
|
||||
const noPassTemplate = await loadPromptFromTemplate("verifyTask/noPass.md");
|
||||
const noPassTemplate = loadPromptFromTemplate("verifyTask/noPass.md");
|
||||
const prompt = generatePrompt(noPassTemplate, {
|
||||
name: task.name,
|
||||
id: task.id,
|
||||
@ -57,7 +55,7 @@ export async function getVerifyTaskPrompt(
|
||||
});
|
||||
return prompt;
|
||||
}
|
||||
const indexTemplate = await loadPromptFromTemplate("verifyTask/index.md");
|
||||
const indexTemplate = loadPromptFromTemplate("verifyTask/index.md");
|
||||
const prompt = generatePrompt(indexTemplate, {
|
||||
name: task.name,
|
||||
id: task.id,
|
||||
|
@ -6,7 +6,6 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { getDataDir } from "../utils/paths.js";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
@ -80,22 +79,22 @@ export function generatePrompt(
|
||||
* @returns 模板內容
|
||||
* @throws Error 如果找不到模板文件
|
||||
*/
|
||||
export async function loadPromptFromTemplate(
|
||||
templatePath: string
|
||||
): Promise<string> {
|
||||
export function loadPromptFromTemplate(templatePath: string): string {
|
||||
const templateSetName = process.env.TEMPLATES_USE || "en";
|
||||
const dataDir = await getDataDir();
|
||||
const dataDir = process.env.DATA_DIR;
|
||||
const builtInTemplatesBaseDir = __dirname;
|
||||
|
||||
let finalPath = "";
|
||||
const checkedPaths: string[] = []; // 用於更詳細的錯誤報告
|
||||
|
||||
// 1. 檢查 DATA_DIR 中的自定義路徑
|
||||
// path.resolve 可以處理 templateSetName 是絕對路徑的情況
|
||||
const customFilePath = path.resolve(dataDir, templateSetName, templatePath);
|
||||
checkedPaths.push(`Custom: ${customFilePath}`);
|
||||
if (fs.existsSync(customFilePath)) {
|
||||
finalPath = customFilePath;
|
||||
if (dataDir) {
|
||||
// path.resolve 可以處理 templateSetName 是絕對路徑的情況
|
||||
const customFilePath = path.resolve(dataDir, templateSetName, templatePath);
|
||||
checkedPaths.push(`Custom: ${customFilePath}`);
|
||||
if (fs.existsSync(customFilePath)) {
|
||||
finalPath = customFilePath;
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 如果未找到自定義路徑,檢查特定的內建模板目錄
|
||||
|
@ -46,11 +46,6 @@
|
||||
<div class="dependency-view">
|
||||
<div class="panel-header">
|
||||
<h2 data-i18n-key="dependency_view_title">Dependency View</h2>
|
||||
<button id="reset-view-btn" class="reset-view-btn" title="Reset View" data-i18n-key="reset_view_btn_title">
|
||||
<svg viewBox="0 0 24 24" width="16" height="16">
|
||||
<path fill="currentColor" d="M12 5V2L8 6l4 4V7c3.31 0 6 2.69 6 6 0 2.97-2.17 5.43-5 5.91v2.02c3.95-.49 7-3.85 7-7.93 0-4.42-3.58-8-8-8zm-6 8c0-1.65.67-3.15 1.76-4.24L6.34 7.34C4.9 8.79 4 10.79 4 13c0 4.08 3.05 7.44 7 7.93v-2.02c-2.83-.48-5-2.94-5-5.91z"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div id="dependency-graph" class="dependency-graph">
|
||||
<p class="placeholder"></p>
|
||||
|
@ -3,7 +3,6 @@
|
||||
"status_indicator_online": "ONLINE",
|
||||
"dependency_view_title": "Dependency View",
|
||||
"dependency_graph_placeholder": "Dependency relationship for all tasks",
|
||||
"reset_view_btn_title": "Reset View",
|
||||
"task_list_title": "Task List",
|
||||
"search_placeholder": "Search tasks...",
|
||||
"sort_option_date_desc": "Creation Time (New-Old)",
|
||||
|
@ -3,7 +3,6 @@
|
||||
"status_indicator_online": "在線",
|
||||
"dependency_view_title": "依賴關係",
|
||||
"dependency_graph_placeholder": "所有任務的依賴關係",
|
||||
"reset_view_btn_title": "重置視圖",
|
||||
"task_list_title": "任務列表",
|
||||
"search_placeholder": "搜索任務...",
|
||||
"sort_option_date_desc": "創建時間 (新-舊)",
|
||||
|
@ -4,10 +4,7 @@ let selectedTaskId = null;
|
||||
let searchTerm = "";
|
||||
let sortOption = "date-asc";
|
||||
let globalAnalysisResult = null; // 新增:儲存全局分析結果
|
||||
let svg, g, simulation;
|
||||
let width, height; // << 新增:將寬高定義為全局變量
|
||||
let isGraphInitialized = false; // << 新增:追蹤圖表是否已初始化
|
||||
let zoom; // << 新增:保存縮放行為對象
|
||||
let svg, g, simulation; // << 修改:定義 D3 相關變量
|
||||
|
||||
// 新增:i18n 全局變量
|
||||
let currentLang = "en"; // 預設語言
|
||||
@ -28,7 +25,6 @@ const globalAnalysisResultElement = document.getElementById(
|
||||
"global-analysis-result"
|
||||
); // 假設 HTML 中有這個元素
|
||||
const langSwitcher = document.getElementById("lang-switcher"); // << 新增:獲取切換器元素
|
||||
const resetViewBtn = document.getElementById("reset-view-btn"); // << 新增:獲取重置按鈕元素
|
||||
|
||||
// 初始化
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
@ -36,7 +32,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
initI18n(); // << 新增:初始化 i18n
|
||||
updateCurrentTime();
|
||||
setInterval(updateCurrentTime, 1000);
|
||||
updateDimensions(); // << 新增:初始化時更新尺寸
|
||||
|
||||
// 事件監聽器
|
||||
// statusFilter.addEventListener("change", renderTasks); // 將由 changeLanguage 觸發或在 applyTranslations 後觸發
|
||||
@ -44,11 +39,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
statusFilter.addEventListener("change", renderTasks);
|
||||
}
|
||||
|
||||
// 新增:重置視圖按鈕事件監聽
|
||||
if (resetViewBtn) {
|
||||
resetViewBtn.addEventListener("click", resetView);
|
||||
}
|
||||
|
||||
// 新增:搜索和排序事件監聽
|
||||
const searchInput = document.getElementById("search-input");
|
||||
const sortOptions = document.getElementById("sort-options");
|
||||
@ -76,16 +66,6 @@ document.addEventListener("DOMContentLoaded", () => {
|
||||
changeLanguage(e.target.value)
|
||||
);
|
||||
}
|
||||
|
||||
// 新增:視窗大小改變時更新尺寸
|
||||
window.addEventListener("resize", () => {
|
||||
updateDimensions();
|
||||
if (svg && simulation) {
|
||||
svg.attr("viewBox", [0, 0, width, height]);
|
||||
simulation.force("center", d3.forceCenter(width / 2, height / 2));
|
||||
simulation.alpha(0.3).restart();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// 新增:i18n 核心函數
|
||||
@ -490,9 +470,6 @@ function renderTasks() {
|
||||
);
|
||||
}
|
||||
|
||||
// 儲存篩選後的任務 ID 集合,用於圖形渲染
|
||||
const filteredTaskIds = new Set(filteredTasks.map(task => task.id));
|
||||
|
||||
filteredTasks.sort((a, b) => {
|
||||
switch (sortOption) {
|
||||
case "name-asc":
|
||||
@ -510,9 +487,6 @@ function renderTasks() {
|
||||
}
|
||||
});
|
||||
|
||||
// 更新圖形的顯示狀態
|
||||
updateGraphVisibility(filteredTaskIds);
|
||||
|
||||
// --- 簡單粗暴的替換 (會導致閃爍) ---
|
||||
// TODO: 實現 DOM Diffing 或更智慧的更新策略
|
||||
if (filteredTasks.length === 0) {
|
||||
@ -565,77 +539,9 @@ function renderTasks() {
|
||||
}
|
||||
}
|
||||
|
||||
// 新增:更新图形可见性的函数
|
||||
function updateGraphVisibility(filteredTaskIds) {
|
||||
if (!g) return;
|
||||
|
||||
// 更新节点的样式
|
||||
g.select(".nodes")
|
||||
.selectAll("g.node-item")
|
||||
.style("opacity", d => filteredTaskIds.has(d.id) ? 1 : 0.2)
|
||||
.style("filter", d => filteredTaskIds.has(d.id) ? "none" : "grayscale(80%)");
|
||||
|
||||
// 更新连接的样式
|
||||
g.select(".links")
|
||||
.selectAll("line.link")
|
||||
.style("opacity", d => {
|
||||
const sourceVisible = filteredTaskIds.has(d.source.id || d.source);
|
||||
const targetVisible = filteredTaskIds.has(d.target.id || d.target);
|
||||
return (sourceVisible && targetVisible) ? 0.6 : 0.1;
|
||||
})
|
||||
.style("stroke", d => {
|
||||
const sourceVisible = filteredTaskIds.has(d.source.id || d.source);
|
||||
const targetVisible = filteredTaskIds.has(d.target.id || d.target);
|
||||
return (sourceVisible && targetVisible) ? "#999" : "#ccc";
|
||||
});
|
||||
|
||||
// 更新缩略图中的节点和连接样式
|
||||
const minimapContent = svg.select(".minimap-content");
|
||||
|
||||
minimapContent.selectAll(".minimap-node")
|
||||
.style("opacity", d => filteredTaskIds.has(d.id) ? 1 : 0.2)
|
||||
.style("filter", d => filteredTaskIds.has(d.id) ? "none" : "grayscale(80%)");
|
||||
|
||||
minimapContent.selectAll(".minimap-link")
|
||||
.style("opacity", d => {
|
||||
const sourceVisible = filteredTaskIds.has(d.source.id || d.source);
|
||||
const targetVisible = filteredTaskIds.has(d.target.id || d.target);
|
||||
return (sourceVisible && targetVisible) ? 0.6 : 0.1;
|
||||
})
|
||||
.style("stroke", d => {
|
||||
const sourceVisible = filteredTaskIds.has(d.source.id || d.source);
|
||||
const targetVisible = filteredTaskIds.has(d.target.id || d.target);
|
||||
return (sourceVisible && targetVisible) ? "#999" : "#ccc";
|
||||
});
|
||||
}
|
||||
|
||||
// 新增:将节点移动到视图中心的函数
|
||||
function centerNode(nodeId) {
|
||||
if (!svg || !g || !simulation) return;
|
||||
|
||||
const node = simulation.nodes().find(n => n.id === nodeId);
|
||||
if (!node) return;
|
||||
|
||||
// 获取当前视图的变换状态
|
||||
const transform = d3.zoomTransform(svg.node());
|
||||
|
||||
// 计算需要的变换以将节点居中
|
||||
const scale = transform.k; // 保持当前缩放级别
|
||||
const x = width / 2 - node.x * scale;
|
||||
const y = height / 2 - node.y * scale;
|
||||
|
||||
// 使用过渡动画平滑地移动到新位置
|
||||
svg.transition()
|
||||
.duration(750) // 750ms的过渡时间
|
||||
.call(zoom.transform, d3.zoomIdentity
|
||||
.translate(x, y)
|
||||
.scale(scale)
|
||||
);
|
||||
}
|
||||
|
||||
// 修改选择任务的函数
|
||||
// 選擇任務
|
||||
function selectTask(taskId) {
|
||||
// 清除旧的选中状态和高亮
|
||||
// 清除舊的選中狀態和高亮
|
||||
if (selectedTaskId) {
|
||||
const previousElement = document.querySelector(
|
||||
`.task-item[data-id="${selectedTaskId}"]`
|
||||
@ -645,7 +551,7 @@ function selectTask(taskId) {
|
||||
}
|
||||
}
|
||||
|
||||
// 如果再次点击同一个任务,则取消选中
|
||||
// 如果再次點擊同一個任務,則取消選中
|
||||
if (selectedTaskId === taskId) {
|
||||
selectedTaskId = null;
|
||||
taskDetailsContent.innerHTML = `<p class="placeholder">${translate(
|
||||
@ -657,7 +563,7 @@ function selectTask(taskId) {
|
||||
|
||||
selectedTaskId = taskId;
|
||||
|
||||
// 添加新的选中状态
|
||||
// 添加新的選中狀態
|
||||
const selectedElement = document.querySelector(
|
||||
`.task-item[data-id="${taskId}"]`
|
||||
);
|
||||
@ -665,7 +571,7 @@ function selectTask(taskId) {
|
||||
selectedElement.classList.add("selected");
|
||||
}
|
||||
|
||||
// 获取并显示任务详情
|
||||
// 獲取並顯示任務詳情
|
||||
const task = tasks.find((t) => t.id === taskId);
|
||||
|
||||
if (!task) {
|
||||
@ -831,59 +737,8 @@ function selectTask(taskId) {
|
||||
|
||||
// --- 原來的 innerHTML 賦值已移除 ---
|
||||
|
||||
// 高亮节点并将其移动到中心
|
||||
highlightNode(taskId);
|
||||
centerNode(taskId);
|
||||
}
|
||||
|
||||
// 新增:重置視圖功能
|
||||
function resetView() {
|
||||
if (!svg || !simulation) return;
|
||||
|
||||
// 添加重置動畫效果
|
||||
resetViewBtn.classList.add("resetting");
|
||||
|
||||
// 計算視圖中心
|
||||
const centerX = width / 2;
|
||||
const centerY = height / 2;
|
||||
|
||||
// 重置縮放和平移(使用 transform 過渡)
|
||||
svg.transition()
|
||||
.duration(750)
|
||||
.call(zoom.transform, d3.zoomIdentity);
|
||||
|
||||
// 重置所有節點位置到中心附近
|
||||
simulation.nodes().forEach(node => {
|
||||
node.x = centerX + (Math.random() - 0.5) * 50; // 在中心點附近隨機分佈
|
||||
node.y = centerY + (Math.random() - 0.5) * 50;
|
||||
node.fx = null; // 清除固定位置
|
||||
node.fy = null;
|
||||
});
|
||||
|
||||
// 重置力導向模擬
|
||||
simulation
|
||||
.force("center", d3.forceCenter(centerX, centerY))
|
||||
.alpha(1) // 完全重啟模擬
|
||||
.restart();
|
||||
|
||||
// 750ms 後移除動畫類
|
||||
setTimeout(() => {
|
||||
resetViewBtn.classList.remove("resetting");
|
||||
}, 750);
|
||||
}
|
||||
|
||||
// 新增:初始化縮放行為
|
||||
function initZoom() {
|
||||
zoom = d3.zoom()
|
||||
.scaleExtent([0.1, 4]) // 設置縮放範圍
|
||||
.on("zoom", (event) => {
|
||||
g.attr("transform", event.transform);
|
||||
updateMinimap(); // 在縮放時更新縮略圖
|
||||
});
|
||||
|
||||
if (svg) {
|
||||
svg.call(zoom);
|
||||
}
|
||||
// 只調用高亮函數
|
||||
highlightNode(taskId); // 只調用 highlightNode
|
||||
}
|
||||
|
||||
// 渲染依賴關係圖 - 修改為全局視圖和 enter/update/exit 模式
|
||||
@ -891,18 +746,22 @@ function renderDependencyGraph() {
|
||||
if (!dependencyGraphElement || !window.d3) {
|
||||
console.warn("D3 or dependency graph element not found.");
|
||||
if (dependencyGraphElement) {
|
||||
// 首次或D3丟失時顯示提示,不清空已有的圖
|
||||
if (!dependencyGraphElement.querySelector("svg")) {
|
||||
dependencyGraphElement.innerHTML = `<p class="placeholder">${translate("error_loading_graph_d3")}</p>`;
|
||||
dependencyGraphElement.innerHTML = `<p class="placeholder">${translate(
|
||||
"error_loading_graph_d3" // Use a specific key
|
||||
)}</p>`;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
updateDimensions();
|
||||
|
||||
// 如果沒有任務,清空圖表並顯示提示
|
||||
if (tasks.length === 0) {
|
||||
dependencyGraphElement.innerHTML = `<p class="placeholder">${translate("dependency_graph_placeholder_empty")}</p>`;
|
||||
dependencyGraphElement.innerHTML = `<p class="placeholder">${translate(
|
||||
"dependency_graph_placeholder_empty"
|
||||
)}</p>`;
|
||||
// 重置 SVG 和 simulation 變數,以便下次正確初始化
|
||||
svg = null;
|
||||
g = null;
|
||||
simulation = null;
|
||||
@ -914,9 +773,10 @@ function renderDependencyGraph() {
|
||||
id: task.id,
|
||||
name: task.name,
|
||||
status: task.status,
|
||||
// 保留現有位置以便平滑過渡
|
||||
x: simulation?.nodes().find((n) => n.id === task.id)?.x,
|
||||
y: simulation?.nodes().find((n) => n.id === task.id)?.y,
|
||||
fx: simulation?.nodes().find((n) => n.id === task.id)?.fx,
|
||||
fx: simulation?.nodes().find((n) => n.id === task.id)?.fx, // 保留固定位置
|
||||
fy: simulation?.nodes().find((n) => n.id === task.id)?.fy,
|
||||
}));
|
||||
|
||||
@ -926,56 +786,44 @@ function renderDependencyGraph() {
|
||||
task.dependencies.forEach((dep) => {
|
||||
const sourceId = typeof dep === "object" ? dep.taskId : dep;
|
||||
const targetId = task.id;
|
||||
if (nodes.some((n) => n.id === sourceId) && nodes.some((n) => n.id === targetId)) {
|
||||
if (
|
||||
nodes.some((n) => n.id === sourceId) &&
|
||||
nodes.some((n) => n.id === targetId)
|
||||
) {
|
||||
// 確保 link 的 source/target 是 ID,以便力導向識別
|
||||
links.push({ source: sourceId, target: targetId });
|
||||
} else {
|
||||
console.warn(`Dependency link ignored: Task ${sourceId} or ${targetId} not found in task list.`);
|
||||
console.warn(
|
||||
`Dependency link ignored: Task ${sourceId} or ${targetId} not found in task list.`
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// 2. D3 繪圖設置與更新
|
||||
const width = dependencyGraphElement.clientWidth;
|
||||
const height = dependencyGraphElement.clientHeight || 400;
|
||||
|
||||
if (!svg) {
|
||||
// --- 首次渲染 ---
|
||||
console.log("First render of dependency graph");
|
||||
dependencyGraphElement.innerHTML = "";
|
||||
dependencyGraphElement.innerHTML = ""; // 清空 placeholder
|
||||
|
||||
svg = d3.select(dependencyGraphElement)
|
||||
svg = d3
|
||||
.select(dependencyGraphElement)
|
||||
.append("svg")
|
||||
.attr("viewBox", [0, 0, width, height])
|
||||
.attr("preserveAspectRatio", "xMidYMid meet");
|
||||
|
||||
// 添加縮略圖背景
|
||||
const minimapSize = Math.min(width, height) * 0.2; // 縮略圖大小為主視圖的20%
|
||||
const minimapMargin = 40;
|
||||
|
||||
// 創建縮略圖容器
|
||||
const minimap = svg.append("g")
|
||||
.attr("class", "minimap")
|
||||
.attr("transform", `translate(${width - minimapSize - minimapMargin}, ${height - minimapSize - minimapMargin*(height/width)})`);
|
||||
g = svg.append("g"); // 主要組,用於縮放和平移
|
||||
|
||||
// 添加縮略圖背景
|
||||
minimap.append("rect")
|
||||
.attr("width", minimapSize)
|
||||
.attr("height", minimapSize)
|
||||
.attr("fill", "rgba(0, 0, 0, 0.2)")
|
||||
.attr("stroke", "#666")
|
||||
.attr("stroke-width", 1)
|
||||
.attr("rx", 4)
|
||||
.attr("ry", 4);
|
||||
|
||||
// 創建縮略圖內容組
|
||||
minimap.append("g")
|
||||
.attr("class", "minimap-content");
|
||||
|
||||
// 添加視口指示器
|
||||
minimap.append("rect")
|
||||
.attr("class", "minimap-viewport");
|
||||
|
||||
g = svg.append("g");
|
||||
|
||||
// 初始化並添加縮放行為
|
||||
initZoom();
|
||||
// 添加縮放和平移
|
||||
svg.call(
|
||||
d3.zoom().on("zoom", (event) => {
|
||||
g.attr("transform", event.transform);
|
||||
})
|
||||
);
|
||||
|
||||
// 添加箭頭定義
|
||||
g.append("defs")
|
||||
@ -992,82 +840,32 @@ function renderDependencyGraph() {
|
||||
.attr("fill", "#999");
|
||||
|
||||
// 初始化力導向模擬
|
||||
simulation = d3.forceSimulation()
|
||||
.force("link", d3.forceLink().id((d) => d.id).distance(100))
|
||||
simulation = d3
|
||||
.forceSimulation() // 初始化時不傳入 nodes
|
||||
.force(
|
||||
"link",
|
||||
d3
|
||||
.forceLink()
|
||||
.id((d) => d.id)
|
||||
.distance(100) // 指定 id 訪問器
|
||||
)
|
||||
.force("charge", d3.forceManyBody().strength(-300))
|
||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||
.force("collide", d3.forceCollide().radius(30))
|
||||
// 新增:水平分布力,用於優化節點在水平方向的分布,根據節點的入度和出度來決定節點的水平位置,入度為0的節點(起始節點)靠左,出度為0的節點(終止節點)靠右,其他節點則分布在中間位置
|
||||
.force("x", d3.forceX().x(d => {
|
||||
// 計算節點的入度和出度
|
||||
const inDegree = links.filter(l => (l.target.id || l.target) === d.id).length;
|
||||
const outDegree = links.filter(l => (l.source.id || l.source) === d.id).length;
|
||||
|
||||
if (inDegree === 0) {
|
||||
// 入度為0的節點(起始節點)靠左
|
||||
return width * 0.2;
|
||||
} else if (outDegree === 0) {
|
||||
// 出度為0的節點(終止節點)靠右
|
||||
return width * 0.8;
|
||||
} else {
|
||||
// 其他節點在中間
|
||||
return width * 0.5;
|
||||
}
|
||||
}).strength(0.2))
|
||||
// 新增:基于節點度數的垂直分布力
|
||||
.force("y", d3.forceY().y(height / 2).strength(d => {
|
||||
// 計算節點的總度數(入度+出度)
|
||||
const inDegree = links.filter(l => (l.target.id || l.target) === d.id).length;
|
||||
const outDegree = links.filter(l => (l.source.id || l.source) === d.id).length;
|
||||
const totalDegree = inDegree + outDegree;
|
||||
|
||||
// 度數越大,力越大(基礎力0.05,每個連接增加0.03,最大0.3)
|
||||
return Math.min(0.05 + totalDegree * 0.03, 0.3);
|
||||
}))
|
||||
.on("tick", ticked);
|
||||
.on("tick", ticked); // 綁定 tick 事件處理函數
|
||||
|
||||
// 添加用於存放連結和節點的組
|
||||
g.append("g").attr("class", "links");
|
||||
g.append("g").attr("class", "nodes");
|
||||
} else {
|
||||
// --- 更新圖表渲染 ---
|
||||
// --- 更新渲染 ---
|
||||
console.log("Updating dependency graph");
|
||||
// 更新 SVG 尺寸和中心力 (如果窗口大小改變)
|
||||
svg.attr("viewBox", [0, 0, width, height]);
|
||||
simulation.force("center", d3.forceCenter(width / 2, height / 2));
|
||||
}
|
||||
|
||||
// --- 預先運算穩定的節點位置 ---
|
||||
// 複製節點和連結以進行穩定化計算
|
||||
const stableNodes = [...nodes];
|
||||
const stableLinks = [...links];
|
||||
|
||||
// 暫時創建一個模擬器來計算穩定的位置
|
||||
const stableSim = d3
|
||||
.forceSimulation(stableNodes)
|
||||
.force("link", d3.forceLink(stableLinks).id(d => d.id).distance(100))
|
||||
.force("charge", d3.forceManyBody().strength(-300))
|
||||
.force("center", d3.forceCenter(width / 2, height / 2))
|
||||
.force("collide", d3.forceCollide().radius(30));
|
||||
|
||||
// 預熱模擬獲得穩定位置
|
||||
for (let i = 0; i < 10; i++) {
|
||||
stableSim.tick();
|
||||
}
|
||||
|
||||
// 將穩定位置複製回原始節點
|
||||
stableNodes.forEach((stableNode) => {
|
||||
const originalNode = nodes.find(n => n.id === stableNode.id);
|
||||
if (originalNode) {
|
||||
originalNode.x = stableNode.x;
|
||||
originalNode.y = stableNode.y;
|
||||
}
|
||||
});
|
||||
|
||||
// 停止臨時模擬器
|
||||
stableSim.stop();
|
||||
// --- 預先運算結束 ---
|
||||
|
||||
// 3. 更新連結 (無動畫)
|
||||
// 3. 更新連結
|
||||
const linkSelection = g
|
||||
.select(".links") // 選擇放置連結的 g 元素
|
||||
.selectAll("line.link")
|
||||
@ -1076,43 +874,62 @@ function renderDependencyGraph() {
|
||||
(d) => `${d.source.id || d.source}-${d.target.id || d.target}`
|
||||
); // Key function 基於 source/target ID
|
||||
|
||||
// Exit - 直接移除舊連結
|
||||
linkSelection.exit().remove();
|
||||
// Exit - 移除舊連結
|
||||
linkSelection
|
||||
.exit()
|
||||
.transition("exit")
|
||||
.duration(300)
|
||||
.attr("stroke-opacity", 0)
|
||||
.remove();
|
||||
|
||||
// Enter - 添加新連結 (無動畫)
|
||||
// Enter - 添加新連結
|
||||
const linkEnter = linkSelection
|
||||
.enter()
|
||||
.append("line")
|
||||
.attr("class", "link")
|
||||
.attr("stroke", "#999")
|
||||
.attr("marker-end", "url(#arrowhead)")
|
||||
.attr("stroke-opacity", 0); // 初始透明
|
||||
|
||||
// Update + Enter - 更新所有連結的屬性 (合併 enter 和 update 選擇集)
|
||||
const linkUpdate = linkSelection.merge(linkEnter);
|
||||
|
||||
linkUpdate
|
||||
.transition("update")
|
||||
.duration(500)
|
||||
.attr("stroke-opacity", 0.6)
|
||||
.attr("stroke-width", 1.5);
|
||||
|
||||
// 立即設置連結位置
|
||||
linkEnter
|
||||
.attr("x1", d => d.source.x || 0)
|
||||
.attr("y1", d => d.source.y || 0)
|
||||
.attr("x2", d => d.target.x || 0)
|
||||
.attr("y2", d => d.target.y || 0);
|
||||
|
||||
// 4. 更新節點 (無動畫)
|
||||
// 4. 更新節點
|
||||
const nodeSelection = g
|
||||
.select(".nodes") // 選擇放置節點的 g 元素
|
||||
.selectAll("g.node-item")
|
||||
.data(nodes, (d) => d.id); // 使用 ID 作為 key
|
||||
|
||||
// Exit - 直接移除舊節點
|
||||
nodeSelection.exit().remove();
|
||||
// Exit - 移除舊節點
|
||||
nodeSelection
|
||||
.exit()
|
||||
.transition("exit")
|
||||
.duration(300)
|
||||
.attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0}) scale(0)`) // 從當前位置縮放消失
|
||||
.attr("opacity", 0)
|
||||
.remove();
|
||||
|
||||
// Enter - 添加新節點組 (無動畫,直接在最終位置創建)
|
||||
// Enter - 添加新節點組
|
||||
const nodeEnter = nodeSelection
|
||||
.enter()
|
||||
.append("g")
|
||||
.attr("class", (d) => `node-item status-${getStatusClass(d.status)}`) // 使用輔助函數設置 class
|
||||
.attr("data-id", (d) => d.id)
|
||||
// 直接使用預計算的位置,無需縮放或透明度過渡
|
||||
.attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0})`)
|
||||
// 初始位置:從模擬計算的位置(如果存在)或隨機位置出現,初始縮放為0
|
||||
.attr(
|
||||
"transform",
|
||||
(d) =>
|
||||
`translate(${d.x || Math.random() * width}, ${
|
||||
d.y || Math.random() * height
|
||||
}) scale(0)`
|
||||
)
|
||||
.attr("opacity", 0)
|
||||
.call(drag(simulation)); // 添加拖拽
|
||||
|
||||
// 添加圓形到 Enter 選擇集
|
||||
@ -1120,8 +937,8 @@ function renderDependencyGraph() {
|
||||
.append("circle")
|
||||
.attr("r", 10)
|
||||
.attr("stroke", "#fff")
|
||||
.attr("stroke-width", 1.5)
|
||||
.attr("fill", getNodeColor); // 直接設置顏色
|
||||
.attr("stroke-width", 1.5);
|
||||
// 顏色將在 merge 後通過 update 過渡設置
|
||||
|
||||
// 添加文字到 Enter 選擇集
|
||||
nodeEnter
|
||||
@ -1143,14 +960,28 @@ function renderDependencyGraph() {
|
||||
event.stopPropagation();
|
||||
});
|
||||
|
||||
// Update - 立即更新現有節點 (無動畫)
|
||||
nodeSelection
|
||||
.attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0})`)
|
||||
.attr("class", (d) => `node-item status-${getStatusClass(d.status)}`);
|
||||
// Update + Enter - 合併並更新所有節點
|
||||
const nodeUpdate = nodeSelection.merge(nodeEnter);
|
||||
|
||||
nodeSelection
|
||||
// 過渡到最終位置和狀態
|
||||
nodeUpdate
|
||||
.transition("update")
|
||||
.duration(500)
|
||||
.attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0}) scale(1)`) // 移動到模擬位置並恢復大小
|
||||
.attr("opacity", 1);
|
||||
|
||||
// 更新節點顏色 (單獨過渡)
|
||||
nodeUpdate
|
||||
.select("circle")
|
||||
.attr("fill", getNodeColor);
|
||||
.transition("color")
|
||||
.duration(500)
|
||||
.attr("fill", getNodeColor); // 使用已有的 getNodeColor 函數
|
||||
|
||||
// 更新節點狀態 Class (即時更新,無需過渡)
|
||||
nodeUpdate.attr(
|
||||
"class",
|
||||
(d) => `node-item status-${getStatusClass(d.status)}`
|
||||
);
|
||||
|
||||
// << 新增:重新定義 drag 函數 >>
|
||||
function drag(simulation) {
|
||||
@ -1181,24 +1012,10 @@ function renderDependencyGraph() {
|
||||
}
|
||||
// << drag 函數定義結束 >>
|
||||
|
||||
// 5. 更新力導向模擬,但不啟動
|
||||
simulation.nodes(nodes); // 更新模擬節點
|
||||
// 5. 更新力導向模擬
|
||||
simulation.nodes(nodes); // 在處理完 enter/exit 後更新模擬節點
|
||||
simulation.force("link").links(links); // 更新模擬連結
|
||||
|
||||
// 更新水平分布力的目標位置
|
||||
simulation.force("x").x(d => {
|
||||
const inDegree = links.filter(l => (l.target.id || l.target) === d.id).length;
|
||||
const outDegree = links.filter(l => (l.source.id || l.source) === d.id).length;
|
||||
|
||||
if (inDegree === 0) {
|
||||
return width * 0.2;
|
||||
} else if (outDegree === 0) {
|
||||
return width * 0.8;
|
||||
} else {
|
||||
return width * 0.5;
|
||||
}
|
||||
});
|
||||
// 注意:移除了 restart() 調用,防止刷新時的動畫跳變
|
||||
simulation.alpha(0.3).restart(); // 重新激活模擬
|
||||
}
|
||||
|
||||
// Tick 函數: 更新節點和連結位置
|
||||
@ -1218,9 +1035,6 @@ function ticked() {
|
||||
.selectAll("g.node-item")
|
||||
// << 修改:添加座標後備值 >>
|
||||
.attr("transform", (d) => `translate(${d.x || 0}, ${d.y || 0})`);
|
||||
|
||||
// 更新縮略圖
|
||||
updateMinimap();
|
||||
}
|
||||
|
||||
// 函數:根據節點數據返回顏色 (示例)
|
||||
@ -1377,90 +1191,5 @@ function getStatusClass(status) {
|
||||
return status ? status.replace(/_/g, "-") : "unknown"; // 替換所有下劃線
|
||||
}
|
||||
|
||||
// 新增:更新寬高的函數
|
||||
function updateDimensions() {
|
||||
if (dependencyGraphElement) {
|
||||
width = dependencyGraphElement.clientWidth;
|
||||
height = dependencyGraphElement.clientHeight || 400;
|
||||
}
|
||||
}
|
||||
|
||||
// 添加縮略圖更新函數
|
||||
function updateMinimap() {
|
||||
if (!svg || !simulation) return;
|
||||
|
||||
const minimapSize = Math.min(width, height) * 0.2;
|
||||
const nodes = simulation.nodes();
|
||||
const links = simulation.force("link").links();
|
||||
|
||||
// 計算當前圖的邊界(添加padding)
|
||||
const padding = 20; // 添加內邊距
|
||||
const xExtent = d3.extent(nodes, d => d.x);
|
||||
const yExtent = d3.extent(nodes, d => d.y);
|
||||
const graphWidth = (xExtent[1] - xExtent[0]) || width;
|
||||
const graphHeight = (yExtent[1] - yExtent[0]) || height;
|
||||
|
||||
// 計算縮放比例,確保考慮padding
|
||||
const scale = Math.min(
|
||||
minimapSize / (graphWidth + padding * 2),
|
||||
minimapSize / (graphHeight + padding * 2)
|
||||
) * 0.9; // 0.9作為安全係數
|
||||
|
||||
// 創建縮放函數,加入padding
|
||||
const minimapX = d3.scaleLinear()
|
||||
.domain([xExtent[0] - padding, xExtent[1] + padding])
|
||||
.range([0, minimapSize]);
|
||||
const minimapY = d3.scaleLinear()
|
||||
.domain([yExtent[0] - padding, yExtent[1] + padding])
|
||||
.range([0, minimapSize]);
|
||||
|
||||
// 更新縮略圖中的連接
|
||||
const minimapContent = svg.select(".minimap-content");
|
||||
const minimapLinks = minimapContent.selectAll(".minimap-link")
|
||||
.data(links);
|
||||
|
||||
minimapLinks.enter()
|
||||
.append("line")
|
||||
.attr("class", "minimap-link")
|
||||
.merge(minimapLinks)
|
||||
.attr("x1", d => minimapX(d.source.x))
|
||||
.attr("y1", d => minimapY(d.source.y))
|
||||
.attr("x2", d => minimapX(d.target.x))
|
||||
.attr("y2", d => minimapY(d.target.y))
|
||||
.attr("stroke", "#999")
|
||||
.attr("stroke-width", 0.5)
|
||||
.attr("stroke-opacity", 0.6);
|
||||
|
||||
minimapLinks.exit().remove();
|
||||
|
||||
// 更新縮略圖中的節點
|
||||
const minimapNodes = minimapContent.selectAll(".minimap-node")
|
||||
.data(nodes);
|
||||
|
||||
minimapNodes.enter()
|
||||
.append("circle")
|
||||
.attr("class", "minimap-node")
|
||||
.attr("r", 2)
|
||||
.merge(minimapNodes)
|
||||
.attr("cx", d => minimapX(d.x))
|
||||
.attr("cy", d => minimapY(d.y))
|
||||
.attr("fill", getNodeColor);
|
||||
|
||||
minimapNodes.exit().remove();
|
||||
|
||||
// 更新視口指示器
|
||||
const transform = d3.zoomTransform(svg.node());
|
||||
const viewportWidth = width / transform.k;
|
||||
const viewportHeight = height / transform.k;
|
||||
const viewportX = -transform.x / transform.k;
|
||||
const viewportY = -transform.y / transform.k;
|
||||
|
||||
svg.select(".minimap-viewport")
|
||||
.attr("x", minimapX(viewportX))
|
||||
.attr("y", minimapY(viewportY))
|
||||
.attr("width", minimapX(viewportX + viewportWidth) - minimapX(viewportX))
|
||||
.attr("height", minimapY(viewportY + viewportHeight) - minimapY(viewportY));
|
||||
}
|
||||
|
||||
// 函數:啟用節點拖拽 (保持不變)
|
||||
// ... drag ...
|
||||
|
@ -123,32 +123,6 @@ main {
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
|
||||
}
|
||||
|
||||
.reset-view-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.reset-view-btn:hover {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
transform: rotate(-30deg);
|
||||
}
|
||||
|
||||
.reset-view-btn svg {
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.reset-view-btn.resetting svg {
|
||||
transform: rotate(-360deg);
|
||||
}
|
||||
|
||||
h2 {
|
||||
font-size: 1.2rem;
|
||||
font-weight: 500;
|
||||
@ -485,11 +459,3 @@ g.node-item.highlighted circle {
|
||||
flex-grow: 1; /* 讓搜索框佔據更多空間 */
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
/* 新增:缩略图视口指示器样式 */
|
||||
.minimap-viewport {
|
||||
fill: rgba(255, 255, 255, 0.025);
|
||||
stroke: rgba(255, 255, 255, 0.1);
|
||||
stroke-width: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
@ -11,7 +11,7 @@ export const initProjectRulesSchema = z.object({});
|
||||
export async function initProjectRules() {
|
||||
try {
|
||||
// 從生成器獲取提示詞
|
||||
const promptContent = await getInitProjectRulesPrompt();
|
||||
const promptContent = getInitProjectRulesPrompt();
|
||||
|
||||
// 返回成功響應
|
||||
return {
|
||||
|
@ -2,7 +2,6 @@ import { z } from "zod";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { getResearchModePrompt } from "../../prompts/index.js";
|
||||
import { getMemoryDir } from "../../utils/paths.js";
|
||||
|
||||
// 研究模式工具
|
||||
export const researchModeSchema = z.object({
|
||||
@ -41,10 +40,11 @@ export async function researchMode({
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "../../..");
|
||||
const MEMORY_DIR = await getMemoryDir();
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, "data");
|
||||
const MEMORY_DIR = path.join(DATA_DIR, "memory");
|
||||
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getResearchModePrompt({
|
||||
const prompt = getResearchModePrompt({
|
||||
topic,
|
||||
previousState,
|
||||
currentState,
|
||||
|
@ -32,7 +32,7 @@ export async function analyzeTask({
|
||||
previousAnalysis,
|
||||
}: z.infer<typeof analyzeTaskSchema>) {
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getAnalyzeTaskPrompt({
|
||||
const prompt = getAnalyzeTaskPrompt({
|
||||
summary,
|
||||
initialConcept,
|
||||
previousAnalysis,
|
||||
|
@ -25,7 +25,7 @@ export async function clearAllTasks({
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: await getClearAllTasksPrompt({ confirm: false }),
|
||||
text: getClearAllTasksPrompt({ confirm: false }),
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -38,7 +38,7 @@ export async function clearAllTasks({
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: await getClearAllTasksPrompt({ isEmpty: true }),
|
||||
text: getClearAllTasksPrompt({ isEmpty: true }),
|
||||
},
|
||||
],
|
||||
};
|
||||
@ -51,7 +51,7 @@ export async function clearAllTasks({
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: await getClearAllTasksPrompt({
|
||||
text: getClearAllTasksPrompt({
|
||||
success: result.success,
|
||||
message: result.message,
|
||||
backupFile: result.backupFile,
|
||||
|
@ -25,7 +25,7 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: await getDeleteTaskPrompt({ taskId }),
|
||||
text: getDeleteTaskPrompt({ taskId }),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
@ -37,11 +37,7 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: await getDeleteTaskPrompt({
|
||||
taskId,
|
||||
task,
|
||||
isTaskCompleted: true,
|
||||
}),
|
||||
text: getDeleteTaskPrompt({ taskId, task, isTaskCompleted: true }),
|
||||
},
|
||||
],
|
||||
isError: true,
|
||||
@ -54,7 +50,7 @@ export async function deleteTask({ taskId }: z.infer<typeof deleteTaskSchema>) {
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: await getDeleteTaskPrompt({
|
||||
text: getDeleteTaskPrompt({
|
||||
taskId,
|
||||
task,
|
||||
success: result.success,
|
||||
|
@ -126,7 +126,7 @@ export async function executeTask({
|
||||
}
|
||||
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getExecuteTaskPrompt({
|
||||
const prompt = getExecuteTaskPrompt({
|
||||
task,
|
||||
complexityAssessment,
|
||||
relatedFilesSummary,
|
||||
|
@ -38,7 +38,7 @@ export async function getTaskDetail({
|
||||
const task = result.tasks[0];
|
||||
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getGetTaskDetailPrompt({
|
||||
const prompt = getGetTaskDetailPrompt({
|
||||
taskId,
|
||||
task,
|
||||
});
|
||||
@ -53,7 +53,7 @@ export async function getTaskDetail({
|
||||
};
|
||||
} catch (error) {
|
||||
// 使用prompt生成器獲取錯誤訊息
|
||||
const errorPrompt = await getGetTaskDetailPrompt({
|
||||
const errorPrompt = getGetTaskDetailPrompt({
|
||||
taskId,
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
});
|
||||
|
@ -55,7 +55,7 @@ export async function listTasks({ status }: z.infer<typeof listTasksSchema>) {
|
||||
}, {} as Record<string, typeof tasks>);
|
||||
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getListTasksPrompt({
|
||||
const prompt = getListTasksPrompt({
|
||||
status,
|
||||
tasks: tasksByStatus,
|
||||
allTasks: filteredTasks,
|
||||
|
@ -4,7 +4,6 @@ import { fileURLToPath } from "url";
|
||||
import { getAllTasks } from "../../models/taskModel.js";
|
||||
import { TaskStatus, Task } from "../../types/index.js";
|
||||
import { getPlanTaskPrompt } from "../../prompts/index.js";
|
||||
import { getMemoryDir } from "../../utils/paths.js";
|
||||
|
||||
// 開始規劃工具
|
||||
export const planTaskSchema = z.object({
|
||||
@ -34,7 +33,8 @@ export async function planTask({
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
const PROJECT_ROOT = path.resolve(__dirname, "../../..");
|
||||
const MEMORY_DIR = await getMemoryDir();
|
||||
const DATA_DIR = process.env.DATA_DIR || path.join(PROJECT_ROOT, "data");
|
||||
const MEMORY_DIR = path.join(DATA_DIR, "memory");
|
||||
|
||||
// 準備所需參數
|
||||
let completedTasks: Task[] = [];
|
||||
@ -56,7 +56,7 @@ export async function planTask({
|
||||
}
|
||||
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getPlanTaskPrompt({
|
||||
const prompt = getPlanTaskPrompt({
|
||||
description,
|
||||
requirements,
|
||||
existingTasksReference,
|
||||
|
@ -44,7 +44,7 @@ export async function queryTask({
|
||||
const results = await searchTasksWithCommand(query, isId, page, pageSize);
|
||||
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getQueryTaskPrompt({
|
||||
const prompt = getQueryTaskPrompt({
|
||||
query,
|
||||
isId,
|
||||
tasks: results.tasks,
|
||||
|
@ -24,7 +24,7 @@ export async function reflectTask({
|
||||
analysis,
|
||||
}: z.infer<typeof reflectTaskSchema>) {
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getReflectTaskPrompt({
|
||||
const prompt = getReflectTaskPrompt({
|
||||
summary,
|
||||
analysis,
|
||||
});
|
||||
|
@ -209,7 +209,7 @@ export async function splitTasks({
|
||||
}
|
||||
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getSplitTasksPrompt({
|
||||
const prompt = getSplitTasksPrompt({
|
||||
updateMode,
|
||||
createdTasks,
|
||||
allTasks,
|
||||
|
@ -247,7 +247,7 @@ export async function splitTasksRaw({
|
||||
}
|
||||
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getSplitTasksPrompt({
|
||||
const prompt = getSplitTasksPrompt({
|
||||
updateMode,
|
||||
createdTasks,
|
||||
allTasks,
|
||||
|
@ -84,7 +84,7 @@ export async function updateTaskContent({
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: await getUpdateTaskContentPrompt({
|
||||
text: getUpdateTaskContentPrompt({
|
||||
taskId,
|
||||
validationError:
|
||||
"行號設置無效:必須同時設置起始行和結束行,且起始行必須小於結束行",
|
||||
@ -111,7 +111,7 @@ export async function updateTaskContent({
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: await getUpdateTaskContentPrompt({
|
||||
text: getUpdateTaskContentPrompt({
|
||||
taskId,
|
||||
emptyUpdate: true,
|
||||
}),
|
||||
@ -128,7 +128,7 @@ export async function updateTaskContent({
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: await getUpdateTaskContentPrompt({
|
||||
text: getUpdateTaskContentPrompt({
|
||||
taskId,
|
||||
}),
|
||||
},
|
||||
@ -164,7 +164,7 @@ export async function updateTaskContent({
|
||||
content: [
|
||||
{
|
||||
type: "text" as const,
|
||||
text: await getUpdateTaskContentPrompt({
|
||||
text: getUpdateTaskContentPrompt({
|
||||
taskId,
|
||||
task,
|
||||
success: result.success,
|
||||
|
@ -69,7 +69,7 @@ export async function verifyTask({
|
||||
}
|
||||
|
||||
// 使用prompt生成器獲取最終prompt
|
||||
const prompt = await getVerifyTaskPrompt({ task, score, summary });
|
||||
const prompt = getVerifyTaskPrompt({ task, score, summary });
|
||||
|
||||
return {
|
||||
content: [
|
||||
|
@ -74,7 +74,7 @@ export async function processThought(
|
||||
}
|
||||
|
||||
// 格式化思維輸出
|
||||
const formattedThought = await getProcessThoughtPrompt(thoughtData);
|
||||
const formattedThought = getProcessThoughtPrompt(thoughtData);
|
||||
|
||||
// 返回成功響應
|
||||
return {
|
||||
|
@ -1,116 +0,0 @@
|
||||
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;
|
||||
}
|
@ -1,153 +0,0 @@
|
||||
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;
|
||||
},
|
||||
};
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user