V2.0 重大升级:项目管理、协作规约、子任务、附件支持
新功能: 1. 项目管理 - 所有任务隶属于项目 2. 协作规约 - 每个项目可设置规约,智能体自动接收 3. 信息共享清单 - 项目级别的共享笔记 4. 任务增强: - Markdown 格式描述 - 附件上传支持(最大10MB) - 子任务/父任务关系 - 任务类型(需求/设计/开发/测试/部署) - 新状态流(初始/进行中/待测试/测试中/完成) 技术变更: - 新增 multer 依赖(文件上传) - 新增 4 张数据库表 - 完整的 API v2 文档 注意:需要新数据库或迁移旧数据
This commit is contained in:
@@ -0,0 +1,394 @@
|
||||
# API v2 升级说明
|
||||
|
||||
## 🎉 新功能概览
|
||||
|
||||
### 1. 项目管理
|
||||
- 所有任务必须隶属于项目
|
||||
- 项目由人类创建和管理
|
||||
|
||||
### 2. 协作规约
|
||||
- 每个项目可设置协作规约
|
||||
- 智能体领取任务时会收到规约提醒
|
||||
- 智能体必须严格遵守规约内容
|
||||
|
||||
### 3. 信息共享清单
|
||||
- 每个项目维护共享笔记列表
|
||||
- 人类和智能体都可以添加/编辑
|
||||
- 支持 CRUD 操作
|
||||
|
||||
### 4. 任务增强
|
||||
- **Markdown 支持**: 任务描述使用 Markdown 格式
|
||||
- **附件管理**: 支持文件上传(最大 10MB)
|
||||
- **子任务**: 支持父子任务关系
|
||||
- **任务类型**: requirement(需求)| design(设计)| development(开发)| testing(测试)| deployment(部署)
|
||||
- **新状态流**: initial(初始)→ in_progress(进行中)→ testing_pending(待测试)→ testing(测试中)→ completed(完成)
|
||||
|
||||
## 📡 API 端点
|
||||
|
||||
### 项目管理
|
||||
|
||||
#### 创建项目
|
||||
```http
|
||||
POST /api/projects
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "项目名称",
|
||||
"description": "项目描述",
|
||||
"created_by": "human"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取所有项目
|
||||
```http
|
||||
GET /api/projects
|
||||
```
|
||||
|
||||
#### 获取项目详情
|
||||
```http
|
||||
GET /api/projects/:id
|
||||
```
|
||||
|
||||
#### 更新项目
|
||||
```http
|
||||
PUT /api/projects/:id
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"name": "新名称",
|
||||
"description": "新描述",
|
||||
"status": "active" | "archived"
|
||||
}
|
||||
```
|
||||
|
||||
### 协作规约
|
||||
|
||||
#### 设置项目协作规约
|
||||
```http
|
||||
POST /api/projects/:projectId/rules
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"content": "协作规约内容(Markdown)",
|
||||
"created_by": "human"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取协作规约
|
||||
```http
|
||||
GET /api/projects/:projectId/rules
|
||||
```
|
||||
|
||||
### 信息共享清单
|
||||
|
||||
#### 添加共享笔记
|
||||
```http
|
||||
POST /api/projects/:projectId/notes
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"content": "笔记内容(Markdown)",
|
||||
"created_by": "human" | "agent_id"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取共享笔记列表
|
||||
```http
|
||||
GET /api/projects/:projectId/notes
|
||||
```
|
||||
|
||||
#### 更新共享笔记
|
||||
```http
|
||||
PUT /api/projects/:projectId/notes/:noteId
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"content": "更新后的内容"
|
||||
}
|
||||
```
|
||||
|
||||
#### 删除共享笔记
|
||||
```http
|
||||
DELETE /api/projects/:projectId/notes/:noteId
|
||||
```
|
||||
|
||||
### 任务管理(增强)
|
||||
|
||||
#### 创建任务
|
||||
```http
|
||||
POST /api/tasks
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"project_id": "项目ID", // 必填
|
||||
"parent_task_id": "父任务ID", // 可选,创建子任务时使用
|
||||
"title": "任务标题",
|
||||
"description": "# 任务描述\n\n使用 **Markdown** 格式",
|
||||
"task_type": "development", // requirement | design | development | testing | deployment
|
||||
"priority": "normal", // low | normal | high
|
||||
"created_by": "human"
|
||||
}
|
||||
```
|
||||
|
||||
#### 获取任务列表
|
||||
```http
|
||||
GET /api/tasks?project_id=xxx&status=initial&root=true
|
||||
|
||||
参数:
|
||||
- project_id: 按项目过滤
|
||||
- status: initial | in_progress | testing_pending | testing | completed
|
||||
- parent_task_id: 获取某任务的子任务
|
||||
- root=true: 只获取根任务(无父任务)
|
||||
```
|
||||
|
||||
#### 获取任务详情(含子任务和附件)
|
||||
```http
|
||||
GET /api/tasks/:id
|
||||
|
||||
响应:
|
||||
{
|
||||
"id": "xxx",
|
||||
"title": "...",
|
||||
...
|
||||
"subtasks": [ ... ], // 子任务列表
|
||||
"attachments": [ ... ] // 附件列表
|
||||
}
|
||||
```
|
||||
|
||||
#### 领取任务(智能体会收到协作规约)
|
||||
```http
|
||||
POST /api/tasks/:id/claim
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"agent_id": "智能体ID"
|
||||
}
|
||||
|
||||
响应(如果项目有协作规约):
|
||||
{
|
||||
"message": "任务领取成功",
|
||||
"collaboration_rules": "项目协作规约内容",
|
||||
"notice": "⚠️ 请严格遵守项目协作规约"
|
||||
}
|
||||
```
|
||||
|
||||
#### 更新任务
|
||||
```http
|
||||
PUT /api/tasks/:id
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"status": "in_progress",
|
||||
"description": "更新描述",
|
||||
"task_type": "testing",
|
||||
"priority": "high",
|
||||
"result": "任务结果"
|
||||
}
|
||||
```
|
||||
|
||||
### 附件管理
|
||||
|
||||
#### 上传附件
|
||||
```http
|
||||
POST /api/tasks/:taskId/attachments
|
||||
Content-Type: multipart/form-data
|
||||
|
||||
file: [文件]
|
||||
uploaded_by: "human" | "agent_id"
|
||||
|
||||
限制:单文件最大 10MB
|
||||
```
|
||||
|
||||
#### 获取任务附件列表
|
||||
```http
|
||||
GET /api/tasks/:taskId/attachments
|
||||
|
||||
响应:
|
||||
[
|
||||
{
|
||||
"id": "xxx",
|
||||
"filename": "...",
|
||||
"original_name": "原始文件名",
|
||||
"file_size": 12345,
|
||||
"mime_type": "image/png",
|
||||
"url": "/uploads/xxx",
|
||||
...
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
#### 删除附件
|
||||
```http
|
||||
DELETE /api/tasks/:taskId/attachments/:attachmentId
|
||||
```
|
||||
|
||||
### 统计信息(增强)
|
||||
|
||||
```http
|
||||
GET /api/stats
|
||||
|
||||
响应:
|
||||
{
|
||||
"total_agents": 5,
|
||||
"active_agents": 2,
|
||||
"total_projects": 3,
|
||||
"total_tasks": 20,
|
||||
"tasks_by_status": {
|
||||
"initial": 5,
|
||||
"in_progress": 3,
|
||||
"testing_pending": 2,
|
||||
"testing": 1,
|
||||
"completed": 9
|
||||
},
|
||||
"tasks_by_type": {
|
||||
"requirement": 4,
|
||||
"design": 3,
|
||||
"development": 8,
|
||||
"testing": 3,
|
||||
"deployment": 2
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🗄️ 数据库变更
|
||||
|
||||
### 新增表
|
||||
|
||||
1. **projects** - 项目表
|
||||
- id, name, description, created_by, created_at, updated_at, status
|
||||
|
||||
2. **collaboration_rules** - 协作规约表
|
||||
- id, project_id, content, created_by, created_at, updated_at
|
||||
|
||||
3. **project_notes** - 项目共享笔记表
|
||||
- id, project_id, content, created_by, created_at, updated_at
|
||||
|
||||
4. **attachments** - 附件表
|
||||
- id, task_id, filename, original_name, file_path, file_size, mime_type, uploaded_by, uploaded_at
|
||||
|
||||
### 修改表
|
||||
|
||||
**tasks** 表新增字段:
|
||||
- `project_id` - 所属项目(外键,必填)
|
||||
- `parent_task_id` - 父任务ID(外键,可选)
|
||||
- `task_type` - 任务类型(requirement/design/development/testing/deployment)
|
||||
- 状态值更新为:initial | in_progress | testing_pending | testing | completed
|
||||
|
||||
## 🔧 使用示例
|
||||
|
||||
### Python 客户端示例
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
API_BASE = "http://localhost:3000/api"
|
||||
|
||||
# 1. 创建项目
|
||||
project = requests.post(f"{API_BASE}/projects", json={
|
||||
"name": "智能客服系统",
|
||||
"description": "基于 AI 的客服系统开发项目"
|
||||
}).json()
|
||||
|
||||
project_id = project['id']
|
||||
|
||||
# 2. 设置协作规约
|
||||
requests.post(f"{API_BASE}/projects/{project_id}/rules", json={
|
||||
"content": """
|
||||
# 协作规约
|
||||
|
||||
1. **代码规范**: 严格遵循 PEP 8
|
||||
2. **提交规范**: 每次提交必须包含测试
|
||||
3. **沟通规范**: 遇到问题及时在共享清单记录
|
||||
4. **测试要求**: 覆盖率不低于 80%
|
||||
"""
|
||||
})
|
||||
|
||||
# 3. 创建任务
|
||||
task = requests.post(f"{API_BASE}/tasks", json={
|
||||
"project_id": project_id,
|
||||
"title": "设计数据库架构",
|
||||
"description": "## 需求\n\n设计用户、会话、消息三张表",
|
||||
"task_type": "design",
|
||||
"priority": "high"
|
||||
}).json()
|
||||
|
||||
task_id = task['id']
|
||||
|
||||
# 4. 智能体注册并领取任务
|
||||
agent = requests.post(f"{API_BASE}/agents/register", json={
|
||||
"name": "数据库设计专家",
|
||||
"type": "design",
|
||||
"capabilities": ["database_design", "sql"]
|
||||
}).json()
|
||||
|
||||
response = requests.post(f"{API_BASE}/tasks/{task_id}/claim", json={
|
||||
"agent_id": agent['id']
|
||||
}).json()
|
||||
|
||||
# 智能体会收到协作规约
|
||||
if 'collaboration_rules' in response:
|
||||
print("收到协作规约:")
|
||||
print(response['collaboration_rules'])
|
||||
|
||||
# 5. 添加共享笔记
|
||||
requests.post(f"{API_BASE}/projects/{project_id}/notes", json={
|
||||
"content": "数据库已选择 PostgreSQL 15",
|
||||
"created_by": agent['id']
|
||||
})
|
||||
|
||||
# 6. 上传附件(设计图)
|
||||
with open('database_schema.png', 'rb') as f:
|
||||
requests.post(
|
||||
f"{API_BASE}/tasks/{task_id}/attachments",
|
||||
files={'file': f},
|
||||
data={'uploaded_by': agent['id']}
|
||||
)
|
||||
|
||||
# 7. 完成任务
|
||||
requests.put(f"{API_BASE}/tasks/{task_id}", json={
|
||||
"status": "completed",
|
||||
"result": "数据库架构设计完成,详见附件"
|
||||
})
|
||||
```
|
||||
|
||||
## 📝 迁移指南
|
||||
|
||||
如果你有使用 v1 版本的数据,需要:
|
||||
|
||||
1. **备份数据库**
|
||||
```bash
|
||||
pg_dump agent_tasks > backup.sql
|
||||
```
|
||||
|
||||
2. **运行 v2 服务**(会自动创建新表)
|
||||
```bash
|
||||
node server-v2.js
|
||||
```
|
||||
|
||||
3. **手动迁移任务**
|
||||
- 创建一个默认项目
|
||||
- 将所有 v1 任务关联到该项目
|
||||
|
||||
## 🚀 部署
|
||||
|
||||
使用 `server-v2.js` 替代 `server-postgres.js`:
|
||||
|
||||
```bash
|
||||
DB_HOST=localhost DB_PORT=5433 DB_PASSWORD=postgres node server-v2.js
|
||||
```
|
||||
|
||||
或更新 `package.json`:
|
||||
```json
|
||||
{
|
||||
"scripts": {
|
||||
"start": "node server-v2.js"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ⚠️ 重要变更
|
||||
|
||||
1. **所有任务必须属于项目** - 创建任务时 `project_id` 为必填
|
||||
2. **任务状态变更** - 从 3 个状态扩展到 5 个状态
|
||||
3. **新增任务类型** - 必须指定任务类型
|
||||
4. **协作规约强制推送** - 智能体领取任务时自动获取规约
|
||||
Generated
+126
@@ -12,6 +12,7 @@
|
||||
"body-parser": "^2.2.2",
|
||||
"cors": "^2.8.6",
|
||||
"express": "^5.2.1",
|
||||
"multer": "^2.0.2",
|
||||
"pg": "^8.18.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"uuid": "^9.0.1"
|
||||
@@ -130,6 +131,12 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/append-field": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
|
||||
"integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/aproba": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz",
|
||||
@@ -258,6 +265,23 @@
|
||||
"ieee754": "^1.1.13"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/busboy": {
|
||||
"version": "1.6.0",
|
||||
"resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
|
||||
"integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
|
||||
"dependencies": {
|
||||
"streamsearch": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bytes": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
|
||||
@@ -362,6 +386,21 @@
|
||||
"license": "MIT",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/concat-stream": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
|
||||
"integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
|
||||
"engines": [
|
||||
"node >= 6.0"
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.0.2",
|
||||
"typedarray": "^0.0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/console-control-strings": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz",
|
||||
@@ -1350,6 +1389,79 @@
|
||||
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/multer": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
|
||||
"integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"append-field": "^1.0.0",
|
||||
"busboy": "^1.6.0",
|
||||
"concat-stream": "^2.0.0",
|
||||
"mkdirp": "^0.5.6",
|
||||
"object-assign": "^4.1.1",
|
||||
"type-is": "^1.6.18",
|
||||
"xtend": "^4.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.16.0"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/media-typer": {
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
|
||||
"integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mime-db": {
|
||||
"version": "1.52.0",
|
||||
"resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
|
||||
"integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mime-types": {
|
||||
"version": "2.1.35",
|
||||
"resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
|
||||
"integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mime-db": "1.52.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/mkdirp": {
|
||||
"version": "0.5.6",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
|
||||
"integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"minimist": "^1.2.6"
|
||||
},
|
||||
"bin": {
|
||||
"mkdirp": "bin/cmd.js"
|
||||
}
|
||||
},
|
||||
"node_modules/multer/node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
"integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"media-typer": "0.3.0",
|
||||
"mime-types": "~2.1.24"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
@@ -2153,6 +2265,14 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/streamsearch": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
|
||||
"integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/string_decoder": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
|
||||
@@ -2295,6 +2415,12 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/typedarray": {
|
||||
"version": "0.0.6",
|
||||
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
|
||||
"integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/unique-filename": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz",
|
||||
|
||||
+6
-1
@@ -8,13 +8,18 @@
|
||||
"dev": "node server-postgres.js",
|
||||
"sqlite": "node server.js"
|
||||
},
|
||||
"keywords": ["multi-agent", "task-management", "ai"],
|
||||
"keywords": [
|
||||
"multi-agent",
|
||||
"task-management",
|
||||
"ai"
|
||||
],
|
||||
"author": "work-1",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"body-parser": "^2.2.2",
|
||||
"cors": "^2.8.6",
|
||||
"express": "^5.2.1",
|
||||
"multer": "^2.0.2",
|
||||
"pg": "^8.18.0",
|
||||
"sqlite3": "^5.1.7",
|
||||
"uuid": "^9.0.1"
|
||||
|
||||
+861
@@ -0,0 +1,861 @@
|
||||
const express = require('express');
|
||||
const { Pool } = require('pg');
|
||||
const cors = require('cors');
|
||||
const bodyParser = require('body-parser');
|
||||
const { v4: uuidv4 } = require('uuid');
|
||||
const os = require('os');
|
||||
const multer = require('multer');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
|
||||
// 中间件
|
||||
app.use(cors());
|
||||
app.use(bodyParser.json({ limit: '50mb' }));
|
||||
app.use(express.static('public'));
|
||||
app.use('/uploads', express.static('uploads'));
|
||||
|
||||
// 配置文件上传
|
||||
const storage = multer.diskStorage({
|
||||
destination: (req, file, cb) => {
|
||||
const dir = 'uploads/';
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
cb(null, dir);
|
||||
},
|
||||
filename: (req, file, cb) => {
|
||||
const uniqueName = `${Date.now()}-${Math.round(Math.random() * 1E9)}${path.extname(file.originalname)}`;
|
||||
cb(null, uniqueName);
|
||||
}
|
||||
});
|
||||
|
||||
const upload = multer({
|
||||
storage,
|
||||
limits: { fileSize: 10 * 1024 * 1024 } // 10MB
|
||||
});
|
||||
|
||||
// PostgreSQL 连接池
|
||||
const pool = new Pool({
|
||||
host: process.env.DB_HOST || 'postgres',
|
||||
port: process.env.DB_PORT || 5432,
|
||||
database: process.env.DB_NAME || 'agent_tasks',
|
||||
user: process.env.DB_USER || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
max: 20,
|
||||
idleTimeoutMillis: 30000,
|
||||
connectionTimeoutMillis: 2000,
|
||||
});
|
||||
|
||||
// 测试数据库连接
|
||||
pool.connect((err, client, release) => {
|
||||
if (err) {
|
||||
console.error('❌ 数据库连接失败:', err.message);
|
||||
process.exit(1);
|
||||
} else {
|
||||
console.log('✅ 已连接到 PostgreSQL 数据库');
|
||||
release();
|
||||
initDatabase();
|
||||
}
|
||||
});
|
||||
|
||||
// 创建表结构
|
||||
async function initDatabase() {
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
// 智能体表
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS agents (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(100),
|
||||
capabilities JSONB,
|
||||
status VARCHAR(50) DEFAULT 'idle',
|
||||
registered_at BIGINT,
|
||||
last_heartbeat BIGINT
|
||||
)
|
||||
`);
|
||||
|
||||
// 项目表
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
name VARCHAR(500) NOT NULL,
|
||||
description TEXT,
|
||||
created_by VARCHAR(255),
|
||||
created_at BIGINT,
|
||||
updated_at BIGINT,
|
||||
status VARCHAR(50) DEFAULT 'active'
|
||||
)
|
||||
`);
|
||||
|
||||
// 协作规约表
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS collaboration_rules (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
project_id VARCHAR(255) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
created_by VARCHAR(255),
|
||||
created_at BIGINT,
|
||||
updated_at BIGINT
|
||||
)
|
||||
`);
|
||||
|
||||
// 项目信息共享清单表
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS project_notes (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
project_id VARCHAR(255) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
content TEXT NOT NULL,
|
||||
created_by VARCHAR(255),
|
||||
created_at BIGINT,
|
||||
updated_at BIGINT
|
||||
)
|
||||
`);
|
||||
|
||||
// 任务表(扩展)
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS tasks (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
project_id VARCHAR(255) REFERENCES projects(id) ON DELETE CASCADE,
|
||||
parent_task_id VARCHAR(255) REFERENCES tasks(id) ON DELETE SET NULL,
|
||||
title VARCHAR(500) NOT NULL,
|
||||
description TEXT,
|
||||
task_type VARCHAR(50) DEFAULT 'development',
|
||||
priority VARCHAR(50) DEFAULT 'normal',
|
||||
status VARCHAR(50) DEFAULT 'initial',
|
||||
created_by VARCHAR(255),
|
||||
assigned_to VARCHAR(255),
|
||||
created_at BIGINT,
|
||||
updated_at BIGINT,
|
||||
completed_at BIGINT,
|
||||
result TEXT
|
||||
)
|
||||
`);
|
||||
|
||||
// 附件表
|
||||
await client.query(`
|
||||
CREATE TABLE IF NOT EXISTS attachments (
|
||||
id VARCHAR(255) PRIMARY KEY,
|
||||
task_id VARCHAR(255) REFERENCES tasks(id) ON DELETE CASCADE,
|
||||
filename VARCHAR(500) NOT NULL,
|
||||
original_name VARCHAR(500) NOT NULL,
|
||||
file_path VARCHAR(1000) NOT NULL,
|
||||
file_size BIGINT,
|
||||
mime_type VARCHAR(100),
|
||||
uploaded_by VARCHAR(255),
|
||||
uploaded_at BIGINT
|
||||
)
|
||||
`);
|
||||
|
||||
// 创建索引
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_agents_status ON agents(status)`);
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_tasks_status ON tasks(status)`);
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_tasks_project ON tasks(project_id)`);
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_tasks_parent ON tasks(parent_task_id)`);
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_tasks_assigned ON tasks(assigned_to)`);
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_attachments_task ON attachments(task_id)`);
|
||||
await client.query(`CREATE INDEX IF NOT EXISTS idx_project_notes_project ON project_notes(project_id)`);
|
||||
|
||||
await client.query('COMMIT');
|
||||
console.log('✅ 数据库表已初始化');
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
console.error('❌ 数据库初始化失败:', err);
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== 项目管理 API ====================
|
||||
|
||||
// 创建项目
|
||||
app.post('/api/projects', async (req, res) => {
|
||||
const { name, description, created_by } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: '项目名称不能为空' });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
const now = Date.now();
|
||||
|
||||
try {
|
||||
await pool.query(
|
||||
`INSERT INTO projects (id, name, description, created_by, created_at, updated_at, status)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, 'active')`,
|
||||
[id, name, description || '', created_by || 'human', now, now]
|
||||
);
|
||||
|
||||
res.json({
|
||||
id,
|
||||
name,
|
||||
status: 'active',
|
||||
message: '项目创建成功'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有项目
|
||||
app.get('/api/projects', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM projects ORDER BY created_at DESC');
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取单个项目详情
|
||||
app.get('/api/projects/:id', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM projects WHERE id = $1', [req.params.id]);
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: '项目不存在' });
|
||||
}
|
||||
res.json(result.rows[0]);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新项目
|
||||
app.put('/api/projects/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { name, description, status } = req.body;
|
||||
|
||||
const updates = [];
|
||||
const params = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (name) {
|
||||
updates.push(`name = $${paramIndex++}`);
|
||||
params.push(name);
|
||||
}
|
||||
if (description !== undefined) {
|
||||
updates.push(`description = $${paramIndex++}`);
|
||||
params.push(description);
|
||||
}
|
||||
if (status) {
|
||||
updates.push(`status = $${paramIndex++}`);
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
updates.push(`updated_at = $${paramIndex++}`);
|
||||
params.push(Date.now());
|
||||
params.push(id);
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
`UPDATE projects SET ${updates.join(', ')} WHERE id = $${paramIndex}`,
|
||||
params
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ error: '项目不存在' });
|
||||
}
|
||||
|
||||
res.json({ message: '项目更新成功' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 协作规约 API ====================
|
||||
|
||||
// 设置协作规约
|
||||
app.post('/api/projects/:projectId/rules', async (req, res) => {
|
||||
const { projectId } = req.params;
|
||||
const { content, created_by } = req.body;
|
||||
|
||||
if (!content) {
|
||||
return res.status(400).json({ error: '规约内容不能为空' });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
const now = Date.now();
|
||||
|
||||
try {
|
||||
// 检查项目是否存在
|
||||
const projectCheck = await pool.query('SELECT id FROM projects WHERE id = $1', [projectId]);
|
||||
if (projectCheck.rows.length === 0) {
|
||||
return res.status(404).json({ error: '项目不存在' });
|
||||
}
|
||||
|
||||
// 删除旧规约(每个项目只保留一个)
|
||||
await pool.query('DELETE FROM collaboration_rules WHERE project_id = $1', [projectId]);
|
||||
|
||||
// 创建新规约
|
||||
await pool.query(
|
||||
`INSERT INTO collaboration_rules (id, project_id, content, created_by, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[id, projectId, content, created_by || 'human', now, now]
|
||||
);
|
||||
|
||||
res.json({
|
||||
id,
|
||||
message: '协作规约设置成功'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取协作规约
|
||||
app.get('/api/projects/:projectId/rules', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM collaboration_rules WHERE project_id = $1',
|
||||
[req.params.projectId]
|
||||
);
|
||||
res.json(result.rows[0] || null);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 信息共享清单 API ====================
|
||||
|
||||
// 添加共享笔记
|
||||
app.post('/api/projects/:projectId/notes', async (req, res) => {
|
||||
const { projectId } = req.params;
|
||||
const { content, created_by } = req.body;
|
||||
|
||||
if (!content) {
|
||||
return res.status(400).json({ error: '笔记内容不能为空' });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
const now = Date.now();
|
||||
|
||||
try {
|
||||
await pool.query(
|
||||
`INSERT INTO project_notes (id, project_id, content, created_by, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[id, projectId, content, created_by || 'human', now, now]
|
||||
);
|
||||
|
||||
res.json({
|
||||
id,
|
||||
message: '笔记添加成功'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取共享笔记列表
|
||||
app.get('/api/projects/:projectId/notes', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM project_notes WHERE project_id = $1 ORDER BY created_at DESC',
|
||||
[req.params.projectId]
|
||||
);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新共享笔记
|
||||
app.put('/api/projects/:projectId/notes/:noteId', async (req, res) => {
|
||||
const { noteId } = req.params;
|
||||
const { content } = req.body;
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'UPDATE project_notes SET content = $1, updated_at = $2 WHERE id = $3',
|
||||
[content, Date.now(), noteId]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ error: '笔记不存在' });
|
||||
}
|
||||
|
||||
res.json({ message: '笔记更新成功' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除共享笔记
|
||||
app.delete('/api/projects/:projectId/notes/:noteId', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'DELETE FROM project_notes WHERE id = $1',
|
||||
[req.params.noteId]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ error: '笔记不存在' });
|
||||
}
|
||||
|
||||
res.json({ message: '笔记删除成功' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 智能体 API ====================
|
||||
|
||||
// 注册智能体
|
||||
app.post('/api/agents/register', async (req, res) => {
|
||||
const { name, type, capabilities } = req.body;
|
||||
|
||||
if (!name) {
|
||||
return res.status(400).json({ error: '智能体名称不能为空' });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
const now = Date.now();
|
||||
|
||||
try {
|
||||
await pool.query(
|
||||
`INSERT INTO agents (id, name, type, capabilities, status, registered_at, last_heartbeat)
|
||||
VALUES ($1, $2, $3, $4, 'idle', $5, $6)`,
|
||||
[id, name, type || 'general', JSON.stringify(capabilities || []), now, now]
|
||||
);
|
||||
|
||||
res.json({
|
||||
id,
|
||||
name,
|
||||
type: type || 'general',
|
||||
status: 'idle',
|
||||
message: '智能体注册成功'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取所有智能体
|
||||
app.get('/api/agents', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query('SELECT * FROM agents ORDER BY registered_at DESC');
|
||||
res.json(result.rows.map(row => ({
|
||||
...row,
|
||||
capabilities: row.capabilities || []
|
||||
})));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新智能体状态(心跳)
|
||||
app.post('/api/agents/:id/heartbeat', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { status } = req.body;
|
||||
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'UPDATE agents SET status = $1, last_heartbeat = $2 WHERE id = $3',
|
||||
[status || 'idle', Date.now(), id]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ error: '智能体不存在' });
|
||||
}
|
||||
|
||||
res.json({ message: '心跳更新成功' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 任务管理 API ====================
|
||||
|
||||
// 创建任务(支持子任务)
|
||||
app.post('/api/tasks', async (req, res) => {
|
||||
const { project_id, parent_task_id, title, description, task_type, priority, created_by } = req.body;
|
||||
|
||||
if (!title) {
|
||||
return res.status(400).json({ error: '任务标题不能为空' });
|
||||
}
|
||||
|
||||
if (!project_id) {
|
||||
return res.status(400).json({ error: '必须指定项目ID' });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
const now = Date.now();
|
||||
|
||||
try {
|
||||
await pool.query(
|
||||
`INSERT INTO tasks (id, project_id, parent_task_id, title, description, task_type, priority, status, created_by, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, 'initial', $8, $9, $10)`,
|
||||
[id, project_id, parent_task_id || null, title, description || '',
|
||||
task_type || 'development', priority || 'normal', created_by || 'human', now, now]
|
||||
);
|
||||
|
||||
res.json({
|
||||
id,
|
||||
title,
|
||||
status: 'initial',
|
||||
message: '任务创建成功'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取任务列表(支持按项目、状态、父任务过滤)
|
||||
app.get('/api/tasks', async (req, res) => {
|
||||
const { project_id, status, parent_task_id } = req.query;
|
||||
|
||||
try {
|
||||
let query = 'SELECT * FROM tasks WHERE 1=1';
|
||||
const params = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (project_id) {
|
||||
query += ` AND project_id = $${paramIndex++}`;
|
||||
params.push(project_id);
|
||||
}
|
||||
|
||||
if (status) {
|
||||
query += ` AND status = $${paramIndex++}`;
|
||||
params.push(status);
|
||||
}
|
||||
|
||||
if (parent_task_id) {
|
||||
query += ` AND parent_task_id = $${paramIndex++}`;
|
||||
params.push(parent_task_id);
|
||||
} else if (req.query.root === 'true') {
|
||||
query += ' AND parent_task_id IS NULL';
|
||||
}
|
||||
|
||||
query += ' ORDER BY created_at DESC';
|
||||
|
||||
const result = await pool.query(query, params);
|
||||
res.json(result.rows);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取任务详情(包含子任务和附件)
|
||||
app.get('/api/tasks/:id', async (req, res) => {
|
||||
try {
|
||||
const taskResult = await pool.query('SELECT * FROM tasks WHERE id = $1', [req.params.id]);
|
||||
if (taskResult.rows.length === 0) {
|
||||
return res.status(404).json({ error: '任务不存在' });
|
||||
}
|
||||
|
||||
const task = taskResult.rows[0];
|
||||
|
||||
// 获取子任务
|
||||
const subtasksResult = await pool.query(
|
||||
'SELECT * FROM tasks WHERE parent_task_id = $1 ORDER BY created_at',
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
// 获取附件
|
||||
const attachmentsResult = await pool.query(
|
||||
'SELECT * FROM attachments WHERE task_id = $1 ORDER BY uploaded_at',
|
||||
[req.params.id]
|
||||
);
|
||||
|
||||
res.json({
|
||||
...task,
|
||||
subtasks: subtasksResult.rows,
|
||||
attachments: attachmentsResult.rows
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 领取任务
|
||||
app.post('/api/tasks/:id/claim', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { agent_id } = req.body;
|
||||
|
||||
if (!agent_id) {
|
||||
return res.status(400).json({ error: '需要提供智能体ID' });
|
||||
}
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const taskResult = await client.query('SELECT status, project_id FROM tasks WHERE id = $1', [id]);
|
||||
|
||||
if (taskResult.rows.length === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(404).json({ error: '任务不存在' });
|
||||
}
|
||||
|
||||
const task = taskResult.rows[0];
|
||||
|
||||
if (task.status !== 'initial' && task.status !== 'in_progress') {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(400).json({ error: '任务状态不允许领取' });
|
||||
}
|
||||
|
||||
// 获取项目的协作规约
|
||||
const rulesResult = await client.query(
|
||||
'SELECT content FROM collaboration_rules WHERE project_id = $1',
|
||||
[task.project_id]
|
||||
);
|
||||
|
||||
await client.query(
|
||||
'UPDATE tasks SET status = $1, assigned_to = $2, updated_at = $3 WHERE id = $4',
|
||||
['in_progress', agent_id, Date.now(), id]
|
||||
);
|
||||
|
||||
await client.query('UPDATE agents SET status = $1 WHERE id = $2', ['busy', agent_id]);
|
||||
|
||||
await client.query('COMMIT');
|
||||
|
||||
const response = {
|
||||
message: '任务领取成功'
|
||||
};
|
||||
|
||||
// 如果有协作规约,返回给智能体
|
||||
if (rulesResult.rows.length > 0) {
|
||||
response.collaboration_rules = rulesResult.rows[0].content;
|
||||
response.notice = '⚠️ 请严格遵守项目协作规约';
|
||||
}
|
||||
|
||||
res.json(response);
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
res.status(500).json({ error: err.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
// 更新任务状态
|
||||
app.put('/api/tasks/:id', async (req, res) => {
|
||||
const { id } = req.params;
|
||||
const { status, result, description, task_type, priority } = req.body;
|
||||
|
||||
const client = await pool.connect();
|
||||
try {
|
||||
await client.query('BEGIN');
|
||||
|
||||
const now = Date.now();
|
||||
const updates = [];
|
||||
const params = [];
|
||||
let paramIndex = 1;
|
||||
|
||||
if (status) {
|
||||
updates.push(`status = $${paramIndex++}`);
|
||||
params.push(status);
|
||||
}
|
||||
if (result !== undefined) {
|
||||
updates.push(`result = $${paramIndex++}`);
|
||||
params.push(result);
|
||||
}
|
||||
if (description !== undefined) {
|
||||
updates.push(`description = $${paramIndex++}`);
|
||||
params.push(description);
|
||||
}
|
||||
if (task_type) {
|
||||
updates.push(`task_type = $${paramIndex++}`);
|
||||
params.push(task_type);
|
||||
}
|
||||
if (priority) {
|
||||
updates.push(`priority = $${paramIndex++}`);
|
||||
params.push(priority);
|
||||
}
|
||||
if (status === 'completed') {
|
||||
updates.push(`completed_at = $${paramIndex++}`);
|
||||
params.push(now);
|
||||
}
|
||||
|
||||
updates.push(`updated_at = $${paramIndex++}`);
|
||||
params.push(now);
|
||||
params.push(id);
|
||||
|
||||
const updateResult = await client.query(
|
||||
`UPDATE tasks SET ${updates.join(', ')} WHERE id = $${paramIndex}`,
|
||||
params
|
||||
);
|
||||
|
||||
if (updateResult.rowCount === 0) {
|
||||
await client.query('ROLLBACK');
|
||||
return res.status(404).json({ error: '任务不存在' });
|
||||
}
|
||||
|
||||
// 如果任务完成,将智能体状态改为 idle
|
||||
if (status === 'completed') {
|
||||
const taskResult = await client.query('SELECT assigned_to FROM tasks WHERE id = $1', [id]);
|
||||
if (taskResult.rows.length > 0 && taskResult.rows[0].assigned_to) {
|
||||
await client.query('UPDATE agents SET status = $1 WHERE id = $2', ['idle', taskResult.rows[0].assigned_to]);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query('COMMIT');
|
||||
res.json({ message: '任务状态更新成功' });
|
||||
} catch (err) {
|
||||
await client.query('ROLLBACK');
|
||||
res.status(500).json({ error: err.message });
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 附件管理 API ====================
|
||||
|
||||
// 上传附件
|
||||
app.post('/api/tasks/:taskId/attachments', upload.single('file'), async (req, res) => {
|
||||
const { taskId } = req.params;
|
||||
const { uploaded_by } = req.body;
|
||||
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ error: '没有上传文件' });
|
||||
}
|
||||
|
||||
const id = uuidv4();
|
||||
const now = Date.now();
|
||||
|
||||
try {
|
||||
await pool.query(
|
||||
`INSERT INTO attachments (id, task_id, filename, original_name, file_path, file_size, mime_type, uploaded_by, uploaded_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
|
||||
[id, taskId, req.file.filename, req.file.originalname, req.file.path,
|
||||
req.file.size, req.file.mimetype, uploaded_by || 'human', now]
|
||||
);
|
||||
|
||||
res.json({
|
||||
id,
|
||||
filename: req.file.filename,
|
||||
original_name: req.file.originalname,
|
||||
url: `/uploads/${req.file.filename}`,
|
||||
message: '附件上传成功'
|
||||
});
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取任务附件列表
|
||||
app.get('/api/tasks/:taskId/attachments', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT * FROM attachments WHERE task_id = $1 ORDER BY uploaded_at DESC',
|
||||
[req.params.taskId]
|
||||
);
|
||||
res.json(result.rows.map(row => ({
|
||||
...row,
|
||||
url: `/uploads/${row.filename}`
|
||||
})));
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除附件
|
||||
app.delete('/api/tasks/:taskId/attachments/:attachmentId', async (req, res) => {
|
||||
try {
|
||||
const result = await pool.query(
|
||||
'SELECT file_path FROM attachments WHERE id = $1',
|
||||
[req.params.attachmentId]
|
||||
);
|
||||
|
||||
if (result.rows.length === 0) {
|
||||
return res.status(404).json({ error: '附件不存在' });
|
||||
}
|
||||
|
||||
// 删除文件
|
||||
const filePath = result.rows[0].file_path;
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
|
||||
// 删除数据库记录
|
||||
await pool.query('DELETE FROM attachments WHERE id = $1', [req.params.attachmentId]);
|
||||
|
||||
res.json({ message: '附件删除成功' });
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== 统计信息 API ====================
|
||||
|
||||
app.get('/api/stats', async (req, res) => {
|
||||
try {
|
||||
const stats = {
|
||||
total_agents: 0,
|
||||
active_agents: 0,
|
||||
total_projects: 0,
|
||||
total_tasks: 0,
|
||||
tasks_by_status: {},
|
||||
tasks_by_type: {}
|
||||
};
|
||||
|
||||
const results = await Promise.all([
|
||||
pool.query('SELECT COUNT(*) as count FROM agents'),
|
||||
pool.query('SELECT COUNT(*) as count FROM agents WHERE status = $1', ['busy']),
|
||||
pool.query('SELECT COUNT(*) as count FROM projects'),
|
||||
pool.query('SELECT COUNT(*) as count FROM tasks'),
|
||||
pool.query('SELECT status, COUNT(*) as count FROM tasks GROUP BY status'),
|
||||
pool.query('SELECT task_type, COUNT(*) as count FROM tasks GROUP BY task_type')
|
||||
]);
|
||||
|
||||
stats.total_agents = parseInt(results[0].rows[0].count);
|
||||
stats.active_agents = parseInt(results[1].rows[0].count);
|
||||
stats.total_projects = parseInt(results[2].rows[0].count);
|
||||
stats.total_tasks = parseInt(results[3].rows[0].count);
|
||||
|
||||
results[4].rows.forEach(row => {
|
||||
stats.tasks_by_status[row.status] = parseInt(row.count);
|
||||
});
|
||||
|
||||
results[5].rows.forEach(row => {
|
||||
stats.tasks_by_type[row.task_type] = parseInt(row.count);
|
||||
});
|
||||
|
||||
res.json(stats);
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', async (req, res) => {
|
||||
try {
|
||||
await pool.query('SELECT 1');
|
||||
res.json({ status: 'healthy', database: 'connected' });
|
||||
} catch (err) {
|
||||
res.status(503).json({ status: 'unhealthy', error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
// 获取本机 IP 地址
|
||||
function getLocalIP() {
|
||||
const interfaces = os.networkInterfaces();
|
||||
for (const name of Object.keys(interfaces)) {
|
||||
for (const iface of interfaces[name]) {
|
||||
if (iface.family === 'IPv4' && !iface.internal) {
|
||||
return iface.address;
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'localhost';
|
||||
}
|
||||
|
||||
// 启动服务器
|
||||
app.listen(PORT, '0.0.0.0', () => {
|
||||
const localIP = getLocalIP();
|
||||
console.log(`🚀 多智能体任务服务已启动 (v2.0)`);
|
||||
console.log(` 本地访问: http://localhost:${PORT}`);
|
||||
console.log(` 远程访问: http://${localIP}:${PORT}`);
|
||||
});
|
||||
|
||||
// 优雅关闭
|
||||
process.on('SIGINT', async () => {
|
||||
console.log('\n正在关闭服务...');
|
||||
await pool.end();
|
||||
console.log('数据库连接已关闭');
|
||||
process.exit(0);
|
||||
});
|
||||
Reference in New Issue
Block a user