diff --git a/API-V2.md b/API-V2.md new file mode 100644 index 0000000..a5c3648 --- /dev/null +++ b/API-V2.md @@ -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. **协作规约强制推送** - 智能体领取任务时自动获取规约 diff --git a/package-lock.json b/package-lock.json index f3b6b2a..9b48d52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index d7deb4a..5953c63 100644 --- a/package.json +++ b/package.json @@ -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" diff --git a/server-v2.js b/server-v2.js new file mode 100644 index 0000000..755d1a9 --- /dev/null +++ b/server-v2.js @@ -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); +}); \ No newline at end of file