/** * requirement-generator-v1 Skill Web 服务器 * * 功能: * 1. 通过 SDK 调用 requirement-generator-v1 Skill * 2. 支持 AskUserQuestion 交互 * 3. 支持多轮对话 * 4. 文档展示和下载 */ import express from 'express'; import { query } from '@anthropic-ai/claude-agent-sdk'; import path from 'path'; import { fileURLToPath } from 'url'; import fs from 'fs'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Skills 项目根目录(包含 .claude/skills/ 和 .claude/agents/) const SKILLS_PROJECT_DIR = path.resolve(__dirname, '..'); const app = express(); const PORT = 3457; app.use(express.json()); app.use(express.static(path.join(__dirname, 'public'))); // 存储待处理的 AskUserQuestion 请求 const pendingQuestions = new Map(); // 存储会话信息 const sessions = new Map(); /** * SSE 流式执行 Skill * GET /api/stream?prompt=xxx&sessionId=xxx */ app.get('/api/stream', async (req, res) => { const { prompt, sessionId } = req.query; const requestId = Date.now().toString(); const existingClaudeSessionId = sessionId ? sessions.get(sessionId) : null; console.log('\n========================================'); console.log('[Server] 收到请求'); console.log('[Server] Prompt:', prompt); console.log('[Server] SessionId:', existingClaudeSessionId || '(新会话)'); console.log('[Server] CWD:', SKILLS_PROJECT_DIR); console.log('========================================\n'); res.setHeader('Content-Type', 'text/event-stream'); res.setHeader('Cache-Control', 'no-cache'); res.setHeader('Connection', 'keep-alive'); res.setHeader('X-Request-Id', requestId); const sendEvent = (type, data) => { res.write(`data: ${JSON.stringify({ type, ...data })}\n\n`); }; try { const options = { // 项目目录 - 必须包含 .claude/skills/ 和 .claude/agents/ cwd: SKILLS_PROJECT_DIR, // 权限模式 - 自动执行所有工具 permissionMode: 'bypassPermissions', // 启用 Skill 和 Agent 加载 settingSources: ['user', 'project'], // 不指定 allowedTools,让 SDK 使用默认值(允许所有工具) // 继续会话 ...(existingClaudeSessionId && { resume: existingClaudeSessionId }), // 处理 AskUserQuestion canUseTool: async (toolName, input) => { console.log(`[SDK] canUseTool: ${toolName}`); if (toolName === 'AskUserQuestion') { sendEvent('ask_user_question', { requestId, questions: input.questions }); return new Promise((resolve) => { pendingQuestions.set(requestId, (answers) => { console.log('[SDK] 收到用户回答:', answers); resolve({ behavior: 'allow', updatedInput: { questions: input.questions, answers: answers } }); }); // 60分钟超时 setTimeout(() => { if (pendingQuestions.has(requestId)) { console.log('[SDK] 等待用户回答超时 (60分钟)'); pendingQuestions.delete(requestId); resolve({ behavior: 'deny' }); } }, 3600000); }); } return { behavior: 'allow' }; } }; sendEvent('status', { message: existingClaudeSessionId ? `继续会话 ${existingClaudeSessionId.substring(0, 8)}...` : '正在启动 Skill...' }); const response = query({ prompt: prompt || '生成需求文档', options }); let currentClaudeSessionId = existingClaudeSessionId; for await (const message of response) { console.log('\n[SDK] ────────────────────────────'); console.log('[SDK] Message type:', message.type); if (message.subtype) console.log('[SDK] Subtype:', message.subtype); // 详细日志 if (message.type === 'assistant' && message.message?.content) { const content = message.message.content; if (Array.isArray(content)) { content.forEach((block, i) => { if (block.type === 'text') { const text = block.text.substring(0, 200); console.log(`[SDK] Content[${i}] text:`, text + (block.text.length > 200 ? '...' : '')); } else if (block.type === 'tool_use') { console.log(`[SDK] Content[${i}] tool_use:`, block.name); console.log('[SDK] Input:', JSON.stringify(block.input, null, 2).substring(0, 500)); } }); } else if (typeof content === 'string') { console.log('[SDK] Content:', content.substring(0, 200)); } } if (message.type === 'user' && message.message?.content) { const content = message.message.content; if (Array.isArray(content)) { content.forEach((block, i) => { if (block.type === 'tool_result') { const result = typeof block.content === 'string' ? block.content.substring(0, 300) : JSON.stringify(block.content).substring(0, 300); console.log(`[SDK] ToolResult[${i}]:`, result + '...'); } }); } } if (message.type === 'system' && message.subtype === 'init') { currentClaudeSessionId = message.session_id; console.log('[SDK] Session ID:', currentClaudeSessionId); const clientSessionId = sessionId || currentClaudeSessionId; sessions.set(clientSessionId, currentClaudeSessionId); sendEvent('session', { clientSessionId, claudeSessionId: currentClaudeSessionId, isNewSession: !existingClaudeSessionId }); } else if (message.type === 'stream_event') { const event = message.event || {}; if (event.type === 'content_block_delta') { const delta = event.delta; let text = ''; if (typeof delta === 'string') { text = delta; } else if (delta?.text) { text = delta.text; } if (text) { sendEvent('delta', { text }); } } else if (event.type === 'content_block_start') { const block = event.content_block; if (block?.type === 'tool_use') { sendEvent('tool_start', { toolName: block.name, toolId: block.id }); } } } else if (message.type === 'assistant') { let content = ''; const toolCalls = []; if (typeof message.message?.content === 'string') { content = message.message.content; } else if (Array.isArray(message.message?.content)) { for (const block of message.message.content) { if (block.type === 'text') { content += block.text; } else if (block.type === 'tool_use') { toolCalls.push({ name: block.name, id: block.id, input: block.input }); } } } if (content) { sendEvent('assistant', { content }); } if (toolCalls.length > 0) { sendEvent('tool_calls', { tools: toolCalls }); } } else if (message.type === 'result') { sendEvent('result', { subtype: message.subtype, sessionId: currentClaudeSessionId }); } } sendEvent('done', { message: '执行完成', sessionId: currentClaudeSessionId }); } catch (error) { console.error('[Server] 错误:', error); sendEvent('error', { message: error.message }); } finally { pendingQuestions.delete(requestId); res.end(); } }); /** * 提交用户回答 * POST /api/answer */ app.post('/api/answer', (req, res) => { const { requestId, answers } = req.body; console.log('[Server] 收到用户回答:', { requestId, answers }); const resolver = pendingQuestions.get(requestId); if (resolver) { resolver(answers); pendingQuestions.delete(requestId); res.json({ success: true }); } else { res.status(404).json({ error: '未找到待处理的问题' }); } }); /** * 获取文档内容 * GET /api/document?type=final|draft */ app.get('/api/document', (req, res) => { const type = req.query.type || 'final'; // 优先查找 requirement_final.md,否则 requirement.md const finalPath = path.join(SKILLS_PROJECT_DIR, 'requirement_final.md'); const draftPath = path.join(SKILLS_PROJECT_DIR, 'requirement.md'); let filePath; let docType; if (type === 'final' && fs.existsSync(finalPath)) { filePath = finalPath; docType = 'final'; } else if (fs.existsSync(draftPath)) { filePath = draftPath; docType = 'draft'; } else { return res.status(404).json({ error: '文档不存在' }); } try { const content = fs.readFileSync(filePath, 'utf-8'); res.json({ type: docType, filename: path.basename(filePath), content }); } catch (error) { res.status(500).json({ error: '读取文档失败: ' + error.message }); } }); /** * 下载文档 * GET /api/download?type=final|draft */ app.get('/api/download', (req, res) => { const type = req.query.type || 'final'; const finalPath = path.join(SKILLS_PROJECT_DIR, 'requirement_final.md'); const draftPath = path.join(SKILLS_PROJECT_DIR, 'requirement.md'); let filePath; if (type === 'final' && fs.existsSync(finalPath)) { filePath = finalPath; } else if (fs.existsSync(draftPath)) { filePath = draftPath; } else { return res.status(404).json({ error: '文档不存在' }); } res.download(filePath); }); /** * 获取会话列表 */ app.get('/api/sessions', (req, res) => { const sessionList = []; for (const [clientId, claudeId] of sessions.entries()) { sessionList.push({ clientId, claudeId }); } res.json({ sessions: sessionList }); }); app.listen(PORT, () => { console.log(` ======================================== 需求文档生成器 Web 服务已启动 地址: http://localhost:${PORT} ======================================== 项目目录: ${SKILLS_PROJECT_DIR} Skill: requirement-generator-v1 使用前请确保已安装依赖: npm install `); });